Add ImageDecoder for external image loading libraries
The integration with external libraries like Glide or Coil is currently extremely complicated. Providing the boilerplate code to handle the ImageDecoder interface and a better hook for custom image decoders in DefaultRenderersFactory allows apps to easily inject their own logic for external image loading libraries. PiperOrigin-RevId: 659508914
This commit is contained in:
parent
e7eef0ce34
commit
b00e018697
@ -63,6 +63,8 @@
|
||||
values.
|
||||
* Metadata:
|
||||
* Image:
|
||||
* Add `ExternallyLoadedImageDecoder` for simplified integration with
|
||||
external image loading libraries like Glide or Coil.
|
||||
* DataSource:
|
||||
* Add `FileDescriptorDataSource`, a new `DataSource` that can be used to
|
||||
read from a `FileDescriptor`
|
||||
|
@ -623,7 +623,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
||||
* @param out An array to which the built renderers should be appended.
|
||||
*/
|
||||
protected void buildImageRenderers(ArrayList<Renderer> out) {
|
||||
out.add(new ImageRenderer(ImageDecoder.Factory.DEFAULT, /* imageOutput= */ null));
|
||||
out.add(new ImageRenderer(getImageDecoderFactory(), /* imageOutput= */ null));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -669,4 +669,9 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
||||
protected MediaCodecAdapter.Factory getCodecAdapterFactory() {
|
||||
return codecAdapterFactory;
|
||||
}
|
||||
|
||||
/** Returns the {@link ImageDecoder.Factory} used to build the image renderer. */
|
||||
protected ImageDecoder.Factory getImageDecoderFactory() {
|
||||
return ImageDecoder.Factory.DEFAULT;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.image;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static androidx.media3.common.util.Assertions.checkState;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.decoder.DecoderInputBuffer;
|
||||
import androidx.media3.exoplayer.RendererCapabilities;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* An {@link ImageDecoder} for externally loaded images.
|
||||
*
|
||||
* @see MimeTypes#APPLICATION_EXTERNALLY_LOADED_IMAGE
|
||||
*/
|
||||
@UnstableApi
|
||||
public final class ExternallyLoadedImageDecoder implements ImageDecoder {
|
||||
|
||||
/** A data class providing information about the external image request. */
|
||||
public static final class ExternalImageRequest {
|
||||
|
||||
/** The {@link Uri} for the external image. */
|
||||
public final Uri uri;
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @param uri The {@link Uri} for the external image.
|
||||
*/
|
||||
public ExternalImageRequest(Uri uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
}
|
||||
|
||||
/** The resolver that resolves an external image request to a Bitmap. */
|
||||
public interface BitmapResolver {
|
||||
|
||||
/**
|
||||
* Returns a {@link ListenableFuture} for the Bitmap referenced by the given {@link
|
||||
* ExternalImageRequest}.
|
||||
*
|
||||
* @param request The {@link ExternalImageRequest}.
|
||||
* @return A {@link ListenableFuture} returning the {@link Bitmap} for the request.
|
||||
*/
|
||||
ListenableFuture<Bitmap> resolve(ExternalImageRequest request);
|
||||
}
|
||||
|
||||
/** A {@link ImageDecoder.Factory} for {@link ExternallyLoadedImageDecoder} instances. */
|
||||
public static final class Factory implements ImageDecoder.Factory {
|
||||
|
||||
private final BitmapResolver bitmapResolver;
|
||||
|
||||
/**
|
||||
* Creates the factory.
|
||||
*
|
||||
* @param bitmapResolver The {@link BitmapResolver} to resolve the {@link ExternalImageRequest}
|
||||
* to a {@link Bitmap}.
|
||||
*/
|
||||
public Factory(BitmapResolver bitmapResolver) {
|
||||
this.bitmapResolver = bitmapResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @RendererCapabilities.Capabilities int supportsFormat(Format format) {
|
||||
boolean isExternallyLoadedImage =
|
||||
Objects.equals(format.sampleMimeType, MimeTypes.APPLICATION_EXTERNALLY_LOADED_IMAGE);
|
||||
return RendererCapabilities.create(
|
||||
isExternallyLoadedImage
|
||||
? C.FORMAT_HANDLED
|
||||
: MimeTypes.isImage(format.sampleMimeType)
|
||||
? C.FORMAT_UNSUPPORTED_SUBTYPE
|
||||
: C.FORMAT_UNSUPPORTED_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExternallyLoadedImageDecoder createImageDecoder() {
|
||||
return new ExternallyLoadedImageDecoder(bitmapResolver);
|
||||
}
|
||||
}
|
||||
|
||||
private final BitmapResolver bitmapResolver;
|
||||
private final DecoderInputBuffer inputBuffer;
|
||||
private final ImageOutputBuffer outputBuffer;
|
||||
|
||||
@Nullable private ListenableFuture<Bitmap> pendingDecode;
|
||||
private long pendingDecodeTimeUs;
|
||||
private boolean pendingEndOfStream;
|
||||
|
||||
private ExternallyLoadedImageDecoder(BitmapResolver bitmapResolver) {
|
||||
this.bitmapResolver = bitmapResolver;
|
||||
this.inputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
|
||||
this.outputBuffer =
|
||||
new ImageOutputBuffer() {
|
||||
@Override
|
||||
public void release() {
|
||||
clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "externallyLoadedImageDecoder";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOutputStartTimeUs(long outputStartTimeUs) {
|
||||
// Intentionally unused to render images that start earlier than the intended start time.
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public DecoderInputBuffer dequeueInputBuffer() {
|
||||
return pendingDecode == null ? inputBuffer : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueInputBuffer(DecoderInputBuffer inputBuffer) {
|
||||
if (inputBuffer.isEndOfStream()) {
|
||||
pendingEndOfStream = true;
|
||||
inputBuffer.clear();
|
||||
return;
|
||||
}
|
||||
ByteBuffer inputData = checkNotNull(inputBuffer.data);
|
||||
checkState(inputData.hasArray());
|
||||
Uri imageUri =
|
||||
Uri.parse(
|
||||
new String(
|
||||
inputData.array(), inputData.arrayOffset(), inputData.remaining(), Charsets.UTF_8));
|
||||
pendingDecode = bitmapResolver.resolve(new ExternalImageRequest(imageUri));
|
||||
pendingDecodeTimeUs = inputBuffer.timeUs;
|
||||
inputBuffer.clear();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ImageOutputBuffer dequeueOutputBuffer() throws ImageDecoderException {
|
||||
if (pendingEndOfStream) {
|
||||
outputBuffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
|
||||
pendingEndOfStream = false;
|
||||
return outputBuffer;
|
||||
}
|
||||
if (pendingDecode == null || !pendingDecode.isDone()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
outputBuffer.bitmap = Futures.getDone(pendingDecode);
|
||||
outputBuffer.timeUs = pendingDecodeTimeUs;
|
||||
return outputBuffer;
|
||||
} catch (ExecutionException e) {
|
||||
throw new ImageDecoderException(e.getCause());
|
||||
} catch (CancellationException e) {
|
||||
throw new ImageDecoderException(e);
|
||||
} finally {
|
||||
pendingDecode = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
resetState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
resetState();
|
||||
}
|
||||
|
||||
private void resetState() {
|
||||
if (pendingDecode != null) {
|
||||
pendingDecode.cancel(/* mayInterruptIfRunning= */ false);
|
||||
pendingDecode = null;
|
||||
}
|
||||
pendingEndOfStream = false;
|
||||
outputBuffer.release();
|
||||
}
|
||||
}
|
@ -16,13 +16,22 @@
|
||||
package androidx.media3.exoplayer.image;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.decoder.DecoderOutputBuffer;
|
||||
|
||||
/** Output buffer for {@link ImageDecoder}s. */
|
||||
/** Output buffer for {@link ImageDecoder} instances. */
|
||||
@UnstableApi
|
||||
public abstract class ImageOutputBuffer extends DecoderOutputBuffer {
|
||||
|
||||
/** The decoded {@link Bitmap}. */
|
||||
@Nullable public Bitmap bitmap;
|
||||
|
||||
@Override
|
||||
@CallSuper
|
||||
public void clear() {
|
||||
bitmap = null;
|
||||
super.clear();
|
||||
}
|
||||
}
|
||||
|
@ -458,7 +458,7 @@ public class ImageRenderer extends BaseRenderer {
|
||||
// Input buffers with no data that are also non-EOS, only carry the timestamp for a grid
|
||||
// tile. These buffers are not queued.
|
||||
boolean shouldQueueBuffer =
|
||||
checkStateNotNull(inputBuffer.data).remaining() > 0
|
||||
(inputBuffer.data != null && inputBuffer.data.remaining() > 0)
|
||||
|| checkStateNotNull(inputBuffer).isEndOfStream();
|
||||
if (shouldQueueBuffer) {
|
||||
checkStateNotNull(decoder).queueInputBuffer(checkStateNotNull(inputBuffer));
|
||||
|
@ -24,9 +24,9 @@ import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import androidx.media3.common.PlaybackException;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.util.Clock;
|
||||
import androidx.media3.common.util.ConditionVariable;
|
||||
@ -35,9 +35,7 @@ import androidx.media3.datasource.DataSourceUtil;
|
||||
import androidx.media3.datasource.DataSpec;
|
||||
import androidx.media3.exoplayer.ExoPlaybackException;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.exoplayer.RendererCapabilities;
|
||||
import androidx.media3.exoplayer.image.BitmapFactoryImageDecoder;
|
||||
import androidx.media3.exoplayer.image.ImageDecoder;
|
||||
import androidx.media3.exoplayer.image.ExternallyLoadedImageDecoder;
|
||||
import androidx.media3.exoplayer.image.ImageDecoderException;
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
|
||||
import androidx.media3.exoplayer.source.MediaSource;
|
||||
@ -48,12 +46,11 @@ import androidx.media3.test.utils.robolectric.PlaybackOutput;
|
||||
import androidx.media3.test.utils.robolectric.TestPlayerRunHelper;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.util.concurrent.ListeningExecutorService;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.annotation.GraphicsMode;
|
||||
@ -69,18 +66,20 @@ public final class ExternallyLoadedImagePlaybackTest {
|
||||
public void imagePlayback_validExternalLoader_callsLoadOnceAndPlaysSuccessfully()
|
||||
throws Exception {
|
||||
Context applicationContext = ApplicationProvider.getApplicationContext();
|
||||
CapturingRenderersFactory renderersFactory =
|
||||
new CapturingRenderersFactory(applicationContext)
|
||||
.setImageDecoderFactory(new CustomImageDecoderFactory());
|
||||
Clock clock = new FakeClock(/* isAutoAdvancing= */ true);
|
||||
AtomicInteger externalLoaderCallCount = new AtomicInteger();
|
||||
ListeningExecutorService listeningExecutorService =
|
||||
MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
|
||||
CapturingRenderersFactory renderersFactory =
|
||||
new CapturingRenderersFactory(applicationContext)
|
||||
.setImageDecoderFactory(
|
||||
new ExternallyLoadedImageDecoder.Factory(
|
||||
request -> listeningExecutorService.submit(() -> decode(request.uri))));
|
||||
Clock clock = new FakeClock(/* isAutoAdvancing= */ true);
|
||||
ArrayList<Uri> externalLoaderUris = new ArrayList<>();
|
||||
MediaSource.Factory mediaSourceFactory =
|
||||
new DefaultMediaSourceFactory(applicationContext)
|
||||
.setExternalImageLoader(
|
||||
unused ->
|
||||
listeningExecutorService.submit(externalLoaderCallCount::getAndIncrement));
|
||||
request ->
|
||||
listeningExecutorService.submit(() -> externalLoaderUris.add(request.uri)));
|
||||
ExoPlayer player =
|
||||
new ExoPlayer.Builder(applicationContext, renderersFactory)
|
||||
.setClock(clock)
|
||||
@ -88,9 +87,10 @@ public final class ExternallyLoadedImagePlaybackTest {
|
||||
.build();
|
||||
PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory);
|
||||
long durationMs = 5 * C.MILLIS_PER_SECOND;
|
||||
Uri uri = Uri.parse("asset:///media/" + INPUT_FILE);
|
||||
player.setMediaItem(
|
||||
new MediaItem.Builder()
|
||||
.setUri("asset:///media/" + INPUT_FILE)
|
||||
.setUri(uri)
|
||||
.setImageDurationMs(durationMs)
|
||||
.setMimeType(MimeTypes.APPLICATION_EXTERNALLY_LOADED_IMAGE)
|
||||
.build());
|
||||
@ -103,7 +103,7 @@ public final class ExternallyLoadedImagePlaybackTest {
|
||||
long playbackDurationMs = clock.elapsedRealtime() - playerStartedMs;
|
||||
player.release();
|
||||
|
||||
assertThat(externalLoaderCallCount.get()).isEqualTo(1);
|
||||
assertThat(externalLoaderUris).containsExactly(uri);
|
||||
assertThat(playbackDurationMs).isAtLeast(durationMs);
|
||||
DumpFileAsserts.assertOutput(
|
||||
applicationContext, playbackOutput, "playbackdumps/" + INPUT_FILE + ".dump");
|
||||
@ -112,18 +112,21 @@ public final class ExternallyLoadedImagePlaybackTest {
|
||||
@Test
|
||||
public void imagePlayback_externalLoaderFutureFails_propagatesFailure() throws Exception {
|
||||
Context applicationContext = ApplicationProvider.getApplicationContext();
|
||||
CapturingRenderersFactory renderersFactory =
|
||||
new CapturingRenderersFactory(applicationContext)
|
||||
.setImageDecoderFactory(new CustomImageDecoderFactory());
|
||||
ListeningExecutorService listeningExecutorService =
|
||||
MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
|
||||
CapturingRenderersFactory renderersFactory =
|
||||
new CapturingRenderersFactory(applicationContext)
|
||||
.setImageDecoderFactory(
|
||||
new ExternallyLoadedImageDecoder.Factory(
|
||||
request -> listeningExecutorService.submit(() -> decode(request.uri))));
|
||||
RuntimeException exception = new RuntimeException("My Exception");
|
||||
MediaSource.Factory mediaSourceFactory =
|
||||
new DefaultMediaSourceFactory(applicationContext)
|
||||
.setExternalImageLoader(
|
||||
unused ->
|
||||
listeningExecutorService.submit(
|
||||
() -> {
|
||||
throw new RuntimeException("My Exception");
|
||||
throw exception;
|
||||
}));
|
||||
ExoPlayer player =
|
||||
new ExoPlayer.Builder(applicationContext, renderersFactory)
|
||||
@ -139,17 +142,21 @@ public final class ExternallyLoadedImagePlaybackTest {
|
||||
player.prepare();
|
||||
|
||||
ExoPlaybackException error = TestPlayerRunHelper.runUntilError(player);
|
||||
assertThat(error).isNotNull();
|
||||
|
||||
assertThat(error.errorCode).isEqualTo(PlaybackException.ERROR_CODE_IO_UNSPECIFIED);
|
||||
assertThat(error.getSourceException()).hasCauseThat().isEqualTo(exception);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void imagePlayback_loadingCompletedWhenFutureCompletes() throws Exception {
|
||||
Context applicationContext = ApplicationProvider.getApplicationContext();
|
||||
CapturingRenderersFactory renderersFactory =
|
||||
new CapturingRenderersFactory(applicationContext)
|
||||
.setImageDecoderFactory(new CustomImageDecoderFactory());
|
||||
ListeningExecutorService listeningExecutorService =
|
||||
MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
|
||||
CapturingRenderersFactory renderersFactory =
|
||||
new CapturingRenderersFactory(applicationContext)
|
||||
.setImageDecoderFactory(
|
||||
new ExternallyLoadedImageDecoder.Factory(
|
||||
request -> listeningExecutorService.submit(() -> decode(request.uri))));
|
||||
ConditionVariable loadingComplete = new ConditionVariable();
|
||||
MediaSource.Factory mediaSourceFactory =
|
||||
new DefaultMediaSourceFactory(applicationContext)
|
||||
@ -177,27 +184,10 @@ public final class ExternallyLoadedImagePlaybackTest {
|
||||
TestPlayerRunHelper.runUntilIsLoading(player, /* expectedIsLoading= */ false);
|
||||
}
|
||||
|
||||
private static final class CustomImageDecoderFactory implements ImageDecoder.Factory {
|
||||
|
||||
@Override
|
||||
public @RendererCapabilities.Capabilities int supportsFormat(Format format) {
|
||||
return format.sampleMimeType.equals(MimeTypes.APPLICATION_EXTERNALLY_LOADED_IMAGE)
|
||||
? RendererCapabilities.create(C.FORMAT_HANDLED)
|
||||
: RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageDecoder createImageDecoder() {
|
||||
return new BitmapFactoryImageDecoder.Factory(ExternallyLoadedImagePlaybackTest::decode)
|
||||
.createImageDecoder();
|
||||
}
|
||||
}
|
||||
|
||||
private static Bitmap decode(byte[] data, int length) throws ImageDecoderException {
|
||||
String uriString = new String(data, Charsets.UTF_8);
|
||||
private static Bitmap decode(Uri uri) throws ImageDecoderException {
|
||||
AssetDataSource assetDataSource =
|
||||
new AssetDataSource(ApplicationProvider.getApplicationContext());
|
||||
DataSpec dataSpec = new DataSpec(Uri.parse(uriString));
|
||||
DataSpec dataSpec = new DataSpec(uri);
|
||||
@Nullable Bitmap bitmap;
|
||||
|
||||
try {
|
||||
@ -209,8 +199,7 @@ public final class ExternallyLoadedImagePlaybackTest {
|
||||
}
|
||||
if (bitmap == null) {
|
||||
throw new ImageDecoderException(
|
||||
"Could not decode image data with BitmapFactory. uriString decoded from data = "
|
||||
+ uriString);
|
||||
"Could not decode image data with BitmapFactory. uriString decoded from data = " + uri);
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
@ -0,0 +1,343 @@
|
||||
/*
|
||||
* Copyright 2024 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.image;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.util.concurrent.Futures.immediateCancelledFuture;
|
||||
import static com.google.common.util.concurrent.Futures.immediateFuture;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import androidx.media3.decoder.DecoderInputBuffer;
|
||||
import androidx.media3.exoplayer.RendererCapabilities;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit test for {@link ExternallyLoadedImageDecoder}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExternallyLoadedImageDecoderTest {
|
||||
|
||||
@Test
|
||||
public void factorySupportsFormat_externallyLoadedImage_returnsFormatSupported() {
|
||||
ExternallyLoadedImageDecoder.Factory factory =
|
||||
new ExternallyLoadedImageDecoder.Factory(request -> immediateCancelledFuture());
|
||||
Format format =
|
||||
new Format.Builder()
|
||||
.setSampleMimeType(MimeTypes.APPLICATION_EXTERNALLY_LOADED_IMAGE)
|
||||
.build();
|
||||
|
||||
assertThat(factory.supportsFormat(format))
|
||||
.isEqualTo(RendererCapabilities.create(C.FORMAT_HANDLED));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void factorySupportsFormat_noSampleMimeType_returnsUnsupportedType() {
|
||||
ExternallyLoadedImageDecoder.Factory factory =
|
||||
new ExternallyLoadedImageDecoder.Factory(request -> immediateCancelledFuture());
|
||||
Format format = new Format.Builder().build();
|
||||
|
||||
assertThat(factory.supportsFormat(format))
|
||||
.isEqualTo(RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void factorySupportsFormat_nonImageMimeType_returnsUnsupportedType() {
|
||||
ExternallyLoadedImageDecoder.Factory factory =
|
||||
new ExternallyLoadedImageDecoder.Factory(request -> immediateCancelledFuture());
|
||||
Format format = new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_AV1).build();
|
||||
|
||||
assertThat(factory.supportsFormat(format))
|
||||
.isEqualTo(RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void factorySupportsFormat_unsupportedImageMimeType_returnsUnsupportedSubType() {
|
||||
ExternallyLoadedImageDecoder.Factory factory =
|
||||
new ExternallyLoadedImageDecoder.Factory(request -> immediateCancelledFuture());
|
||||
Format format = new Format.Builder().setSampleMimeType("image/custom").build();
|
||||
|
||||
assertThat(factory.supportsFormat(format))
|
||||
.isEqualTo(RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decoding_withMultipleBuffersAndEndOfStream_producesExpectedOutput() throws Exception {
|
||||
Uri uri1 = Uri.parse("https://image1_longer_name_than_image2.test");
|
||||
Uri uri2 = Uri.parse("https://image2.test");
|
||||
byte[] uri1Bytes = uri1.toString().getBytes(Charsets.UTF_8);
|
||||
byte[] uri2Bytes = uri2.toString().getBytes(Charsets.UTF_8);
|
||||
Bitmap bitmap1 = Bitmap.createBitmap(/* width= */ 5, /* height= */ 5, Bitmap.Config.ARGB_8888);
|
||||
Bitmap bitmap2 = Bitmap.createBitmap(/* width= */ 7, /* height= */ 7, Bitmap.Config.ARGB_8888);
|
||||
ExternallyLoadedImageDecoder decoder =
|
||||
new ExternallyLoadedImageDecoder.Factory(
|
||||
request -> immediateFuture(request.uri.equals(uri1) ? bitmap1 : bitmap2))
|
||||
.createImageDecoder();
|
||||
|
||||
DecoderInputBuffer inputBuffer1 = decoder.dequeueInputBuffer();
|
||||
inputBuffer1.timeUs = 555;
|
||||
inputBuffer1.ensureSpaceForWrite(uri1Bytes.length);
|
||||
inputBuffer1.data.put(uri1Bytes);
|
||||
inputBuffer1.data.flip();
|
||||
decoder.queueInputBuffer(inputBuffer1);
|
||||
ImageOutputBuffer outputBuffer1 = decoder.dequeueOutputBuffer();
|
||||
|
||||
assertThat(outputBuffer1.timeUs).isEqualTo(555);
|
||||
assertThat(outputBuffer1.isEndOfStream()).isFalse();
|
||||
assertThat(outputBuffer1.bitmap).isEqualTo(bitmap1);
|
||||
|
||||
outputBuffer1.release();
|
||||
DecoderInputBuffer inputBuffer2 = decoder.dequeueInputBuffer();
|
||||
inputBuffer2.timeUs = 777;
|
||||
inputBuffer2.ensureSpaceForWrite(uri2Bytes.length);
|
||||
inputBuffer2.data.put(uri2Bytes);
|
||||
inputBuffer2.data.flip();
|
||||
decoder.queueInputBuffer(inputBuffer2);
|
||||
ImageOutputBuffer outputBuffer2 = decoder.dequeueOutputBuffer();
|
||||
|
||||
assertThat(outputBuffer2.timeUs).isEqualTo(777);
|
||||
assertThat(outputBuffer2.isEndOfStream()).isFalse();
|
||||
assertThat(outputBuffer2.bitmap).isEqualTo(bitmap2);
|
||||
|
||||
outputBuffer2.release();
|
||||
DecoderInputBuffer inputBufferEos = decoder.dequeueInputBuffer();
|
||||
inputBufferEos.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
|
||||
decoder.queueInputBuffer(inputBufferEos);
|
||||
ImageOutputBuffer outputBufferEos = decoder.dequeueOutputBuffer();
|
||||
|
||||
assertThat(outputBufferEos.isEndOfStream()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBuffer_withDelayedBitmap_onlyReturnsOutputWhenReady() throws Exception {
|
||||
Uri uri = Uri.parse("https://image.test");
|
||||
byte[] uriBytes = uri.toString().getBytes(Charsets.UTF_8);
|
||||
Bitmap bitmap = Bitmap.createBitmap(/* width= */ 5, /* height= */ 5, Bitmap.Config.ARGB_8888);
|
||||
SettableFuture<Bitmap> settableFuture = SettableFuture.create();
|
||||
ExternallyLoadedImageDecoder decoder =
|
||||
new ExternallyLoadedImageDecoder.Factory(request -> settableFuture).createImageDecoder();
|
||||
DecoderInputBuffer inputBuffer = decoder.dequeueInputBuffer();
|
||||
inputBuffer.timeUs = 555;
|
||||
inputBuffer.ensureSpaceForWrite(uriBytes.length);
|
||||
inputBuffer.data.put(uriBytes);
|
||||
inputBuffer.data.flip();
|
||||
decoder.queueInputBuffer(inputBuffer);
|
||||
|
||||
assertThat(decoder.dequeueOutputBuffer()).isNull();
|
||||
settableFuture.set(bitmap);
|
||||
assertThat(decoder.dequeueOutputBuffer()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dequeueOutputBuffer_withFailedFuture_throwsExceptionWithOriginalCause()
|
||||
throws Exception {
|
||||
Uri uri = Uri.parse("https://image.test");
|
||||
byte[] uriBytes = uri.toString().getBytes(Charsets.UTF_8);
|
||||
Throwable testThrowable = new Throwable();
|
||||
SettableFuture<Bitmap> settableFuture = SettableFuture.create();
|
||||
ExternallyLoadedImageDecoder decoder =
|
||||
new ExternallyLoadedImageDecoder.Factory(request -> settableFuture).createImageDecoder();
|
||||
DecoderInputBuffer inputBuffer = decoder.dequeueInputBuffer();
|
||||
inputBuffer.timeUs = 555;
|
||||
inputBuffer.ensureSpaceForWrite(uriBytes.length);
|
||||
inputBuffer.data.put(uriBytes);
|
||||
inputBuffer.data.flip();
|
||||
decoder.queueInputBuffer(inputBuffer);
|
||||
|
||||
assertThat(decoder.dequeueOutputBuffer()).isNull();
|
||||
settableFuture.setException(testThrowable);
|
||||
ImageDecoderException exception =
|
||||
assertThrows(ImageDecoderException.class, decoder::dequeueOutputBuffer);
|
||||
assertThat(exception).hasCauseThat().isEqualTo(testThrowable);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
dequeueOutputBuffer_withCancelledFuture_throwsExceptionWithCancellationExceptionCause()
|
||||
throws Exception {
|
||||
Uri uri = Uri.parse("https://image.test");
|
||||
byte[] uriBytes = uri.toString().getBytes(Charsets.UTF_8);
|
||||
SettableFuture<Bitmap> settableFuture = SettableFuture.create();
|
||||
ExternallyLoadedImageDecoder decoder =
|
||||
new ExternallyLoadedImageDecoder.Factory(request -> settableFuture).createImageDecoder();
|
||||
DecoderInputBuffer inputBuffer = decoder.dequeueInputBuffer();
|
||||
inputBuffer.timeUs = 555;
|
||||
inputBuffer.ensureSpaceForWrite(uriBytes.length);
|
||||
inputBuffer.data.put(uriBytes);
|
||||
inputBuffer.data.flip();
|
||||
decoder.queueInputBuffer(inputBuffer);
|
||||
|
||||
assertThat(decoder.dequeueOutputBuffer()).isNull();
|
||||
settableFuture.cancel(/* mayInterruptIfRunning= */ false);
|
||||
ImageDecoderException exception =
|
||||
assertThrows(ImageDecoderException.class, decoder::dequeueOutputBuffer);
|
||||
assertThat(exception).hasCauseThat().isInstanceOf(CancellationException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flush_beforeFirstBuffer_allowsToQueueNextBuffer() throws Exception {
|
||||
Uri uri = Uri.parse("https://image.test");
|
||||
byte[] uriBytes = uri.toString().getBytes(Charsets.UTF_8);
|
||||
Bitmap bitmap = Bitmap.createBitmap(/* width= */ 5, /* height= */ 5, Bitmap.Config.ARGB_8888);
|
||||
ExternallyLoadedImageDecoder decoder =
|
||||
new ExternallyLoadedImageDecoder.Factory(request -> immediateFuture(bitmap))
|
||||
.createImageDecoder();
|
||||
|
||||
decoder.flush();
|
||||
DecoderInputBuffer inputBuffer = decoder.dequeueInputBuffer();
|
||||
inputBuffer.timeUs = 555;
|
||||
inputBuffer.ensureSpaceForWrite(uriBytes.length);
|
||||
inputBuffer.data.put(uriBytes);
|
||||
inputBuffer.data.flip();
|
||||
decoder.queueInputBuffer(inputBuffer);
|
||||
ImageOutputBuffer outputBuffer = decoder.dequeueOutputBuffer();
|
||||
|
||||
assertThat(outputBuffer.timeUs).isEqualTo(555);
|
||||
assertThat(outputBuffer.isEndOfStream()).isFalse();
|
||||
assertThat(outputBuffer.bitmap).isEqualTo(bitmap);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flush_duringDecoding_cancelsPendingDecodeAndAllowsToQueueNextBuffer()
|
||||
throws Exception {
|
||||
Uri uri1 = Uri.parse("https://image1.test");
|
||||
Uri uri2 = Uri.parse("https://image2.test");
|
||||
byte[] uri1Bytes = uri1.toString().getBytes(Charsets.UTF_8);
|
||||
byte[] uri2Bytes = uri2.toString().getBytes(Charsets.UTF_8);
|
||||
Bitmap bitmap2 = Bitmap.createBitmap(/* width= */ 7, /* height= */ 7, Bitmap.Config.ARGB_8888);
|
||||
SettableFuture<Bitmap> settableFuture = SettableFuture.create();
|
||||
ExternallyLoadedImageDecoder decoder =
|
||||
new ExternallyLoadedImageDecoder.Factory(
|
||||
request -> request.uri.equals(uri1) ? settableFuture : immediateFuture(bitmap2))
|
||||
.createImageDecoder();
|
||||
DecoderInputBuffer inputBuffer = decoder.dequeueInputBuffer();
|
||||
inputBuffer.timeUs = 111;
|
||||
inputBuffer.ensureSpaceForWrite(uri1Bytes.length);
|
||||
inputBuffer.data.put(uri1Bytes);
|
||||
inputBuffer.data.flip();
|
||||
decoder.queueInputBuffer(inputBuffer);
|
||||
|
||||
decoder.flush();
|
||||
DecoderInputBuffer newInputBuffer = decoder.dequeueInputBuffer();
|
||||
newInputBuffer.timeUs = 555;
|
||||
newInputBuffer.ensureSpaceForWrite(uri2Bytes.length);
|
||||
newInputBuffer.data.put(uri2Bytes);
|
||||
newInputBuffer.data.flip();
|
||||
decoder.queueInputBuffer(newInputBuffer);
|
||||
ImageOutputBuffer newOutputBuffer = decoder.dequeueOutputBuffer();
|
||||
|
||||
assertThat(settableFuture.isCancelled()).isTrue();
|
||||
assertThat(newOutputBuffer.timeUs).isEqualTo(555);
|
||||
assertThat(newOutputBuffer.isEndOfStream()).isFalse();
|
||||
assertThat(newOutputBuffer.bitmap).isEqualTo(bitmap2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flush_afterDecoding_allowsToQueueNextBuffer() throws Exception {
|
||||
Uri uri1 = Uri.parse("https://image1.test");
|
||||
Uri uri2 = Uri.parse("https://image2.test");
|
||||
byte[] uri1Bytes = uri1.toString().getBytes(Charsets.UTF_8);
|
||||
byte[] uri2Bytes = uri2.toString().getBytes(Charsets.UTF_8);
|
||||
Bitmap bitmap1 = Bitmap.createBitmap(/* width= */ 5, /* height= */ 5, Bitmap.Config.ARGB_8888);
|
||||
Bitmap bitmap2 = Bitmap.createBitmap(/* width= */ 7, /* height= */ 7, Bitmap.Config.ARGB_8888);
|
||||
ExternallyLoadedImageDecoder decoder =
|
||||
new ExternallyLoadedImageDecoder.Factory(
|
||||
request -> immediateFuture(request.uri.equals(uri1) ? bitmap1 : bitmap2))
|
||||
.createImageDecoder();
|
||||
DecoderInputBuffer inputBuffer = decoder.dequeueInputBuffer();
|
||||
inputBuffer.timeUs = 111;
|
||||
inputBuffer.ensureSpaceForWrite(uri1Bytes.length);
|
||||
inputBuffer.data.put(uri1Bytes);
|
||||
inputBuffer.data.flip();
|
||||
decoder.queueInputBuffer(inputBuffer);
|
||||
ImageOutputBuffer outputBuffer = decoder.dequeueOutputBuffer();
|
||||
outputBuffer.release();
|
||||
|
||||
decoder.flush();
|
||||
DecoderInputBuffer newInputBuffer = decoder.dequeueInputBuffer();
|
||||
newInputBuffer.timeUs = 555;
|
||||
newInputBuffer.ensureSpaceForWrite(uri2Bytes.length);
|
||||
newInputBuffer.data.put(uri2Bytes);
|
||||
newInputBuffer.data.flip();
|
||||
decoder.queueInputBuffer(newInputBuffer);
|
||||
ImageOutputBuffer newOutputBuffer = decoder.dequeueOutputBuffer();
|
||||
|
||||
assertThat(newOutputBuffer.timeUs).isEqualTo(555);
|
||||
assertThat(newOutputBuffer.isEndOfStream()).isFalse();
|
||||
assertThat(newOutputBuffer.bitmap).isEqualTo(bitmap2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flush_duringEndOfStreamSample_allowsToQueueNextBuffer() throws Exception {
|
||||
Uri uri = Uri.parse("https://image1.test");
|
||||
byte[] uriBytes = uri.toString().getBytes(Charsets.UTF_8);
|
||||
Bitmap bitmap = Bitmap.createBitmap(/* width= */ 5, /* height= */ 5, Bitmap.Config.ARGB_8888);
|
||||
ExternallyLoadedImageDecoder decoder =
|
||||
new ExternallyLoadedImageDecoder.Factory(request -> immediateFuture(bitmap))
|
||||
.createImageDecoder();
|
||||
DecoderInputBuffer inputBuffer = decoder.dequeueInputBuffer();
|
||||
inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
|
||||
decoder.queueInputBuffer(inputBuffer);
|
||||
|
||||
decoder.flush();
|
||||
DecoderInputBuffer newInputBuffer = decoder.dequeueInputBuffer();
|
||||
newInputBuffer.timeUs = 555;
|
||||
newInputBuffer.ensureSpaceForWrite(uriBytes.length);
|
||||
newInputBuffer.data.put(uriBytes);
|
||||
newInputBuffer.data.flip();
|
||||
decoder.queueInputBuffer(newInputBuffer);
|
||||
ImageOutputBuffer newOutputBuffer = decoder.dequeueOutputBuffer();
|
||||
|
||||
assertThat(newOutputBuffer.timeUs).isEqualTo(555);
|
||||
assertThat(newOutputBuffer.isEndOfStream()).isFalse();
|
||||
assertThat(newOutputBuffer.bitmap).isEqualTo(bitmap);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flush_afterEndOfStreamSample_allowsToQueueNextBuffer() throws Exception {
|
||||
Uri uri = Uri.parse("https://image1.test");
|
||||
byte[] uriBytes = uri.toString().getBytes(Charsets.UTF_8);
|
||||
Bitmap bitmap = Bitmap.createBitmap(/* width= */ 5, /* height= */ 5, Bitmap.Config.ARGB_8888);
|
||||
ExternallyLoadedImageDecoder decoder =
|
||||
new ExternallyLoadedImageDecoder.Factory(request -> immediateFuture(bitmap))
|
||||
.createImageDecoder();
|
||||
DecoderInputBuffer inputBuffer = decoder.dequeueInputBuffer();
|
||||
inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
|
||||
decoder.queueInputBuffer(inputBuffer);
|
||||
ImageOutputBuffer outputBuffer = decoder.dequeueOutputBuffer();
|
||||
outputBuffer.release();
|
||||
|
||||
decoder.flush();
|
||||
DecoderInputBuffer newInputBuffer = decoder.dequeueInputBuffer();
|
||||
newInputBuffer.timeUs = 555;
|
||||
newInputBuffer.ensureSpaceForWrite(uriBytes.length);
|
||||
newInputBuffer.data.put(uriBytes);
|
||||
newInputBuffer.data.flip();
|
||||
decoder.queueInputBuffer(newInputBuffer);
|
||||
ImageOutputBuffer newOutputBuffer = decoder.dequeueOutputBuffer();
|
||||
|
||||
assertThat(newOutputBuffer.timeUs).isEqualTo(555);
|
||||
assertThat(newOutputBuffer.isEndOfStream()).isFalse();
|
||||
assertThat(newOutputBuffer.bitmap).isEqualTo(bitmap);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user