mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Fix Frame Extractor getting stuck
Frame Extractor was getting stuck with SeekParameters.CLOSEST_SYNC. onPositionDiscontinuity callback was sometimes being called with a non-adjusted new position. Fix this by monitoring player state ready. For the player to become ready, we have to override renderer isReady. PiperOrigin-RevId: 701924752
This commit is contained in:
parent
da4376d48d
commit
d214e90ce4
@ -209,6 +209,45 @@ public class FrameExtractorTest {
|
|||||||
.isEqualTo(3);
|
.isEqualTo(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void extractFrame_repeatedPositionMsAndClosestSync_returnsTheSameFrame() throws Exception {
|
||||||
|
frameExtractor =
|
||||||
|
new ExperimentalFrameExtractor(
|
||||||
|
context,
|
||||||
|
new ExperimentalFrameExtractor.Configuration.Builder()
|
||||||
|
.setSeekParameters(CLOSEST_SYNC)
|
||||||
|
.build(),
|
||||||
|
MediaItem.fromUri(FILE_PATH),
|
||||||
|
/* effects= */ ImmutableList.of());
|
||||||
|
ImmutableList<Long> requestedFramePositionsMs = ImmutableList.of(0L, 0L, 33L, 34L, 34L);
|
||||||
|
ImmutableList<Long> expectedFramePositionsMs = ImmutableList.of(0L, 0L, 0L, 0L, 0L);
|
||||||
|
List<ListenableFuture<Frame>> frameFutures = new ArrayList<>();
|
||||||
|
|
||||||
|
for (long positionMs : requestedFramePositionsMs) {
|
||||||
|
frameFutures.add(frameExtractor.getFrame(positionMs));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < expectedFramePositionsMs.size(); i++) {
|
||||||
|
ListenableFuture<Frame> frameListenableFuture = frameFutures.get(i);
|
||||||
|
Frame frame = frameListenableFuture.get(TIMEOUT_SECONDS, SECONDS);
|
||||||
|
maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual_" + i, frame.bitmap, /* path= */ null);
|
||||||
|
Bitmap expectedBitmap =
|
||||||
|
readBitmap(
|
||||||
|
/* assetString= */ GOLDEN_ASSET_FOLDER_PATH
|
||||||
|
+ "sample_with_increasing_timestamps_360p_"
|
||||||
|
+ String.format(Locale.US, "%.3f", frame.presentationTimeMs / 1000f)
|
||||||
|
+ ".png");
|
||||||
|
|
||||||
|
assertBitmapsAreSimilar(expectedBitmap, frame.bitmap, PSNR_THRESHOLD);
|
||||||
|
assertThat(frame.presentationTimeMs).isEqualTo(expectedFramePositionsMs.get(i));
|
||||||
|
}
|
||||||
|
assertThat(
|
||||||
|
frameExtractor
|
||||||
|
.getDecoderCounters()
|
||||||
|
.get(TIMEOUT_SECONDS, SECONDS)
|
||||||
|
.renderedOutputBufferCount)
|
||||||
|
.isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void extractFrame_randomAccess_returnsCorrectFrames() throws Exception {
|
public void extractFrame_randomAccess_returnsCorrectFrames() throws Exception {
|
||||||
frameExtractor =
|
frameExtractor =
|
||||||
|
@ -24,7 +24,6 @@ import static androidx.media3.common.ColorInfo.SDR_BT709_LIMITED;
|
|||||||
import static androidx.media3.common.ColorInfo.isTransferHdr;
|
import static androidx.media3.common.ColorInfo.isTransferHdr;
|
||||||
import static androidx.media3.common.PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK;
|
import static androidx.media3.common.PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK;
|
||||||
import static androidx.media3.common.PlaybackException.ERROR_CODE_INVALID_STATE;
|
import static androidx.media3.common.PlaybackException.ERROR_CODE_INVALID_STATE;
|
||||||
import static androidx.media3.common.Player.DISCONTINUITY_REASON_SEEK;
|
|
||||||
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.GlUtil.createRgb10A2Texture;
|
import static androidx.media3.common.util.GlUtil.createRgb10A2Texture;
|
||||||
@ -85,6 +84,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import org.checkerframework.checker.initialization.qual.Initialized;
|
import org.checkerframework.checker.initialization.qual.Initialized;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
@ -211,6 +211,14 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
|
|||||||
private final ExoPlayer player;
|
private final ExoPlayer player;
|
||||||
private final Handler playerApplicationThreadHandler;
|
private final Handler playerApplicationThreadHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link AtomicBoolean} that indicates whether the frame being extracted requires decoding and
|
||||||
|
* rendering, or if the new seek position resolves to the last extracted frame. Accessed on both
|
||||||
|
* the {@linkplain ExoPlayer#getApplicationLooper() ExoPlayer application thread}, and the
|
||||||
|
* ExoPlayer playback thread.
|
||||||
|
*/
|
||||||
|
private final AtomicBoolean extractedFrameNeedsRendering;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link SettableFuture} representing the frame currently being extracted. Accessed on both the
|
* A {@link SettableFuture} representing the frame currently being extracted. Accessed on both the
|
||||||
* {@linkplain ExoPlayer#getApplicationLooper() ExoPlayer application thread}, and the video
|
* {@linkplain ExoPlayer#getApplicationLooper() ExoPlayer application thread}, and the video
|
||||||
@ -261,6 +269,7 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
|
|||||||
.setSeekParameters(configuration.seekParameters)
|
.setSeekParameters(configuration.seekParameters)
|
||||||
.build();
|
.build();
|
||||||
playerApplicationThreadHandler = new Handler(player.getApplicationLooper());
|
playerApplicationThreadHandler = new Handler(player.getApplicationLooper());
|
||||||
|
extractedFrameNeedsRendering = new AtomicBoolean();
|
||||||
lastRequestedFrameFuture = SettableFuture.create();
|
lastRequestedFrameFuture = SettableFuture.create();
|
||||||
// TODO: b/350498258 - Extracting the first frame is a workaround for ExoPlayer.setVideoEffects
|
// TODO: b/350498258 - Extracting the first frame is a workaround for ExoPlayer.setVideoEffects
|
||||||
// returning incorrect timestamps if we seek the player before rendering starts from zero.
|
// returning incorrect timestamps if we seek the player before rendering starts from zero.
|
||||||
@ -314,6 +323,7 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
|
|||||||
checkState(
|
checkState(
|
||||||
frameBeingExtractedFutureAtomicReference.compareAndSet(
|
frameBeingExtractedFutureAtomicReference.compareAndSet(
|
||||||
null, frameSettableFuture));
|
null, frameSettableFuture));
|
||||||
|
extractedFrameNeedsRendering.set(false);
|
||||||
player.seekTo(positionMs);
|
player.seekTo(positionMs);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -363,14 +373,14 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPositionDiscontinuity(
|
public void onPlaybackStateChanged(EventTime eventTime, @Player.State int state) {
|
||||||
EventTime eventTime,
|
// The player enters STATE_BUFFERING at the start of a seek.
|
||||||
Player.PositionInfo oldPosition,
|
// At the end of a seek, the player enters STATE_READY after the video renderer position has
|
||||||
Player.PositionInfo newPosition,
|
// been reset, and the renderer reports that it's ready.
|
||||||
@Player.DiscontinuityReason int reason) {
|
if (state == Player.STATE_READY && !extractedFrameNeedsRendering.get()) {
|
||||||
if (oldPosition.equals(newPosition) && reason == DISCONTINUITY_REASON_SEEK) {
|
// If the seek resolves to the current position, the renderer position will not be reset
|
||||||
// When the new seeking position resolves to the old position, no frames are rendered.
|
// and extractedFrameNeedsRendering remains false. No frames are rendered. Repeat the
|
||||||
// Repeat the previously returned frame.
|
// previously returned frame.
|
||||||
SettableFuture<Frame> frameBeingExtractedFuture =
|
SettableFuture<Frame> frameBeingExtractedFuture =
|
||||||
checkNotNull(frameBeingExtractedFutureAtomicReference.getAndSet(null));
|
checkNotNull(frameBeingExtractedFutureAtomicReference.getAndSet(null));
|
||||||
frameBeingExtractedFuture.set(checkNotNull(lastExtractedFrame));
|
frameBeingExtractedFuture.set(checkNotNull(lastExtractedFrame));
|
||||||
@ -557,10 +567,10 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** A custom MediaCodecVideoRenderer that renders only one frame per position reset. */
|
/** A custom MediaCodecVideoRenderer that renders only one frame per position reset. */
|
||||||
private static final class FrameExtractorRenderer extends MediaCodecVideoRenderer {
|
private final class FrameExtractorRenderer extends MediaCodecVideoRenderer {
|
||||||
private final boolean toneMapHdrToSdr;
|
private final boolean toneMapHdrToSdr;
|
||||||
|
|
||||||
private boolean frameRenderedSinceLastReset;
|
private boolean frameRenderedSinceLastPositionReset;
|
||||||
private List<Effect> effectsFromPlayer;
|
private List<Effect> effectsFromPlayer;
|
||||||
private @MonotonicNonNull Effect rotation;
|
private @MonotonicNonNull Effect rotation;
|
||||||
|
|
||||||
@ -626,9 +636,22 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
|
|||||||
super.setVideoEffects(effectBuilder.build());
|
super.setVideoEffects(effectBuilder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReady() {
|
||||||
|
// When using FrameReadingGlShaderProgram, frames will not be rendered to the output surface,
|
||||||
|
// and VideoFrameRenderControl.onFrameAvailableForRendering will not be called. The base class
|
||||||
|
// never becomes ready.
|
||||||
|
if (frameRenderedSinceLastPositionReset) {
|
||||||
|
// Treat this renderer as ready if a frame has been rendered into the effects pipeline.
|
||||||
|
// The renderer needs to become ready for ExoPlayer to enter STATE_READY.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.isReady();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
||||||
if (!frameRenderedSinceLastReset) {
|
if (!frameRenderedSinceLastPositionReset) {
|
||||||
super.render(positionUs, elapsedRealtimeUs);
|
super.render(positionUs, elapsedRealtimeUs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -647,7 +670,7 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
|
|||||||
boolean isLastBuffer,
|
boolean isLastBuffer,
|
||||||
Format format)
|
Format format)
|
||||||
throws ExoPlaybackException {
|
throws ExoPlaybackException {
|
||||||
if (frameRenderedSinceLastReset) {
|
if (frameRenderedSinceLastPositionReset) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return super.processOutputBuffer(
|
return super.processOutputBuffer(
|
||||||
@ -667,17 +690,18 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
|
|||||||
@Override
|
@Override
|
||||||
protected void renderOutputBufferV21(
|
protected void renderOutputBufferV21(
|
||||||
MediaCodecAdapter codec, int index, long presentationTimeUs, long releaseTimeNs) {
|
MediaCodecAdapter codec, int index, long presentationTimeUs, long releaseTimeNs) {
|
||||||
if (frameRenderedSinceLastReset) {
|
if (frameRenderedSinceLastPositionReset) {
|
||||||
// Do not skip this buffer to prevent the decoder from making more progress.
|
// Do not skip this buffer to prevent the decoder from making more progress.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
frameRenderedSinceLastReset = true;
|
frameRenderedSinceLastPositionReset = true;
|
||||||
super.renderOutputBufferV21(codec, index, presentationTimeUs, releaseTimeNs);
|
super.renderOutputBufferV21(codec, index, presentationTimeUs, releaseTimeNs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
|
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
|
||||||
frameRenderedSinceLastReset = false;
|
frameRenderedSinceLastPositionReset = false;
|
||||||
|
extractedFrameNeedsRendering.set(true);
|
||||||
super.onPositionReset(positionUs, joining);
|
super.onPositionReset(positionUs, joining);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user