mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Support multiple streams in the ImageRenderer
PiperOrigin-RevId: 565410924
This commit is contained in:
parent
5ef5d46708
commit
16b0ea850f
20 changed files with 526 additions and 108 deletions
|
|
@ -31,4 +31,7 @@ public interface ImageOutput {
|
||||||
* @param bitmap The new image available.
|
* @param bitmap The new image available.
|
||||||
*/
|
*/
|
||||||
void onImageAvailable(long presentationTimeUs, Bitmap bitmap);
|
void onImageAvailable(long presentationTimeUs, Bitmap bitmap);
|
||||||
|
|
||||||
|
/** Called when the renderer is disabled. */
|
||||||
|
void onDisabled();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,16 +15,22 @@
|
||||||
*/
|
*/
|
||||||
package androidx.media3.exoplayer.image;
|
package androidx.media3.exoplayer.image;
|
||||||
|
|
||||||
import static androidx.media3.common.PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK;
|
import static androidx.media3.common.C.FIRST_FRAME_NOT_RENDERED;
|
||||||
|
import static androidx.media3.common.C.FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED;
|
||||||
|
import static androidx.media3.common.C.FIRST_FRAME_RENDERED;
|
||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
import static androidx.media3.common.util.Assertions.checkState;
|
import static androidx.media3.common.util.Assertions.checkState;
|
||||||
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||||
import static androidx.media3.exoplayer.source.SampleStream.FLAG_REQUIRE_FORMAT;
|
import static androidx.media3.exoplayer.source.SampleStream.FLAG_REQUIRE_FORMAT;
|
||||||
|
import static java.lang.Math.min;
|
||||||
|
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
import androidx.annotation.IntDef;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.PlaybackException;
|
import androidx.media3.common.PlaybackException;
|
||||||
|
import androidx.media3.common.util.LongArrayQueue;
|
||||||
import androidx.media3.common.util.TraceUtil;
|
import androidx.media3.common.util.TraceUtil;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.decoder.DecoderInputBuffer;
|
import androidx.media3.decoder.DecoderInputBuffer;
|
||||||
|
|
@ -35,31 +41,61 @@ import androidx.media3.exoplayer.Renderer;
|
||||||
import androidx.media3.exoplayer.RendererCapabilities;
|
import androidx.media3.exoplayer.RendererCapabilities;
|
||||||
import androidx.media3.exoplayer.source.MediaSource;
|
import androidx.media3.exoplayer.source.MediaSource;
|
||||||
import androidx.media3.exoplayer.source.SampleStream;
|
import androidx.media3.exoplayer.source.SampleStream;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
|
|
||||||
// TODO(b/289989736): Currently works for one stream only. Refactor so that it works for multiple
|
|
||||||
// inputs streams.
|
|
||||||
/** A {@link Renderer} implementation for images. */
|
/** A {@link Renderer} implementation for images. */
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public final class ImageRenderer extends BaseRenderer {
|
public final class ImageRenderer extends BaseRenderer {
|
||||||
|
|
||||||
private static final String TAG = "ImageRenderer";
|
private static final String TAG = "ImageRenderer";
|
||||||
|
|
||||||
private final DecoderInputBuffer flagsOnlyBuffer;
|
/** Decoder reinitialization states. */
|
||||||
|
@Documented
|
||||||
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
|
@Target(TYPE_USE)
|
||||||
|
@IntDef({
|
||||||
|
REINITIALIZATION_STATE_NONE,
|
||||||
|
REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM_THEN_WAIT,
|
||||||
|
REINITIALIZATION_STATE_WAIT_END_OF_STREAM
|
||||||
|
})
|
||||||
|
private @interface ReinitializationState {}
|
||||||
|
|
||||||
|
/** The decoder does not need to be re-initialized. */
|
||||||
|
private static final int REINITIALIZATION_STATE_NONE = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The input format has changed in a way that requires the decoder to be re-initialized, but we
|
||||||
|
* haven't yet signaled an end of stream to the existing decoder. We need to do so in order to
|
||||||
|
* ensure that it outputs any remaining buffers before we release it.
|
||||||
|
*/
|
||||||
|
private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM_THEN_WAIT = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The input format has changed in a way that requires the decoder to be re-initialized, and we've
|
||||||
|
* signaled an end of stream to the existing decoder. We're waiting for the decoder to output an
|
||||||
|
* end of stream signal to indicate that it has output any remaining buffers before we release it.
|
||||||
|
*/
|
||||||
|
private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 3;
|
||||||
|
|
||||||
private final ImageDecoder.Factory decoderFactory;
|
private final ImageDecoder.Factory decoderFactory;
|
||||||
private final ImageOutput imageOutput;
|
private final ImageOutput imageOutput;
|
||||||
|
private final DecoderInputBuffer flagsOnlyBuffer;
|
||||||
|
private final LongArrayQueue offsetQueue;
|
||||||
|
|
||||||
private @C.FirstFrameState int firstFrameState;
|
|
||||||
private boolean inputStreamEnded;
|
private boolean inputStreamEnded;
|
||||||
private boolean outputStreamEnded;
|
private boolean outputStreamEnded;
|
||||||
private long durationUs;
|
private @ReinitializationState int decoderReinitializationState;
|
||||||
private long offsetUs;
|
private @C.FirstFrameState int firstFrameState;
|
||||||
|
private @Nullable Format inputFormat;
|
||||||
private @Nullable ImageDecoder decoder;
|
private @Nullable ImageDecoder decoder;
|
||||||
private @Nullable DecoderInputBuffer inputBuffer;
|
private @Nullable DecoderInputBuffer inputBuffer;
|
||||||
private @Nullable ImageOutputBuffer outputBuffer;
|
private @Nullable ImageOutputBuffer outputBuffer;
|
||||||
private @MonotonicNonNull Format inputFormat;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance.
|
* Creates an instance.
|
||||||
|
|
@ -71,11 +107,12 @@ public final class ImageRenderer extends BaseRenderer {
|
||||||
*/
|
*/
|
||||||
public ImageRenderer(ImageDecoder.Factory decoderFactory, ImageOutput imageOutput) {
|
public ImageRenderer(ImageDecoder.Factory decoderFactory, ImageOutput imageOutput) {
|
||||||
super(C.TRACK_TYPE_IMAGE);
|
super(C.TRACK_TYPE_IMAGE);
|
||||||
flagsOnlyBuffer = DecoderInputBuffer.newNoDataInstance();
|
|
||||||
this.decoderFactory = decoderFactory;
|
this.decoderFactory = decoderFactory;
|
||||||
this.imageOutput = imageOutput;
|
this.imageOutput = imageOutput;
|
||||||
durationUs = C.TIME_UNSET;
|
flagsOnlyBuffer = DecoderInputBuffer.newNoDataInstance();
|
||||||
firstFrameState = C.FIRST_FRAME_NOT_RENDERED;
|
offsetQueue = new LongArrayQueue();
|
||||||
|
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
|
||||||
|
firstFrameState = FIRST_FRAME_NOT_RENDERED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -90,11 +127,11 @@ public final class ImageRenderer extends BaseRenderer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
||||||
checkState(durationUs != C.TIME_UNSET);
|
|
||||||
if (outputStreamEnded) {
|
if (outputStreamEnded) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// If the offsetQueue is empty, we haven't been given a stream to render.
|
||||||
|
checkState(!offsetQueue.isEmpty());
|
||||||
if (inputFormat == null) {
|
if (inputFormat == null) {
|
||||||
// We don't have a format yet, so try and read one.
|
// We don't have a format yet, so try and read one.
|
||||||
FormatHolder formatHolder = getFormatHolder();
|
FormatHolder formatHolder = getFormatHolder();
|
||||||
|
|
@ -102,9 +139,9 @@ public final class ImageRenderer extends BaseRenderer {
|
||||||
@SampleStream.ReadDataResult
|
@SampleStream.ReadDataResult
|
||||||
int result = readSource(formatHolder, flagsOnlyBuffer, FLAG_REQUIRE_FORMAT);
|
int result = readSource(formatHolder, flagsOnlyBuffer, FLAG_REQUIRE_FORMAT);
|
||||||
if (result == C.RESULT_FORMAT_READ) {
|
if (result == C.RESULT_FORMAT_READ) {
|
||||||
// Note that this works because we only expect to enter this if-condition once per playback
|
// Note that this works because we only expect to enter this if-condition once per playback.
|
||||||
// for now.
|
inputFormat = checkNotNull(formatHolder.format);
|
||||||
maybeInitDecoder(checkNotNull(formatHolder.format));
|
initDecoder();
|
||||||
} else if (result == C.RESULT_BUFFER_READ) {
|
} else if (result == C.RESULT_BUFFER_READ) {
|
||||||
// End of stream read having not read a format.
|
// End of stream read having not read a format.
|
||||||
checkState(flagsOnlyBuffer.isEndOfStream());
|
checkState(flagsOnlyBuffer.isEndOfStream());
|
||||||
|
|
@ -116,7 +153,6 @@ public final class ImageRenderer extends BaseRenderer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Rendering loop.
|
// Rendering loop.
|
||||||
TraceUtil.beginSection("drainAndFeedDecoder");
|
TraceUtil.beginSection("drainAndFeedDecoder");
|
||||||
|
|
@ -130,7 +166,9 @@ public final class ImageRenderer extends BaseRenderer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isReady() {
|
public boolean isReady() {
|
||||||
return firstFrameState == C.FIRST_FRAME_RENDERED;
|
return firstFrameState == FIRST_FRAME_RENDERED
|
||||||
|
|| (firstFrameState == FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED
|
||||||
|
&& outputBuffer != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -138,6 +176,14 @@ public final class ImageRenderer extends BaseRenderer {
|
||||||
return outputStreamEnded;
|
return outputStreamEnded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) {
|
||||||
|
firstFrameState =
|
||||||
|
mayRenderStartOfStream
|
||||||
|
? C.FIRST_FRAME_NOT_RENDERED
|
||||||
|
: C.FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onStreamChanged(
|
protected void onStreamChanged(
|
||||||
Format[] formats,
|
Format[] formats,
|
||||||
|
|
@ -145,36 +191,41 @@ public final class ImageRenderer extends BaseRenderer {
|
||||||
long offsetUs,
|
long offsetUs,
|
||||||
MediaSource.MediaPeriodId mediaPeriodId)
|
MediaSource.MediaPeriodId mediaPeriodId)
|
||||||
throws ExoPlaybackException {
|
throws ExoPlaybackException {
|
||||||
// TODO(b/289989736): when the mediaPeriodId is signalled to the renders, collect and set
|
|
||||||
// durationUs here.
|
|
||||||
durationUs = 2 * C.MICROS_PER_SECOND;
|
|
||||||
this.offsetUs = offsetUs;
|
|
||||||
super.onStreamChanged(formats, startPositionUs, offsetUs, mediaPeriodId);
|
super.onStreamChanged(formats, startPositionUs, offsetUs, mediaPeriodId);
|
||||||
|
offsetQueue.add(offsetUs);
|
||||||
|
inputStreamEnded = false;
|
||||||
|
outputStreamEnded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPositionReset(long positionUs, boolean joining) {
|
protected void onPositionReset(long positionUs, boolean joining) {
|
||||||
// Since the renderer only supports playing one image from, this is currently a no-op (don't
|
lowerFirstFrameState(FIRST_FRAME_NOT_RENDERED);
|
||||||
// need to consider a new stream because it will be the same as the last one).
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDisabled() {
|
protected void onDisabled() {
|
||||||
releaseResources();
|
offsetQueue.clear();
|
||||||
|
inputFormat = null;
|
||||||
|
releaseDecoderResources();
|
||||||
|
imageOutput.onDisabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onReset() {
|
protected void onReset() {
|
||||||
releaseResources();
|
offsetQueue.clear();
|
||||||
|
releaseDecoderResources();
|
||||||
|
lowerFirstFrameState(FIRST_FRAME_NOT_RENDERED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onRelease() {
|
protected void onRelease() {
|
||||||
releaseResources();
|
offsetQueue.clear();
|
||||||
|
releaseDecoderResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to dequeue an output buffer from the decoder and, if successful, renders it.
|
* Attempts to dequeue an output buffer from the decoder and, if successful and permitted to,
|
||||||
|
* renders it.
|
||||||
*
|
*
|
||||||
* @param positionUs The player's current position.
|
* @param positionUs The player's current position.
|
||||||
* @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
|
* @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
|
||||||
|
|
@ -183,7 +234,7 @@ public final class ImageRenderer extends BaseRenderer {
|
||||||
* @throws ImageDecoderException If an error occurs draining the output buffer.
|
* @throws ImageDecoderException If an error occurs draining the output buffer.
|
||||||
*/
|
*/
|
||||||
private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs)
|
private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs)
|
||||||
throws ImageDecoderException {
|
throws ImageDecoderException, ExoPlaybackException {
|
||||||
if (outputBuffer == null) {
|
if (outputBuffer == null) {
|
||||||
checkStateNotNull(decoder);
|
checkStateNotNull(decoder);
|
||||||
outputBuffer = decoder.dequeueOutputBuffer();
|
outputBuffer = decoder.dequeueOutputBuffer();
|
||||||
|
|
@ -191,27 +242,45 @@ public final class ImageRenderer extends BaseRenderer {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (outputBuffer.isEndOfStream()) {
|
if (firstFrameState == FIRST_FRAME_NOT_RENDERED_ONLY_ALLOWED_IF_STARTED
|
||||||
outputBuffer.release();
|
&& getState() != STATE_STARTED) {
|
||||||
outputBuffer = null;
|
|
||||||
outputStreamEnded = true;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (checkNotNull(outputBuffer).isEndOfStream()) {
|
||||||
|
offsetQueue.remove();
|
||||||
|
if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
|
||||||
|
// We're waiting to re-initialize the decoder, and have now processed all final buffers.
|
||||||
|
releaseDecoderResources();
|
||||||
|
checkStateNotNull(inputFormat);
|
||||||
|
initDecoder();
|
||||||
|
} else {
|
||||||
|
checkNotNull(outputBuffer).release();
|
||||||
|
outputBuffer = null;
|
||||||
|
if (offsetQueue.isEmpty()) {
|
||||||
|
outputStreamEnded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
checkStateNotNull(outputBuffer);
|
||||||
if (!processOutputBuffer(positionUs, elapsedRealtimeUs)) {
|
if (!processOutputBuffer(positionUs, elapsedRealtimeUs)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
firstFrameState = FIRST_FRAME_RENDERED;
|
||||||
firstFrameState = C.FIRST_FRAME_RENDERED;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresNonNull("outputBuffer")
|
|
||||||
@SuppressWarnings("unused") // Will be used or removed when the integrated with the videoSink.
|
@SuppressWarnings("unused") // Will be used or removed when the integrated with the videoSink.
|
||||||
|
@RequiresNonNull("outputBuffer")
|
||||||
private boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs) {
|
private boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs) {
|
||||||
checkStateNotNull(
|
Bitmap outputBitmap =
|
||||||
outputBuffer.bitmap, "Non-EOS buffer came back from the decoder without bitmap.");
|
checkNotNull(
|
||||||
imageOutput.onImageAvailable(positionUs - offsetUs, outputBuffer.bitmap);
|
outputBuffer.bitmap, "Non-EOS buffer came back from the decoder without bitmap.");
|
||||||
|
if (positionUs < outputBuffer.timeUs) {
|
||||||
|
// It's too early to render the buffer.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
imageOutput.onImageAvailable(outputBuffer.timeUs - offsetQueue.element(), outputBitmap);
|
||||||
checkNotNull(outputBuffer).release();
|
checkNotNull(outputBuffer).release();
|
||||||
outputBuffer = null;
|
outputBuffer = null;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -220,9 +289,12 @@ public final class ImageRenderer extends BaseRenderer {
|
||||||
/**
|
/**
|
||||||
* @return Whether we can feed more input data to the decoder.
|
* @return Whether we can feed more input data to the decoder.
|
||||||
*/
|
*/
|
||||||
private boolean feedInputBuffer() throws ExoPlaybackException, ImageDecoderException {
|
private boolean feedInputBuffer() throws ImageDecoderException {
|
||||||
FormatHolder formatHolder = getFormatHolder();
|
FormatHolder formatHolder = getFormatHolder();
|
||||||
if (decoder == null || inputStreamEnded) {
|
if (decoder == null
|
||||||
|
|| decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM
|
||||||
|
|| inputStreamEnded) {
|
||||||
|
// We need to reinitialize the decoder or the input stream has ended.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (inputBuffer == null) {
|
if (inputBuffer == null) {
|
||||||
|
|
@ -231,6 +303,14 @@ public final class ImageRenderer extends BaseRenderer {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM_THEN_WAIT) {
|
||||||
|
checkStateNotNull(inputBuffer);
|
||||||
|
inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
|
||||||
|
checkNotNull(decoder).queueInputBuffer(inputBuffer);
|
||||||
|
inputBuffer = null;
|
||||||
|
decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
switch (readSource(formatHolder, inputBuffer, /* readFlags= */ 0)) {
|
switch (readSource(formatHolder, inputBuffer, /* readFlags= */ 0)) {
|
||||||
case C.RESULT_NOTHING_READ:
|
case C.RESULT_NOTHING_READ:
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -245,26 +325,18 @@ public final class ImageRenderer extends BaseRenderer {
|
||||||
inputBuffer = null;
|
inputBuffer = null;
|
||||||
return true;
|
return true;
|
||||||
case C.RESULT_FORMAT_READ:
|
case C.RESULT_FORMAT_READ:
|
||||||
if (checkNotNull(formatHolder.format).equals(inputFormat)) {
|
inputFormat = checkNotNull(formatHolder.format);
|
||||||
return true;
|
decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM_THEN_WAIT;
|
||||||
}
|
return true;
|
||||||
throw createRendererException(
|
|
||||||
new UnsupportedOperationException(
|
|
||||||
"Changing format is not supported in the ImageRenderer."),
|
|
||||||
formatHolder.format,
|
|
||||||
ERROR_CODE_FAILED_RUNTIME_CHECK);
|
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException();
|
throw new IllegalStateException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresNonNull("inputFormat")
|
||||||
@EnsuresNonNull("decoder")
|
@EnsuresNonNull("decoder")
|
||||||
private void maybeInitDecoder(Format format) throws ExoPlaybackException {
|
private void initDecoder() throws ExoPlaybackException {
|
||||||
if (inputFormat != null && inputFormat.equals(format) && decoder != null) {
|
if (canCreateDecoderForFormat(inputFormat)) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
inputFormat = format;
|
|
||||||
if (canCreateDecoderForFormat(format)) {
|
|
||||||
if (decoder != null) {
|
if (decoder != null) {
|
||||||
decoder.release();
|
decoder.release();
|
||||||
}
|
}
|
||||||
|
|
@ -272,7 +344,7 @@ public final class ImageRenderer extends BaseRenderer {
|
||||||
} else {
|
} else {
|
||||||
throw createRendererException(
|
throw createRendererException(
|
||||||
new ImageDecoderException("Provided decoder factory can't create decoder for format."),
|
new ImageDecoderException("Provided decoder factory can't create decoder for format."),
|
||||||
format,
|
inputFormat,
|
||||||
PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED);
|
PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -283,12 +355,17 @@ public final class ImageRenderer extends BaseRenderer {
|
||||||
|| supportsFormat == RendererCapabilities.create(C.FORMAT_EXCEEDS_CAPABILITIES);
|
|| supportsFormat == RendererCapabilities.create(C.FORMAT_EXCEEDS_CAPABILITIES);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void releaseResources() {
|
private void lowerFirstFrameState(@C.FirstFrameState int firstFrameState) {
|
||||||
|
this.firstFrameState = min(this.firstFrameState, firstFrameState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseDecoderResources() {
|
||||||
inputBuffer = null;
|
inputBuffer = null;
|
||||||
if (outputBuffer != null) {
|
if (outputBuffer != null) {
|
||||||
outputBuffer.release();
|
outputBuffer.release();
|
||||||
}
|
}
|
||||||
outputBuffer = null;
|
outputBuffer = null;
|
||||||
|
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
|
||||||
if (decoder != null) {
|
if (decoder != null) {
|
||||||
decoder.release();
|
decoder.release();
|
||||||
decoder = null;
|
decoder = null;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2020 The Android Open Source Project
|
* Copyright (C) 2023 The Android Open Source Project
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -30,7 +30,13 @@ import androidx.media3.test.utils.FakeClock;
|
||||||
import androidx.media3.test.utils.robolectric.PlaybackOutput;
|
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 com.google.common.collect.ImmutableList;
|
import com.google.common.collect.Collections2;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.robolectric.ParameterizedRobolectricTestRunner;
|
import org.robolectric.ParameterizedRobolectricTestRunner;
|
||||||
|
|
@ -42,17 +48,23 @@ import org.robolectric.annotation.GraphicsMode;
|
||||||
@RunWith(ParameterizedRobolectricTestRunner.class)
|
@RunWith(ParameterizedRobolectricTestRunner.class)
|
||||||
@GraphicsMode(value = NATIVE)
|
@GraphicsMode(value = NATIVE)
|
||||||
public class ImagePlaybackTest {
|
public class ImagePlaybackTest {
|
||||||
|
@Parameter public Set<String> inputFiles;
|
||||||
@Parameter public String inputFile;
|
|
||||||
|
|
||||||
@Parameters(name = "{0}")
|
@Parameters(name = "{0}")
|
||||||
public static ImmutableList<String> mediaSamples() {
|
public static List<Set<String>> mediaSamples() {
|
||||||
// TODO(b/289989736): When extraction for other types of images is implemented, add those image
|
// Robolectric's ShadowNativeBitmapFactory doesn't support decoding HEIF format, so we don't
|
||||||
// types to this list.
|
// test that here.
|
||||||
// Robolectric's NativeShadowBitmapFactory doesn't support decoding HEIF format, so we don't
|
// TODO b/300457060 - Find out why jpegs cause flaky failures in this test and then add jpegs to
|
||||||
// test that format here.
|
// this list if possible.
|
||||||
return ImmutableList.of(
|
return new ArrayList<>(
|
||||||
"png/non-motion-photo-shortened.png", "jpeg/non-motion-photo-shortened.jpg");
|
Collections2.filter(
|
||||||
|
Sets.powerSet(
|
||||||
|
ImmutableSet.of(
|
||||||
|
"bitmap/input_images/media3test.png",
|
||||||
|
"bmp/non-motion-photo-shortened-cropped.bmp",
|
||||||
|
"png/non-motion-photo-shortened.png",
|
||||||
|
"webp/ic_launcher_round.webp")),
|
||||||
|
/* predicate= */ input -> !input.isEmpty()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -64,23 +76,43 @@ public class ImagePlaybackTest {
|
||||||
ExoPlayer player =
|
ExoPlayer player =
|
||||||
new ExoPlayer.Builder(applicationContext, renderersFactory).setClock(clock).build();
|
new ExoPlayer.Builder(applicationContext, renderersFactory).setClock(clock).build();
|
||||||
PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory);
|
PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory);
|
||||||
long durationMs = 5 * C.MILLIS_PER_SECOND;
|
List<String> sortedInputFiles = new ArrayList<>(inputFiles);
|
||||||
player.setMediaItem(
|
Collections.sort(sortedInputFiles);
|
||||||
new MediaItem.Builder()
|
List<MediaItem> mediaItems = new ArrayList<>(inputFiles.size());
|
||||||
.setUri("asset:///media/" + inputFile)
|
long totalDurationMs = 0;
|
||||||
.setImageDurationMs(durationMs)
|
long currentDurationMs = 3 * C.MILLIS_PER_SECOND;
|
||||||
.build());
|
for (String inputFile : sortedInputFiles) {
|
||||||
|
mediaItems.add(
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setUri("asset:///media/" + inputFile)
|
||||||
|
.setImageDurationMs(currentDurationMs)
|
||||||
|
.build());
|
||||||
|
totalDurationMs += currentDurationMs;
|
||||||
|
if (currentDurationMs < 5 * C.MILLIS_PER_SECOND) {
|
||||||
|
currentDurationMs += C.MILLIS_PER_SECOND;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
player.setMediaItems(mediaItems);
|
||||||
player.prepare();
|
player.prepare();
|
||||||
|
|
||||||
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
|
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
|
||||||
long playerStartedMs = clock.elapsedRealtime();
|
long playerStartedMs = clock.elapsedRealtime();
|
||||||
player.play();
|
player.play();
|
||||||
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
|
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
|
||||||
long playbackDurationMs = clock.elapsedRealtime() - playerStartedMs;
|
long playbackDurationMs = clock.elapsedRealtime() - playerStartedMs;
|
||||||
player.release();
|
player.release();
|
||||||
|
assertThat(playbackDurationMs).isEqualTo(totalDurationMs);
|
||||||
assertThat(playbackDurationMs).isEqualTo(durationMs);
|
|
||||||
DumpFileAsserts.assertOutput(
|
DumpFileAsserts.assertOutput(
|
||||||
applicationContext, playbackOutput, "playbackdumps/" + inputFile + ".dump");
|
applicationContext,
|
||||||
|
playbackOutput,
|
||||||
|
"playbackdumps/image/" + generateName(sortedInputFiles) + ".dump");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String generateName(List<String> sortedInputFiles) {
|
||||||
|
StringBuilder name = new StringBuilder();
|
||||||
|
for (String inputFile : sortedInputFiles) {
|
||||||
|
name.append(inputFile, inputFile.lastIndexOf("/") + 1, inputFile.length()).append("+");
|
||||||
|
}
|
||||||
|
name.setLength(name.length() - 1);
|
||||||
|
return name.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,12 @@ import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.o
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
import android.util.Pair;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.MimeTypes;
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.util.Clock;
|
import androidx.media3.common.util.Clock;
|
||||||
import androidx.media3.common.util.TimedValueQueue;
|
import androidx.media3.common.util.SystemClock;
|
||||||
import androidx.media3.exoplayer.RendererConfiguration;
|
import androidx.media3.exoplayer.RendererConfiguration;
|
||||||
import androidx.media3.exoplayer.analytics.PlayerId;
|
import androidx.media3.exoplayer.analytics.PlayerId;
|
||||||
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
|
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
|
||||||
|
|
@ -34,34 +35,65 @@ import androidx.media3.exoplayer.upstream.DefaultAllocator;
|
||||||
import androidx.media3.test.utils.FakeSampleStream;
|
import androidx.media3.test.utils.FakeSampleStream;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
/** Unit test for {@link ImageRenderer}. */
|
/** Unit tests for {@link ImageRenderer}. */
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class ImageRendererTest {
|
public class ImageRendererTest {
|
||||||
|
private static final long DEFAULT_LOOP_TIMEOUT_MS = 10 * C.MILLIS_PER_SECOND;
|
||||||
private static final Format FORMAT =
|
private static final String IS_READY_TIMEOUT_MESSAGE =
|
||||||
|
"Renderer not ready after " + DEFAULT_LOOP_TIMEOUT_MS + " milliseconds.";
|
||||||
|
private static final String IS_ENDED_TIMEOUT_MESSAGE =
|
||||||
|
"Renderer not ended after " + DEFAULT_LOOP_TIMEOUT_MS + " milliseconds.";
|
||||||
|
private static final String HAS_READ_STREAM_TO_END_TIMEOUT_MESSAGE =
|
||||||
|
"Renderer has not read stream to end after " + DEFAULT_LOOP_TIMEOUT_MS + " milliseconds.";
|
||||||
|
private static final Format PNG_FORMAT =
|
||||||
new Format.Builder()
|
new Format.Builder()
|
||||||
.setContainerMimeType(MimeTypes.IMAGE_PNG)
|
.setContainerMimeType(MimeTypes.IMAGE_PNG)
|
||||||
.setTileCountVertical(1)
|
.setTileCountVertical(1)
|
||||||
.setTileCountHorizontal(1)
|
.setTileCountHorizontal(1)
|
||||||
.build();
|
.build();
|
||||||
|
private static final Format JPEG_FORMAT =
|
||||||
|
new Format.Builder()
|
||||||
|
.setContainerMimeType(MimeTypes.IMAGE_JPEG)
|
||||||
|
.setTileCountVertical(1)
|
||||||
|
.setTileCountHorizontal(1)
|
||||||
|
.build();
|
||||||
|
|
||||||
private final TimedValueQueue<Bitmap> renderedBitmaps = new TimedValueQueue<>();
|
private final List<Pair<Long, Bitmap>> renderedBitmaps = new ArrayList<>();
|
||||||
private final Bitmap fakeDecodedBitmap =
|
private final Bitmap fakeDecodedBitmap1 =
|
||||||
Bitmap.createBitmap(/* width= */ 1, /* height= */ 1, Bitmap.Config.ARGB_8888);
|
Bitmap.createBitmap(/* width= */ 1, /* height= */ 1, Bitmap.Config.ARGB_8888);
|
||||||
|
private final Bitmap fakeDecodedBitmap2 =
|
||||||
|
Bitmap.createBitmap(/* width= */ 2, /* height= */ 2, Bitmap.Config.ARGB_8888);
|
||||||
|
|
||||||
private ImageRenderer renderer;
|
private ImageRenderer renderer;
|
||||||
|
private int decodeCallCount;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
|
decodeCallCount = 0;
|
||||||
ImageDecoder.Factory fakeDecoderFactory =
|
ImageDecoder.Factory fakeDecoderFactory =
|
||||||
new DefaultImageDecoder.Factory((data, length) -> fakeDecodedBitmap);
|
new DefaultImageDecoder.Factory(
|
||||||
ImageOutput capturingImageOutput = renderedBitmaps::add;
|
(data, length) -> ++decodeCallCount == 1 ? fakeDecodedBitmap1 : fakeDecodedBitmap2);
|
||||||
renderer = new ImageRenderer(fakeDecoderFactory, capturingImageOutput);
|
ImageOutput queuingImageOutput =
|
||||||
|
new ImageOutput() {
|
||||||
|
@Override
|
||||||
|
public void onImageAvailable(long presentationTimeUs, Bitmap bitmap) {
|
||||||
|
renderedBitmaps.add(Pair.create(presentationTimeUs, bitmap));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisabled() {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
renderer = new ImageRenderer(fakeDecoderFactory, queuingImageOutput);
|
||||||
renderer.init(/* index= */ 0, PlayerId.UNSET, Clock.DEFAULT);
|
renderer.init(/* index= */ 0, PlayerId.UNSET, Clock.DEFAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,22 +105,12 @@ public class ImageRendererTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void renderOneStream_rendersToImageOutput() throws Exception {
|
public void renderOneStream_withMayRenderStartOfStream_rendersToImageOutput() throws Exception {
|
||||||
FakeSampleStream fakeSampleStream =
|
FakeSampleStream fakeSampleStream = createSampleStream(/* timeUs= */ 0);
|
||||||
new FakeSampleStream(
|
|
||||||
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
|
|
||||||
/* mediaSourceEventDispatcher= */ null,
|
|
||||||
DrmSessionManager.DRM_UNSUPPORTED,
|
|
||||||
new DrmSessionEventListener.EventDispatcher(),
|
|
||||||
FORMAT,
|
|
||||||
ImmutableList.of(
|
|
||||||
oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM));
|
|
||||||
fakeSampleStream.writeData(/* startPositionUs= */ 0);
|
fakeSampleStream.writeData(/* startPositionUs= */ 0);
|
||||||
// TODO(b/289989736): When the mediaPeriodId is signalled to the renders set durationUs here and
|
|
||||||
// assert on it.
|
|
||||||
renderer.enable(
|
renderer.enable(
|
||||||
RendererConfiguration.DEFAULT,
|
RendererConfiguration.DEFAULT,
|
||||||
new Format[] {FORMAT},
|
new Format[] {PNG_FORMAT},
|
||||||
fakeSampleStream,
|
fakeSampleStream,
|
||||||
/* positionUs= */ 0,
|
/* positionUs= */ 0,
|
||||||
/* joining= */ false,
|
/* joining= */ false,
|
||||||
|
|
@ -98,15 +120,168 @@ public class ImageRendererTest {
|
||||||
new MediaSource.MediaPeriodId(new Object()));
|
new MediaSource.MediaPeriodId(new Object()));
|
||||||
renderer.setCurrentStreamFinal();
|
renderer.setCurrentStreamFinal();
|
||||||
|
|
||||||
while (!renderer.isReady()) {
|
StopWatch isReadyStopWatch = new StopWatch(IS_READY_TIMEOUT_MESSAGE);
|
||||||
|
while (!renderer.isReady() && isReadyStopWatch.ensureNotExpired()) {
|
||||||
renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0);
|
renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
assertThat(renderedBitmaps.size()).isEqualTo(1);
|
assertThat(renderedBitmaps).hasSize(1);
|
||||||
assertThat(renderedBitmaps.poll(0)).isSameInstanceAs(fakeDecodedBitmap);
|
assertThat(renderedBitmaps.get(0).first).isEqualTo(0L);
|
||||||
|
assertThat(renderedBitmaps.get(0).second).isSameInstanceAs(fakeDecodedBitmap1);
|
||||||
|
}
|
||||||
|
|
||||||
renderer.render(
|
@Test
|
||||||
/* positionUs= */ C.MICROS_PER_SECOND, /* elapsedRealtimeUs= */ C.MICROS_PER_SECOND);
|
public void renderOneStream_withoutMayRenderStartOfStream_rendersToImageOutput()
|
||||||
assertThat(renderer.isEnded()).isTrue();
|
throws Exception {
|
||||||
|
FakeSampleStream fakeSampleStream = createSampleStream(/* timeUs= */ 0);
|
||||||
|
fakeSampleStream.writeData(/* startPositionUs= */ 0);
|
||||||
|
renderer.enable(
|
||||||
|
RendererConfiguration.DEFAULT,
|
||||||
|
new Format[] {PNG_FORMAT},
|
||||||
|
fakeSampleStream,
|
||||||
|
/* positionUs= */ 0,
|
||||||
|
/* joining= */ false,
|
||||||
|
/* mayRenderStartOfStream= */ false,
|
||||||
|
/* startPositionUs= */ 0,
|
||||||
|
/* offsetUs= */ 0,
|
||||||
|
new MediaSource.MediaPeriodId(new Object()));
|
||||||
|
renderer.setCurrentStreamFinal();
|
||||||
|
|
||||||
|
StopWatch isReadyStopWatch = new StopWatch(IS_READY_TIMEOUT_MESSAGE);
|
||||||
|
while (!renderer.isReady() && isReadyStopWatch.ensureNotExpired()) {
|
||||||
|
renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0);
|
||||||
|
}
|
||||||
|
assertThat(renderedBitmaps).isEmpty();
|
||||||
|
renderer.start();
|
||||||
|
StopWatch isEndedStopWatch = new StopWatch(IS_ENDED_TIMEOUT_MESSAGE);
|
||||||
|
while (!renderer.isEnded() && isEndedStopWatch.ensureNotExpired()) {
|
||||||
|
renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0);
|
||||||
|
}
|
||||||
|
renderer.stop();
|
||||||
|
|
||||||
|
assertThat(renderedBitmaps).hasSize(1);
|
||||||
|
assertThat(renderedBitmaps.get(0).first).isEqualTo(0L);
|
||||||
|
assertThat(renderedBitmaps.get(0).second).isSameInstanceAs(fakeDecodedBitmap1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void renderTwoStreams_sameFormat_rendersToImageOutput() throws Exception {
|
||||||
|
FakeSampleStream fakeSampleStream1 = createSampleStream(/* timeUs= */ 0);
|
||||||
|
fakeSampleStream1.writeData(/* startPositionUs= */ 0);
|
||||||
|
FakeSampleStream fakeSampleStream2 = createSampleStream(/* timeUs= */ 10);
|
||||||
|
fakeSampleStream2.writeData(/* startPositionUs= */ 10);
|
||||||
|
renderer.enable(
|
||||||
|
RendererConfiguration.DEFAULT,
|
||||||
|
new Format[] {PNG_FORMAT},
|
||||||
|
fakeSampleStream1,
|
||||||
|
/* positionUs= */ 0,
|
||||||
|
/* joining= */ false,
|
||||||
|
/* mayRenderStartOfStream= */ true,
|
||||||
|
/* startPositionUs= */ 0,
|
||||||
|
/* offsetUs= */ 0,
|
||||||
|
new MediaSource.MediaPeriodId(new Object()));
|
||||||
|
|
||||||
|
StopWatch isReadyStopWatch = new StopWatch(IS_READY_TIMEOUT_MESSAGE);
|
||||||
|
while (!renderer.isReady() && isReadyStopWatch.ensureNotExpired()) {
|
||||||
|
renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0);
|
||||||
|
}
|
||||||
|
renderer.start();
|
||||||
|
StopWatch hasReadStreamToEndStopWatch = new StopWatch(HAS_READ_STREAM_TO_END_TIMEOUT_MESSAGE);
|
||||||
|
while (!renderer.hasReadStreamToEnd() && hasReadStreamToEndStopWatch.ensureNotExpired()) {
|
||||||
|
renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0);
|
||||||
|
}
|
||||||
|
renderer.replaceStream(
|
||||||
|
new Format[] {PNG_FORMAT},
|
||||||
|
fakeSampleStream2,
|
||||||
|
/* startPositionUs= */ 10,
|
||||||
|
/* offsetUs= */ 0,
|
||||||
|
new MediaSource.MediaPeriodId(new Object()));
|
||||||
|
renderer.setCurrentStreamFinal();
|
||||||
|
StopWatch isEndedStopWatch = new StopWatch(IS_ENDED_TIMEOUT_MESSAGE);
|
||||||
|
while (!renderer.isEnded() && isEndedStopWatch.ensureNotExpired()) {
|
||||||
|
renderer.render(/* positionUs= */ 10, /* elapsedRealtimeUs= */ 0);
|
||||||
|
}
|
||||||
|
renderer.stop();
|
||||||
|
|
||||||
|
assertThat(renderedBitmaps).hasSize(2);
|
||||||
|
assertThat(renderedBitmaps.get(0).first).isEqualTo(0L);
|
||||||
|
assertThat(renderedBitmaps.get(0).second).isSameInstanceAs(fakeDecodedBitmap1);
|
||||||
|
assertThat(renderedBitmaps.get(1).first).isEqualTo(10L);
|
||||||
|
assertThat(renderedBitmaps.get(1).second).isSameInstanceAs(fakeDecodedBitmap2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void renderTwoStreams_differentFormat_rendersToImageOutput() throws Exception {
|
||||||
|
FakeSampleStream fakeSampleStream1 = createSampleStream(/* timeUs= */ 0);
|
||||||
|
fakeSampleStream1.writeData(/* startPositionUs= */ 0);
|
||||||
|
FakeSampleStream fakeSampleStream2 = createSampleStream(/* timeUs= */ 10);
|
||||||
|
fakeSampleStream2.writeData(/* startPositionUs= */ 10);
|
||||||
|
renderer.enable(
|
||||||
|
RendererConfiguration.DEFAULT,
|
||||||
|
new Format[] {PNG_FORMAT},
|
||||||
|
fakeSampleStream1,
|
||||||
|
/* positionUs= */ 0,
|
||||||
|
/* joining= */ false,
|
||||||
|
/* mayRenderStartOfStream= */ true,
|
||||||
|
/* startPositionUs= */ 0,
|
||||||
|
/* offsetUs= */ 0,
|
||||||
|
new MediaSource.MediaPeriodId(new Object()));
|
||||||
|
|
||||||
|
StopWatch isReadyStopWatch = new StopWatch(IS_READY_TIMEOUT_MESSAGE);
|
||||||
|
while (!renderer.isReady() && isReadyStopWatch.ensureNotExpired()) {
|
||||||
|
renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0);
|
||||||
|
}
|
||||||
|
renderer.start();
|
||||||
|
StopWatch hasReadStreamToEndStopWatch = new StopWatch(HAS_READ_STREAM_TO_END_TIMEOUT_MESSAGE);
|
||||||
|
while (!renderer.hasReadStreamToEnd() && hasReadStreamToEndStopWatch.ensureNotExpired()) {
|
||||||
|
renderer.render(/* positionUs= */ 0, /* elapsedRealtimeUs= */ 0);
|
||||||
|
}
|
||||||
|
renderer.replaceStream(
|
||||||
|
new Format[] {JPEG_FORMAT},
|
||||||
|
fakeSampleStream2,
|
||||||
|
/* startPositionUs= */ 10,
|
||||||
|
/* offsetUs= */ 0,
|
||||||
|
new MediaSource.MediaPeriodId(new Object()));
|
||||||
|
renderer.setCurrentStreamFinal();
|
||||||
|
StopWatch isEndedStopWatch = new StopWatch(IS_ENDED_TIMEOUT_MESSAGE);
|
||||||
|
while (!renderer.isEnded() && isEndedStopWatch.ensureNotExpired()) {
|
||||||
|
renderer.render(/* positionUs= */ 10, /* elapsedRealtimeUs= */ 0);
|
||||||
|
}
|
||||||
|
renderer.stop();
|
||||||
|
|
||||||
|
assertThat(renderedBitmaps).hasSize(2);
|
||||||
|
assertThat(renderedBitmaps.get(0).first).isEqualTo(0L);
|
||||||
|
assertThat(renderedBitmaps.get(0).second).isSameInstanceAs(fakeDecodedBitmap1);
|
||||||
|
assertThat(renderedBitmaps.get(1).first).isEqualTo(10L);
|
||||||
|
assertThat(renderedBitmaps.get(1).second).isSameInstanceAs(fakeDecodedBitmap2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FakeSampleStream createSampleStream(long timeUs) {
|
||||||
|
return new FakeSampleStream(
|
||||||
|
new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024),
|
||||||
|
/* mediaSourceEventDispatcher= */ null,
|
||||||
|
DrmSessionManager.DRM_UNSUPPORTED,
|
||||||
|
new DrmSessionEventListener.EventDispatcher(),
|
||||||
|
PNG_FORMAT,
|
||||||
|
ImmutableList.of(oneByteSample(timeUs, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class StopWatch {
|
||||||
|
private final long startTimeMs;
|
||||||
|
private final long timeOutMs;
|
||||||
|
private final String timeoutMessage;
|
||||||
|
|
||||||
|
public StopWatch(String timeoutMessage) {
|
||||||
|
startTimeMs = SystemClock.DEFAULT.currentTimeMillis();
|
||||||
|
timeOutMs = DEFAULT_LOOP_TIMEOUT_MS;
|
||||||
|
this.timeoutMessage = timeoutMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean ensureNotExpired() throws TimeoutException {
|
||||||
|
if (startTimeMs + timeOutMs < SystemClock.DEFAULT.currentTimeMillis()) {
|
||||||
|
throw new TimeoutException(timeoutMessage);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
ImageOutput:
|
||||||
|
rendered image count = 1
|
||||||
|
image output #1:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = 443865884
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
ImageOutput:
|
||||||
|
rendered image count = 2
|
||||||
|
image output #1:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = -389047680
|
||||||
|
image output #2:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = 443865884
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
ImageOutput:
|
||||||
|
rendered image count = 3
|
||||||
|
image output #1:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = -389047680
|
||||||
|
image output #2:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = -1851534335
|
||||||
|
image output #3:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = 443865884
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
ImageOutput:
|
||||||
|
rendered image count = 4
|
||||||
|
image output #1:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = -389047680
|
||||||
|
image output #2:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = -1851534335
|
||||||
|
image output #3:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = 1367007828
|
||||||
|
image output #4:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = 443865884
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
ImageOutput:
|
||||||
|
rendered image count = 3
|
||||||
|
image output #1:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = -389047680
|
||||||
|
image output #2:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = -1851534335
|
||||||
|
image output #3:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = 1367007828
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
ImageOutput:
|
||||||
|
rendered image count = 2
|
||||||
|
image output #1:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = -389047680
|
||||||
|
image output #2:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = -1851534335
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
ImageOutput:
|
||||||
|
rendered image count = 3
|
||||||
|
image output #1:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = -389047680
|
||||||
|
image output #2:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = 1367007828
|
||||||
|
image output #3:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = 443865884
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
ImageOutput:
|
||||||
|
rendered image count = 2
|
||||||
|
image output #1:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = -389047680
|
||||||
|
image output #2:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = 1367007828
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
ImageOutput:
|
||||||
|
rendered image count = 1
|
||||||
|
image output #1:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = -389047680
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
ImageOutput:
|
||||||
|
rendered image count = 2
|
||||||
|
image output #1:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = -1851534335
|
||||||
|
image output #2:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = 443865884
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
ImageOutput:
|
||||||
|
rendered image count = 3
|
||||||
|
image output #1:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = -1851534335
|
||||||
|
image output #2:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = 1367007828
|
||||||
|
image output #3:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = 443865884
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
ImageOutput:
|
||||||
|
rendered image count = 2
|
||||||
|
image output #1:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = -1851534335
|
||||||
|
image output #2:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = 1367007828
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
ImageOutput:
|
||||||
|
rendered image count = 1
|
||||||
|
image output #1:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = -1851534335
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
ImageOutput:
|
||||||
|
rendered image count = 2
|
||||||
|
image output #1:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = 1367007828
|
||||||
|
image output #2:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = 443865884
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
ImageOutput:
|
||||||
|
rendered image count = 1
|
||||||
|
image output #1:
|
||||||
|
presentationTimeUs = 0
|
||||||
|
bitmap hash = 1367007828
|
||||||
|
|
@ -57,6 +57,11 @@ public final class CapturingImageOutput implements Dumpable, ImageOutput {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisabled() {
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void dump(Dumper dumper) {
|
public void dump(Dumper dumper) {
|
||||||
dumper.startBlock("ImageOutput");
|
dumper.startBlock("ImageOutput");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue