mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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.
|
values.
|
||||||
* Metadata:
|
* Metadata:
|
||||||
* Image:
|
* Image:
|
||||||
|
* Add `ExternallyLoadedImageDecoder` for simplified integration with
|
||||||
|
external image loading libraries like Glide or Coil.
|
||||||
* DataSource:
|
* DataSource:
|
||||||
* Add `FileDescriptorDataSource`, a new `DataSource` that can be used to
|
* Add `FileDescriptorDataSource`, a new `DataSource` that can be used to
|
||||||
read from a `FileDescriptor`
|
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.
|
* @param out An array to which the built renderers should be appended.
|
||||||
*/
|
*/
|
||||||
protected void buildImageRenderers(ArrayList<Renderer> out) {
|
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() {
|
protected MediaCodecAdapter.Factory getCodecAdapterFactory() {
|
||||||
return codecAdapterFactory;
|
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;
|
package androidx.media3.exoplayer.image;
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
import androidx.annotation.CallSuper;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.decoder.DecoderOutputBuffer;
|
import androidx.media3.decoder.DecoderOutputBuffer;
|
||||||
|
|
||||||
/** Output buffer for {@link ImageDecoder}s. */
|
/** Output buffer for {@link ImageDecoder} instances. */
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public abstract class ImageOutputBuffer extends DecoderOutputBuffer {
|
public abstract class ImageOutputBuffer extends DecoderOutputBuffer {
|
||||||
|
|
||||||
|
/** The decoded {@link Bitmap}. */
|
||||||
@Nullable public Bitmap 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
|
// Input buffers with no data that are also non-EOS, only carry the timestamp for a grid
|
||||||
// tile. These buffers are not queued.
|
// tile. These buffers are not queued.
|
||||||
boolean shouldQueueBuffer =
|
boolean shouldQueueBuffer =
|
||||||
checkStateNotNull(inputBuffer.data).remaining() > 0
|
(inputBuffer.data != null && inputBuffer.data.remaining() > 0)
|
||||||
|| checkStateNotNull(inputBuffer).isEndOfStream();
|
|| checkStateNotNull(inputBuffer).isEndOfStream();
|
||||||
if (shouldQueueBuffer) {
|
if (shouldQueueBuffer) {
|
||||||
checkStateNotNull(decoder).queueInputBuffer(checkStateNotNull(inputBuffer));
|
checkStateNotNull(decoder).queueInputBuffer(checkStateNotNull(inputBuffer));
|
||||||
|
@ -24,9 +24,9 @@ import android.graphics.BitmapFactory;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Format;
|
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.MimeTypes;
|
import androidx.media3.common.MimeTypes;
|
||||||
|
import androidx.media3.common.PlaybackException;
|
||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
import androidx.media3.common.util.Clock;
|
import androidx.media3.common.util.Clock;
|
||||||
import androidx.media3.common.util.ConditionVariable;
|
import androidx.media3.common.util.ConditionVariable;
|
||||||
@ -35,9 +35,7 @@ import androidx.media3.datasource.DataSourceUtil;
|
|||||||
import androidx.media3.datasource.DataSpec;
|
import androidx.media3.datasource.DataSpec;
|
||||||
import androidx.media3.exoplayer.ExoPlaybackException;
|
import androidx.media3.exoplayer.ExoPlaybackException;
|
||||||
import androidx.media3.exoplayer.ExoPlayer;
|
import androidx.media3.exoplayer.ExoPlayer;
|
||||||
import androidx.media3.exoplayer.RendererCapabilities;
|
import androidx.media3.exoplayer.image.ExternallyLoadedImageDecoder;
|
||||||
import androidx.media3.exoplayer.image.BitmapFactoryImageDecoder;
|
|
||||||
import androidx.media3.exoplayer.image.ImageDecoder;
|
|
||||||
import androidx.media3.exoplayer.image.ImageDecoderException;
|
import androidx.media3.exoplayer.image.ImageDecoderException;
|
||||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
|
||||||
import androidx.media3.exoplayer.source.MediaSource;
|
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.media3.test.utils.robolectric.TestPlayerRunHelper;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
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.ListeningExecutorService;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.robolectric.annotation.GraphicsMode;
|
import org.robolectric.annotation.GraphicsMode;
|
||||||
@ -69,18 +66,20 @@ public final class ExternallyLoadedImagePlaybackTest {
|
|||||||
public void imagePlayback_validExternalLoader_callsLoadOnceAndPlaysSuccessfully()
|
public void imagePlayback_validExternalLoader_callsLoadOnceAndPlaysSuccessfully()
|
||||||
throws Exception {
|
throws Exception {
|
||||||
Context applicationContext = ApplicationProvider.getApplicationContext();
|
Context applicationContext = ApplicationProvider.getApplicationContext();
|
||||||
CapturingRenderersFactory renderersFactory =
|
|
||||||
new CapturingRenderersFactory(applicationContext)
|
|
||||||
.setImageDecoderFactory(new CustomImageDecoderFactory());
|
|
||||||
Clock clock = new FakeClock(/* isAutoAdvancing= */ true);
|
|
||||||
AtomicInteger externalLoaderCallCount = new AtomicInteger();
|
|
||||||
ListeningExecutorService listeningExecutorService =
|
ListeningExecutorService listeningExecutorService =
|
||||||
MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
|
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 =
|
MediaSource.Factory mediaSourceFactory =
|
||||||
new DefaultMediaSourceFactory(applicationContext)
|
new DefaultMediaSourceFactory(applicationContext)
|
||||||
.setExternalImageLoader(
|
.setExternalImageLoader(
|
||||||
unused ->
|
request ->
|
||||||
listeningExecutorService.submit(externalLoaderCallCount::getAndIncrement));
|
listeningExecutorService.submit(() -> externalLoaderUris.add(request.uri)));
|
||||||
ExoPlayer player =
|
ExoPlayer player =
|
||||||
new ExoPlayer.Builder(applicationContext, renderersFactory)
|
new ExoPlayer.Builder(applicationContext, renderersFactory)
|
||||||
.setClock(clock)
|
.setClock(clock)
|
||||||
@ -88,9 +87,10 @@ public final class ExternallyLoadedImagePlaybackTest {
|
|||||||
.build();
|
.build();
|
||||||
PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory);
|
PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory);
|
||||||
long durationMs = 5 * C.MILLIS_PER_SECOND;
|
long durationMs = 5 * C.MILLIS_PER_SECOND;
|
||||||
|
Uri uri = Uri.parse("asset:///media/" + INPUT_FILE);
|
||||||
player.setMediaItem(
|
player.setMediaItem(
|
||||||
new MediaItem.Builder()
|
new MediaItem.Builder()
|
||||||
.setUri("asset:///media/" + INPUT_FILE)
|
.setUri(uri)
|
||||||
.setImageDurationMs(durationMs)
|
.setImageDurationMs(durationMs)
|
||||||
.setMimeType(MimeTypes.APPLICATION_EXTERNALLY_LOADED_IMAGE)
|
.setMimeType(MimeTypes.APPLICATION_EXTERNALLY_LOADED_IMAGE)
|
||||||
.build());
|
.build());
|
||||||
@ -103,7 +103,7 @@ public final class ExternallyLoadedImagePlaybackTest {
|
|||||||
long playbackDurationMs = clock.elapsedRealtime() - playerStartedMs;
|
long playbackDurationMs = clock.elapsedRealtime() - playerStartedMs;
|
||||||
player.release();
|
player.release();
|
||||||
|
|
||||||
assertThat(externalLoaderCallCount.get()).isEqualTo(1);
|
assertThat(externalLoaderUris).containsExactly(uri);
|
||||||
assertThat(playbackDurationMs).isAtLeast(durationMs);
|
assertThat(playbackDurationMs).isAtLeast(durationMs);
|
||||||
DumpFileAsserts.assertOutput(
|
DumpFileAsserts.assertOutput(
|
||||||
applicationContext, playbackOutput, "playbackdumps/" + INPUT_FILE + ".dump");
|
applicationContext, playbackOutput, "playbackdumps/" + INPUT_FILE + ".dump");
|
||||||
@ -112,18 +112,21 @@ public final class ExternallyLoadedImagePlaybackTest {
|
|||||||
@Test
|
@Test
|
||||||
public void imagePlayback_externalLoaderFutureFails_propagatesFailure() throws Exception {
|
public void imagePlayback_externalLoaderFutureFails_propagatesFailure() throws Exception {
|
||||||
Context applicationContext = ApplicationProvider.getApplicationContext();
|
Context applicationContext = ApplicationProvider.getApplicationContext();
|
||||||
CapturingRenderersFactory renderersFactory =
|
|
||||||
new CapturingRenderersFactory(applicationContext)
|
|
||||||
.setImageDecoderFactory(new CustomImageDecoderFactory());
|
|
||||||
ListeningExecutorService listeningExecutorService =
|
ListeningExecutorService listeningExecutorService =
|
||||||
MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
|
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 =
|
MediaSource.Factory mediaSourceFactory =
|
||||||
new DefaultMediaSourceFactory(applicationContext)
|
new DefaultMediaSourceFactory(applicationContext)
|
||||||
.setExternalImageLoader(
|
.setExternalImageLoader(
|
||||||
unused ->
|
unused ->
|
||||||
listeningExecutorService.submit(
|
listeningExecutorService.submit(
|
||||||
() -> {
|
() -> {
|
||||||
throw new RuntimeException("My Exception");
|
throw exception;
|
||||||
}));
|
}));
|
||||||
ExoPlayer player =
|
ExoPlayer player =
|
||||||
new ExoPlayer.Builder(applicationContext, renderersFactory)
|
new ExoPlayer.Builder(applicationContext, renderersFactory)
|
||||||
@ -139,17 +142,21 @@ public final class ExternallyLoadedImagePlaybackTest {
|
|||||||
player.prepare();
|
player.prepare();
|
||||||
|
|
||||||
ExoPlaybackException error = TestPlayerRunHelper.runUntilError(player);
|
ExoPlaybackException error = TestPlayerRunHelper.runUntilError(player);
|
||||||
assertThat(error).isNotNull();
|
|
||||||
|
assertThat(error.errorCode).isEqualTo(PlaybackException.ERROR_CODE_IO_UNSPECIFIED);
|
||||||
|
assertThat(error.getSourceException()).hasCauseThat().isEqualTo(exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void imagePlayback_loadingCompletedWhenFutureCompletes() throws Exception {
|
public void imagePlayback_loadingCompletedWhenFutureCompletes() throws Exception {
|
||||||
Context applicationContext = ApplicationProvider.getApplicationContext();
|
Context applicationContext = ApplicationProvider.getApplicationContext();
|
||||||
CapturingRenderersFactory renderersFactory =
|
|
||||||
new CapturingRenderersFactory(applicationContext)
|
|
||||||
.setImageDecoderFactory(new CustomImageDecoderFactory());
|
|
||||||
ListeningExecutorService listeningExecutorService =
|
ListeningExecutorService listeningExecutorService =
|
||||||
MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
|
MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
|
||||||
|
CapturingRenderersFactory renderersFactory =
|
||||||
|
new CapturingRenderersFactory(applicationContext)
|
||||||
|
.setImageDecoderFactory(
|
||||||
|
new ExternallyLoadedImageDecoder.Factory(
|
||||||
|
request -> listeningExecutorService.submit(() -> decode(request.uri))));
|
||||||
ConditionVariable loadingComplete = new ConditionVariable();
|
ConditionVariable loadingComplete = new ConditionVariable();
|
||||||
MediaSource.Factory mediaSourceFactory =
|
MediaSource.Factory mediaSourceFactory =
|
||||||
new DefaultMediaSourceFactory(applicationContext)
|
new DefaultMediaSourceFactory(applicationContext)
|
||||||
@ -177,27 +184,10 @@ public final class ExternallyLoadedImagePlaybackTest {
|
|||||||
TestPlayerRunHelper.runUntilIsLoading(player, /* expectedIsLoading= */ false);
|
TestPlayerRunHelper.runUntilIsLoading(player, /* expectedIsLoading= */ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class CustomImageDecoderFactory implements ImageDecoder.Factory {
|
private static Bitmap decode(Uri uri) throws ImageDecoderException {
|
||||||
|
|
||||||
@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);
|
|
||||||
AssetDataSource assetDataSource =
|
AssetDataSource assetDataSource =
|
||||||
new AssetDataSource(ApplicationProvider.getApplicationContext());
|
new AssetDataSource(ApplicationProvider.getApplicationContext());
|
||||||
DataSpec dataSpec = new DataSpec(Uri.parse(uriString));
|
DataSpec dataSpec = new DataSpec(uri);
|
||||||
@Nullable Bitmap bitmap;
|
@Nullable Bitmap bitmap;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -209,8 +199,7 @@ public final class ExternallyLoadedImagePlaybackTest {
|
|||||||
}
|
}
|
||||||
if (bitmap == null) {
|
if (bitmap == null) {
|
||||||
throw new ImageDecoderException(
|
throw new ImageDecoderException(
|
||||||
"Could not decode image data with BitmapFactory. uriString decoded from data = "
|
"Could not decode image data with BitmapFactory. uriString decoded from data = " + uri);
|
||||||
+ uriString);
|
|
||||||
}
|
}
|
||||||
return bitmap;
|
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