Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gif previews/thumbnails #749

Closed
Orbyt opened this issue Nov 17, 2015 · 19 comments
Closed

Gif previews/thumbnails #749

Orbyt opened this issue Nov 17, 2015 · 19 comments
Labels

Comments

@Orbyt
Copy link

Orbyt commented Nov 17, 2015

Hey, was wondering how to get gif thumbnails working? Basically I have a recyclerview of card items containing a gif, and would like a preview image/thumbnail image of the gif to load with the view.

@TWiStErRob
Copy link
Collaborator

Uhmm, I think it's:

Glide
    .with(context)
    .load(gifUrl)
    .thumbnail(Glide
         .with(context)
         .load(gifUrl)
         .asBitmap()
    )
    .into(imageView)
;

or in case you just meant thumbnails as small images in a list, just use .asBitmap() after load to load first frame, like in the thumbnail call above.

@Orbyt
Copy link
Author

Orbyt commented Nov 17, 2015

Using

Glide.with(mContext)
    .load(URL)
    .thumbnail(Glide.with(mContext).load(URL).asBitmap())
    .into(imageView);

like you posted above gives a:

Cannot resolve method thumbnail(com.bumptech.glide.BitmapTypeRequest)

Adding just a .asBitmap after .load and omitting the thumbnail code seems to work for loading the first frame, but im guessing it downloads the entire gif first before displaying the first frame, as I just tested with a couple gifs and some of the previews didnt load for a good 7+ seconds, which isnt useful in a list. Is there a reason for this, or a way to do this better?

@TWiStErRob
Copy link
Collaborator

Huh, sorry, I thought bitmap thumbs are handled (see #107), I wrote the code on my phone. Here's a compiling one which should also display much-much faster (see #281 and related issues):

Glide
    .with(context)
    .load(url)
    .diskCacheStrategy(DiskCacheStrategy.SOURCE)
    .thumbnail(Glide
        .with(context)
        .load(url)
        .asBitmap()
        .transcode(new BitmapToGlideDrawableTranscoder(context), GlideDrawable.class)
        .diskCacheStrategy(DiskCacheStrategy.ALL)
    )
    .into(imageView)
;

You can also try NONE instead of ALL to decode the first frame directly from the HTTP stream, but remember that then there'll be two streams: 1 for thumbnail (first frame) and 1 for full GIF. It'll probably show faster with NONE regardless of more networking. Remember that this only applies for the first load, consequtive loads (scrolling back and forth for example) hitting the RESULT cache (included in ALL) or even memory cache should show the thumbnail extremely fast.

Hmm, thinking a little more: you may not even need the thumbnail call, because it should decode the first frame at the same speed. You may get a faster thubmnail load if you use .override to ask to load that first frame in a smaller size. Note that the default is a custom GIF decoder and asBitmap uses BitmapFactory which may be faster (native) for decoding first frames.

@Orbyt
Copy link
Author

Orbyt commented Nov 18, 2015

Ok, so I messed around with a bunch of different configurations. None of the options I tried allowed the gifs to load fast enough to be displayed in a list, even using small gifs. The fastest configuration I found to get anything to show up in the list items was:

Glide.with(mContext)
        .load(URL)
     // .asBitmap()
     // .transcode(new BitmapToGlideDrawableTranscoder(mContext), GlideDrawable.class)
        .diskCacheStrategy(DiskCacheStrategy.SOURCE)
        .thumbnail(Glide
            .with(mContext)
            .load(URL)
            .asBitmap()
            .transcode(new BitmapToGlideDrawableTranscoder(mContext), GlideDrawable.class)
            .diskCacheStrategy(DiskCacheStrategy.NONE)
        )
        .into(holder.clipImageView)
;

This at least gets something to show up quickly, but, they still start animating after awhile, and since its not quick enough for that id rather grab the first frame and just use that for a preview. How can i stop it from animating so that i can use just the first frame?

@TWiStErRob
Copy link
Collaborator

Just use what you passed into thumbnail and call into on it. You can get rid of transcode, it is there to ensure compatibility with the outer load, which is not there any more.

Also consider changing caching to SOURCE so if you allow the user to start the animation by click for example it'll be faster. If that's too slow for initial load you can try to add a .listener() and call downloadOnly so it starts to fill the cache after the preview has been shown, but doesn't start the animation.

@TWiStErRob
Copy link
Collaborator

Note that no picture loader library would be able to make the Internet faster for you :) I think the fastest trick is to decode directly from the network stream and just the first frame.
Also check out the Glide giphy sample, there may be some tricks there I don't know much about, like preloading. There's a compiled apk on the releases page.

@Orbyt
Copy link
Author

Orbyt commented Nov 18, 2015

@TWiStErRob

Just use what you passed into thumbnail and call into on it. You can get rid of transcode, it is there to ensure compatibility with the outer load, which is not there any more.
Also consider changing caching to SOURCE so if you allow the user to start the animation by click for example it'll be faster. If that's too slow for initial load you can try to add a .listener() and call downloadOnly so it starts to fill the cache after the preview has been shown, but doesn't start the animation.

If you have a minute, could you provide an example of what your talking about here^? If i remove the .transcode() inside the thumbnail it shows an error for the thumbnail method.

Especially not sure what you mean here:

Just use what you passed into thumbnail and call into on it.

@TWiStErRob
Copy link
Collaborator

There are two Glide.with.... blocks, use the one between thumbnails parentheses (remove the outer code), call into() on that directly:

Glide
    .with(mContext)
    .load(URL)
    .asBitmap() // load first frame only
    //.transcode(new BitmapToGlideDrawableTranscoder(mContext), GlideDrawable.class)
    .diskCacheStrategy(DiskCacheStrategy.RESULT) // directly from network, but save first frame to cache so next time it's faster
    .listener(...) // call downloadOnly when success
    .into(holder.clipImageView)

@Orbyt
Copy link
Author

Orbyt commented Nov 18, 2015

@TWiStErRob Ok, so if im following you correctly, your saying use this correct?:

 Glide.with(mContext)
    .load(URL)
    .asBitmap()
    .diskCacheStrategy(DiskCacheStrategy.NONE)
    .into(holder.clipImageView)
;

Using NONE instead of RESULT loads the first frame about 3x faster. Is there a way to get something like .crossFade working so it loads up smoothly? It throws errors if trying to use .crossFade and .asBitmap at the same time.

Also, couldnt seems to find any docs on using .listener, but im guessing youd use this to initiate full playback or something?

@TWiStErRob
Copy link
Collaborator

3x: what about the next infinitely many times?
listener: read the javadoc of that method and RequestListener class and methods
crossFade: you would have to roll your own animation

@Orbyt
Copy link
Author

Orbyt commented Nov 18, 2015

@TWiStErRob

3x: what about the next infinitely many times?

Hmm not sure, will test in a bit. Anyway thanks! I'll probably load the static previews, and then load whatever i need in a separate activity when clicked.

I'll leave this open for a bit if anybody else wants to chime in.

@TWiStErRob
Copy link
Collaborator

Closing this as your issue seems to be resolved.
Anyone should feel free to comment on a closed issue as well if they found anything related.

@KimiChiu
Copy link

Hi,
Is there a way to only process the first frame of the GIF file?
I mean, like Picasso, they don't need to download the whole file to show the first frame, which means much faster to display it on the screen.
But Picasso doesn't support playing GIF files, Glide do.
Picasso doesn't share cache to Glide, and vice versa. So we should not use two libraries to prevent the file be downloaded twice.
How do we solve this problem?

@KimiChiu
Copy link

Hi again,
It turns out that it can use the same file cache when I choose to use the same okhttp client for both Picasso and Glide.
Works like a charm.

@TWiStErRob
Copy link
Collaborator

@KimiChiu if you use OkHttp for caching, then you're portentially duplicating files in OkHttp cache and Picasso/Glide cache unless you disable the image loaders' disk caches (Glide: .diskCacheStrategy(NONE) or the default RESULT may also prevent full duplication).

As for first frame only: I think if you use .asBitmap without SOURCE cache enabled, it should behave as Picasso, because using RESULT or NONE cache will force reading directly from a network stream and .asBitmap will force using a BitmapFactory which can only load a single frame and very likely won't bother reading more than header+first frame from the stream.

@KimiChiu
Copy link

@TWiStErRob It stop caching files occasionally after I changed the diskCacheStrategy to NONE. And It will download the same image again if I display another image in a different size with .diskCacheStrategy(RESULT). And tThe cache size seems okay. If Picasso and Glide can share cache files between each other(which means it doesn't download images again), why would the files be duplicated?

Here's what I did:

OkHttpClient okHttpClient = new OkHttpClient();
File cacheFile = new File(getCacheDir(), "okImageCaches");
if( !cacheFile.exists() ) cacheFile.mkdirs();
Cache cache = new Cache(cacheFile, 1024 * 1024 * 100);
okHttpClient.setCache(cache);
okHttpClient.setConnectTimeout(10, TimeUnit.SECONDS);
okHttpClient.setWriteTimeout(10, TimeUnit.SECONDS);
okHttpClient.setReadTimeout(10, TimeUnit.SECONDS);

OkHttpDownloader downloader = new OkHttpDownloader(okHttpClient);

Picasso.Builder builder = new Picasso.Builder(this);
builder.downloader( downloader );
Picasso built = builder.build();
built.setIndicatorsEnabled(true);
//        built.setLoggingEnabled(true);
Picasso.setSingletonInstance(built);

Glide.get(this)
        .register(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(okHttpClient) );

@TWiStErRob
Copy link
Collaborator

TWiStErRob commented Dec 27, 2016

@KimiChiu looks like you have a good grasp over caches. Using NONE or RESULT is the way to go if you want to use OkHttp caches.

My point was: if you used SOURCE/ALL cache in Glide, or whatever the equivalent in Picasso is, and also enable OkHttp cache, then if you load the same image URL in both image loaders you'll get 3 files on the device: 1 in OkHttp cache, 1 in Glide SOURCE cache, 1 in Picasso's cache. They'll be the same byte by byte.

if I display another image in a different size with .diskCacheStrategy(RESULT)

In that use case RESULT is simply not the right strategy, ALL or SOURCE is. First load will populate the SOURCE cache and second load will use that cached version. In case of ALL you'll also get two downsized images in cache which will result in faster load times in the future. It's good for example if you show the current user's photo in 2-3 different sizes.

@KimiChiu
Copy link

KimiChiu commented Apr 3, 2017

I just found a download progress implementation from https://gist.github.com/TWiStErRob/08d5807e396740e52c90
I was wondering, is it possible to check the Content-Type first before it downloads the whole file?
If it is a GIF, then use the DiskStrategy.RESULT, otherwise use the DiskStrategy.SOURCE.
I tried to put these codes in the source method:

private Source source(Source source) {
    return new ForwardingSource(source) {
        long totalBytesRead = 0L;
        @Override public long read(Buffer sink, long byteCount) throws IOException {
            if( responseBody.contentType() != null && responseBody.contentType().toString().contains("/gif")){
                // tells the target it needs to be reloaded
                progressListener.reload(url);
                // interrupt downloading
                throw new ReloadForGIFException("GIF redo download.");
            }
            long bytesRead = super.read(sink, byteCount);
            long fullLength = responseBody.contentLength();
            if (bytesRead == -1) { // this source is exhausted
                totalBytesRead = fullLength;
            } else {
                totalBytesRead += bytesRead;
            }
            progressListener.update(url, totalBytesRead, fullLength);
            return bytesRead;
        }
    };
}

And do the reloads in Glide's onException.
But I don't think using an Exception to control the flow is a good idea.
I also tried to put these codes in NetworkInterceptor, but it sometimes returns an empty Content-Type.
Is there a better place to do this inspection?

@TWiStErRob
Copy link
Collaborator

@KimiChiu Interesting approach. The problem is that the cache strategy has to be defined before the load is started, so currently this exception to control the flow is the one that works. You could try the other type of interceptor for OkHttp. Another possibility is to have the "gif" / "not gif" flag ready when you start the load; wherever you get the image urls from could provide that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants