package com.bumptech.glide.load.resource.bitmap; import android.content.Context; import android.graphics.Bitmap; import android.media.MediaMetadataRetriever; import android.os.ParcelFileDescriptor; import com.bumptech.glide.Glide; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import java.io.IOException; import java.nio.ByteBuffer; import java.security.MessageDigest; /** * An {@link com.bumptech.glide.load.ResourceDecoder} that can decode a thumbnail frame * {@link android.graphics.Bitmap} from a {@link android.os.ParcelFileDescriptor} containing a * video. * * @see android.media.MediaMetadataRetriever */ public class VideoBitmapDecoder implements ResourceDecoder<ParcelFileDescriptor, Bitmap> { /** * A constant indicating we should use whatever frame we consider best, frequently not the first * frame. */ public static final long DEFAULT_FRAME = -1; /** * A long indicating the time position (in microseconds) of the target frame which will be * retrieved. {@link android.media.MediaMetadataRetriever#getFrameAtTime(long)} is used to * extract the video frame. * * <p>When retrieving the frame at the given time position, there is no guarantee that the data * source has a frame located at the position. When this happens, a frame nearby will be returned. * If the long is negative, time position and option will ignored, and any frame that the * implementation considers as representative may be returned. */ public static final Option<Long> TARGET_FRAME = Option.disk( "com.bumptech.glide.load.resource.bitmap.VideoBitmapDecode.TargetFrame", DEFAULT_FRAME, new Option.CacheKeyUpdater<Long>() { private final ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE); @Override public void update(byte[] keyBytes, Long value, MessageDigest messageDigest) { messageDigest.update(keyBytes); synchronized (buffer) { buffer.position(0); messageDigest.update(buffer.putLong(value).array()); } } }); /** * An integer indicating the frame option used to retrieve a target frame. * * <p>This option will be ignored if {@link #TARGET_FRAME} is not set or is set to * {@link #DEFAULT_FRAME}. * * @see MediaMetadataRetriever#getFrameAtTime(long, int) */ public static final Option<Integer> FRAME_OPTION = Option.disk( "com.bumptech.glide.load.resource.bitmap.VideoBitmapDecode.FrameOption", null /*defaultValue*/, new Option.CacheKeyUpdater<Integer>() { private final ByteBuffer buffer = ByteBuffer.allocate(Integer.SIZE / Byte.SIZE); @Override public void update(byte[] keyBytes, Integer value, MessageDigest messageDigest) { if (value == null) { return; } messageDigest.update(keyBytes); synchronized (buffer) { buffer.position(0); messageDigest.update(buffer.putInt(value).array()); } } } ); private static final MediaMetadataRetrieverFactory DEFAULT_FACTORY = new MediaMetadataRetrieverFactory(); private final BitmapPool bitmapPool; private final MediaMetadataRetrieverFactory factory; public VideoBitmapDecoder(Context context) { this(Glide.get(context).getBitmapPool()); } public VideoBitmapDecoder(BitmapPool bitmapPool) { this(bitmapPool, DEFAULT_FACTORY); } // Visible for testing. VideoBitmapDecoder(BitmapPool bitmapPool, MediaMetadataRetrieverFactory factory) { this.bitmapPool = bitmapPool; this.factory = factory; } @Override public boolean handles(ParcelFileDescriptor data, Options options) { MediaMetadataRetriever retriever = factory.build(); try { retriever.setDataSource(data.getFileDescriptor()); return true; } catch (RuntimeException e) { // Throws a generic runtime exception when given invalid data. return false; } finally { retriever.release(); } } @Override public Resource<Bitmap> decode(ParcelFileDescriptor resource, int outWidth, int outHeight, Options options) throws IOException { long frameTimeMicros = options.get(TARGET_FRAME); if (frameTimeMicros < 0 && frameTimeMicros != DEFAULT_FRAME) { throw new IllegalArgumentException( "Requested frame must be non-negative, or DEFAULT_FRAME, given: " + frameTimeMicros); } Integer frameOption = options.get(FRAME_OPTION); final Bitmap result; MediaMetadataRetriever mediaMetadataRetriever = factory.build(); try { mediaMetadataRetriever.setDataSource(resource.getFileDescriptor()); if (frameTimeMicros == DEFAULT_FRAME) { result = mediaMetadataRetriever.getFrameAtTime(); } else if (frameOption == null) { result = mediaMetadataRetriever.getFrameAtTime(frameTimeMicros); } else { result = mediaMetadataRetriever.getFrameAtTime(frameTimeMicros, frameOption); } } finally { mediaMetadataRetriever.release(); } resource.close(); return BitmapResource.obtain(result, bitmapPool); } // Visible for testing. static class MediaMetadataRetrieverFactory { public MediaMetadataRetriever build() { return new MediaMetadataRetriever(); } } }