Skip to content

Commit

Permalink
Support getScaledFrameAtTime.
Browse files Browse the repository at this point in the history
  • Loading branch information
sjudd committed Dec 27, 2017
1 parent 6e9866b commit ded8f77
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 20 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ JUNIT_VERSION=4.13-SNAPSHOT
# Matches the version in Google.
MOCKITO_VERSION=1.9.5
MOCKITO_ANDROID_VERSION=2.11.0
ROBOLECTRIC_VERSION=3.3.2
ROBOLECTRIC_VERSION=3.6.1
MOCKWEBSERVER_VERSION=3.0.0-RC1
TRUTH_VERSION=0.36
JSR_305_VERSION=3.0.2
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package com.bumptech.glide.load.resource.bitmap;

import android.annotation.TargetApi;
import android.content.res.AssetFileDescriptor;
import android.graphics.Bitmap;
import android.media.MediaMetadataRetriever;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import com.bumptech.glide.load.Option;
import com.bumptech.glide.load.Options;
Expand All @@ -30,6 +35,10 @@ public class VideoDecoder<T> implements ResourceDecoder<T, Bitmap> {
*/
public static final long DEFAULT_FRAME = -1;

/** Matches the behavior of {@link MediaMetadataRetriever#getFrameAtTime(long)}. */
@VisibleForTesting
static final int DEFAULT_FRAME_OPTION = MediaMetadataRetriever.OPTION_CLOSEST_SYNC;

/**
* 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
Expand Down Expand Up @@ -66,7 +75,7 @@ public void update(byte[] keyBytes, Long value, MessageDigest messageDigest) {
@SuppressWarnings("WeakerAccess")
public static final Option<Integer> FRAME_OPTION = Option.disk(
"com.bumptech.glide.load.resource.bitmap.VideoBitmapDecode.FrameOption",
null /*defaultValue*/,
/*defaultValue=*/ MediaMetadataRetriever.OPTION_CLOSEST_SYNC,
new Option.CacheKeyUpdater<Integer>() {
private final ByteBuffer buffer = ByteBuffer.allocate(Integer.SIZE / Byte.SIZE);
@Override
Expand Down Expand Up @@ -114,43 +123,59 @@ public static ResourceDecoder<ParcelFileDescriptor, Bitmap> parcel(BitmapPool bi
}

@Override
public boolean handles(T data, Options options) {
public boolean handles(@NonNull T data, @NonNull Options options) {
// Calling setDataSource is expensive so avoid doing so unless we're actually called.
// For non-videos this isn't any cheaper, but for videos it safes the redundant call and
// 50-100ms.
return true;
}

@Override
public Resource<Bitmap> decode(T resource, int outWidth, int outHeight,
Options options) throws IOException {
public Resource<Bitmap> decode(
@NonNull T resource, int outWidth, int outHeight, @NonNull 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);
if (frameOption == null) {
frameOption = DEFAULT_FRAME_OPTION;
}

final Bitmap result;
MediaMetadataRetriever mediaMetadataRetriever = factory.build();
try {
initializer.initialize(mediaMetadataRetriever, resource);
if (frameTimeMicros == DEFAULT_FRAME) {
result = mediaMetadataRetriever.getFrameAtTime();
} else if (frameOption == null) {
result = mediaMetadataRetriever.getFrameAtTime(frameTimeMicros);
} else {
result = mediaMetadataRetriever.getFrameAtTime(frameTimeMicros, frameOption);
}
result =
decodeFrame(mediaMetadataRetriever, frameTimeMicros, frameOption, outWidth, outHeight);
} catch (RuntimeException e) {
// MediaMetadataRetriever APIs throw generic runtime exceptions when given invalid data.
throw new IOException(e);
} finally {
mediaMetadataRetriever.release();
}

return BitmapResource.obtain(result, bitmapPool);
}

@TargetApi(Build.VERSION_CODES.O_MR1)
@Nullable
private static Bitmap decodeFrame(
MediaMetadataRetriever mediaMetadataRetriever,
long frameTimeMicros,
int frameOption,
int outWidth,
int outHeight) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.O_MR1) {
return mediaMetadataRetriever.getScaledFrameAtTime(
frameTimeMicros, frameOption, outWidth, outHeight);
} else {
return mediaMetadataRetriever.getFrameAtTime(frameTimeMicros, frameOption);
}
}

@VisibleForTesting
static class MediaMetadataRetrieverFactory {
public MediaMetadataRetriever build() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ void setIsLowRam() {
}

@Implementation
@Override
public boolean isLowRamDevice() {
return isLowRam;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
package com.bumptech.glide.load.resource.bitmap;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.graphics.Bitmap;
import android.media.MediaMetadataRetriever;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.tests.Util;
import com.bumptech.glide.util.Preconditions;
import java.io.IOException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand All @@ -23,7 +25,7 @@
import org.robolectric.annotation.Config;

@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, sdk = 18)
@Config(manifest = Config.NONE, sdk = 27)
public class VideoDecoderTest {
@Mock private ParcelFileDescriptor resource;
@Mock private VideoDecoder.MediaMetadataRetrieverFactory factory;
Expand All @@ -32,19 +34,29 @@ public class VideoDecoderTest {
@Mock private BitmapPool bitmapPool;
private VideoDecoder<ParcelFileDescriptor> decoder;
private Options options;
private int initialSdkVersion;

@Before
public void setup() {
MockitoAnnotations.initMocks(this);
when(factory.build()).thenReturn(retriever);
decoder = new VideoDecoder<>(bitmapPool, initializer, factory);
options = new Options();

initialSdkVersion = Build.VERSION.SDK_INT;
}

@After
public void tearDown() {
Util.setSdkVersionInt(initialSdkVersion);
}

@Test
public void testReturnsRetrievedFrameForResource() throws IOException {
Util.setSdkVersionInt(19);
Bitmap expected = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
when(retriever.getFrameAtTime()).thenReturn(expected);
when(retriever.getFrameAtTime(VideoDecoder.DEFAULT_FRAME, VideoDecoder.DEFAULT_FRAME_OPTION))
.thenReturn(expected);

Resource<Bitmap> result =
Preconditions.checkNotNull(decoder.decode(resource, 100, 100, options));
Expand All @@ -55,35 +67,46 @@ public void testReturnsRetrievedFrameForResource() throws IOException {

@Test
public void testReleasesMediaMetadataRetriever() throws IOException {
Util.setSdkVersionInt(19);
decoder.decode(resource, 1, 2, options);

verify(retriever).release();
}

@Test(expected = IllegalArgumentException.class)
public void testThrowsExceptionIfCalledWithInvalidFrame() throws IOException {
Util.setSdkVersionInt(19);
options.set(VideoDecoder.TARGET_FRAME, -5L);
new VideoDecoder<>(bitmapPool, initializer, factory).decode(resource, 100, 100, options);
}

@Test
public void testSpecifiesThumbnailFrameIfICalledWithFrameNumber() throws IOException {
Util.setSdkVersionInt(19);
long frame = 5;
options.set(VideoDecoder.TARGET_FRAME, frame);
decoder = new VideoDecoder<>(bitmapPool, initializer, factory);

decoder.decode(resource, 100, 100, options);

verify(retriever).getFrameAtTime(frame);
verify(retriever, never()).getFrameAtTime();
verify(retriever).getFrameAtTime(frame, VideoDecoder.DEFAULT_FRAME_OPTION);
}

@Test
public void testDoesNotSpecifyThumbnailFrameIfCalledWithoutFrameNumber() throws IOException {
Util.setSdkVersionInt(19);
decoder = new VideoDecoder<>(bitmapPool, initializer, factory);
decoder.decode(resource, 100, 100, options);

verify(retriever).getFrameAtTime();
verify(retriever, never()).getFrameAtTime(anyLong());
verify(retriever).getFrameAtTime(VideoDecoder.DEFAULT_FRAME, VideoDecoder.DEFAULT_FRAME_OPTION);
}

@Test
public void getScaledFrameAtTime() throws IOException {
Bitmap expected = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
when(retriever.getScaledFrameAtTime(-1, MediaMetadataRetriever.OPTION_CLOSEST_SYNC, 100, 100))
.thenReturn(expected);

assertThat(decoder.decode(resource, 100, 100, options).get()).isSameAs(expected);
}
}

0 comments on commit ded8f77

Please sign in to comment.