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:
tonihei 2024-08-05 04:33:01 -07:00 committed by Copybara-Service
parent e7eef0ce34
commit b00e018697
7 changed files with 599 additions and 47 deletions

View File

@ -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`

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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));

View File

@ -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;
}

View File

@ -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);
}
}