mirror of
https://github.com/androidx/media.git
synced 2025-04-29 22:36:54 +08:00
Compare commits
4 Commits
83efd8eb66
...
c8a3361cc8
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c8a3361cc8 | ||
![]() |
9182b413dc | ||
![]() |
e11a8a1b19 | ||
![]() |
2f1fc4773c |
@ -80,6 +80,9 @@
|
||||
* Cronet extension:
|
||||
* RTMP extension:
|
||||
* HLS extension:
|
||||
* Fix issue where chunk duration wasn't set in `CmcdData` for HLS media,
|
||||
causing an assertion failure when processing encrypted media segments
|
||||
([#2312](https://github.com/androidx/media/issues/2312)).
|
||||
* DASH extension:
|
||||
* Smooth Streaming extension:
|
||||
* RTSP extension:
|
||||
|
@ -56,6 +56,8 @@ public class VideoDecoderOutputBuffer extends DecoderOutputBuffer {
|
||||
|
||||
@Nullable public int[] yuvStrides;
|
||||
public int colorspace;
|
||||
public int yStride;
|
||||
public int uvStride;
|
||||
|
||||
/**
|
||||
* Supplemental data related to the output frame, if {@link #hasSupplementalData()} returns true.
|
||||
@ -117,6 +119,8 @@ public class VideoDecoderOutputBuffer extends DecoderOutputBuffer {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.colorspace = colorspace;
|
||||
this.yStride = yStride;
|
||||
this.uvStride = uvStride;
|
||||
int uvHeight = (int) (((long) height + 1) / 2);
|
||||
if (!isSafeToMultiply(yStride, height) || !isSafeToMultiply(uvStride, uvHeight)) {
|
||||
return false;
|
||||
|
@ -66,6 +66,17 @@ import androidx.media3.common.VideoFrameProcessingException;
|
||||
super.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void signalEndOfCurrentInputStream() {
|
||||
// TODO: b/391109625 - Support mixed size buffers in the output texture pool to allow
|
||||
// replaying the last frame in a sequence.
|
||||
for (int i = 0; i < cacheSize; i++) {
|
||||
super.releaseOutputFrame(cachedFrames[i].glTextureInfo);
|
||||
}
|
||||
cacheSize = 0;
|
||||
super.signalEndOfCurrentInputStream();
|
||||
}
|
||||
|
||||
/** Returns whether there is no cached frame. */
|
||||
public boolean isEmpty() {
|
||||
return cacheSize == 0;
|
||||
|
@ -731,8 +731,15 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
|
||||
@Override
|
||||
public void redraw() {
|
||||
checkState(isInitialized());
|
||||
// Resignal EOS only for the last item.
|
||||
boolean needsResignalEndOfCurrentInputStream = signaledEndOfStream;
|
||||
long replayedPresentationTimeUs = lastOutputBufferPresentationTimeUs;
|
||||
PlaybackVideoGraphWrapper.this.flush(/* resetPosition= */ false);
|
||||
checkNotNull(videoGraph).redraw();
|
||||
lastOutputBufferPresentationTimeUs = replayedPresentationTimeUs;
|
||||
if (needsResignalEndOfCurrentInputStream) {
|
||||
signalEndOfCurrentInputStream();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -209,6 +209,13 @@ import androidx.media3.exoplayer.ExoPlaybackException;
|
||||
* this method, the end of input signal is ignored.
|
||||
*/
|
||||
public void signalEndOfInput() {
|
||||
if (latestInputPresentationTimeUs == C.TIME_UNSET) {
|
||||
// If EOS is signalled right after a flush without receiving a frame (could happen with frame
|
||||
// replaying as available frame is not reported to the render control), set the latest input
|
||||
// and output timestamp to end of source to ensure isEnded() returns true.
|
||||
latestInputPresentationTimeUs = C.TIME_END_OF_SOURCE;
|
||||
latestOutputPresentationTimeUs = C.TIME_END_OF_SOURCE;
|
||||
}
|
||||
lastPresentationTimeUs = latestInputPresentationTimeUs;
|
||||
}
|
||||
|
||||
|
@ -517,7 +517,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
.setPlaybackRate(loadingInfo.playbackSpeed)
|
||||
.setIsLive(!playlist.hasEndTag)
|
||||
.setDidRebuffer(loadingInfo.rebufferedSince(lastChunkRequestRealtimeMs))
|
||||
.setIsBufferEmpty(queue.isEmpty());
|
||||
.setIsBufferEmpty(queue.isEmpty())
|
||||
.setChunkDurationUs(segmentBaseHolder.segmentBase.durationUs);
|
||||
long nextMediaSequence =
|
||||
segmentBaseHolder.partIndex == C.INDEX_UNSET
|
||||
? segmentBaseHolder.mediaSequence + 1
|
||||
|
@ -112,8 +112,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
.setFlags(segmentBaseHolder.isPreload ? FLAG_MIGHT_NOT_USE_FULL_NETWORK_SPEED : 0)
|
||||
.build();
|
||||
if (cmcdDataFactory != null) {
|
||||
CmcdData cmcdData =
|
||||
cmcdDataFactory.setChunkDurationUs(mediaSegment.durationUs).createCmcdData();
|
||||
CmcdData cmcdData = cmcdDataFactory.createCmcdData();
|
||||
dataSpec = cmcdData.addToDataSpec(dataSpec);
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ package androidx.media3.exoplayer.hls;
|
||||
|
||||
import static androidx.media3.common.Player.DISCONTINUITY_REASON_AUTO_TRANSITION;
|
||||
import static androidx.media3.common.Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED;
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
@ -288,7 +289,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
|
||||
AdPlaybackState actual =
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 1);
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 1,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE);
|
||||
AdPlaybackState expected =
|
||||
new AdPlaybackState("adsId", 0L, 15_000_000L, C.TIME_END_OF_SOURCE)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, C.TIME_UNSET)
|
||||
@ -360,7 +365,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 3))
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 3,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE))
|
||||
.isEqualTo(
|
||||
new AdPlaybackState("adsId", 0L)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET)
|
||||
@ -424,7 +433,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0))
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE))
|
||||
.isEqualTo(
|
||||
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 4_000_000L)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, C.TIME_UNSET, C.TIME_UNSET, C.TIME_UNSET)
|
||||
@ -494,7 +507,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0))
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE))
|
||||
.isEqualTo(
|
||||
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ C.TIME_END_OF_SOURCE)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, 1_000_000L, 1_100_000L, 1_200_000L)
|
||||
@ -562,7 +579,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0))
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE))
|
||||
.isEqualTo(
|
||||
new AdPlaybackState(
|
||||
"adsId", /* adGroupTimesUs...= */ 1_000_000L, 2_000_000L, C.TIME_END_OF_SOURCE)
|
||||
@ -632,7 +653,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0))
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE))
|
||||
.isEqualTo(
|
||||
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 0L)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, 1_000_000L, 1_000_000L)
|
||||
@ -686,7 +711,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0))
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE))
|
||||
.isEqualTo(
|
||||
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 0L)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, 1_000_000L, C.TIME_UNSET)
|
||||
@ -734,7 +763,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0))
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE))
|
||||
.isEqualTo(
|
||||
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 1_123_000L)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, 4_000_000L)
|
||||
@ -773,7 +806,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0))
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE))
|
||||
.isEqualTo(
|
||||
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 1_123_000L)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, 3_456_000L)
|
||||
@ -810,7 +847,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0))
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE))
|
||||
.isEqualTo(
|
||||
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 1_123_000L)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, 1_123_000L)
|
||||
@ -847,7 +888,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0))
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE))
|
||||
.isEqualTo(
|
||||
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 1_123_000L)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, 2_234_000L)
|
||||
@ -882,7 +927,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0))
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE))
|
||||
.isEqualTo(
|
||||
new AdPlaybackState("adsId", /* adGroupTimesUs...= */ 1_123_000L)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, C.TIME_UNSET)
|
||||
@ -1368,7 +1417,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
.withAdId(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, "ad0-0");
|
||||
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0);
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE);
|
||||
|
||||
runMainLooperUntil(assetListLoadingListener::completed, TIMEOUT_MS, Clock.DEFAULT);
|
||||
verify(mockAdsLoaderListener)
|
||||
@ -1441,7 +1494,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
when(mockPlayer.createMessage(any())).thenReturn(midRollPlayerMessage);
|
||||
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0);
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE);
|
||||
|
||||
runMainLooperUntil(assetListLoadingListener::completed, TIMEOUT_MS, Clock.DEFAULT);
|
||||
verify(mockAdsLoaderListener)
|
||||
@ -1483,7 +1540,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
|
||||
assertThat(
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0))
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE))
|
||||
.isEqualTo(expectedAdPlaybackStateAtTimelineChange);
|
||||
|
||||
runMainLooperUntil(assetListLoadingListener::completed, TIMEOUT_MS, Clock.DEFAULT);
|
||||
@ -1569,7 +1630,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
.withAdId(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, "ad1-0");
|
||||
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 1);
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 1,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE);
|
||||
|
||||
runMainLooperUntil(assetListLoadingListener::failed, TIMEOUT_MS, Clock.DEFAULT);
|
||||
ArgumentCaptor<AdPlaybackState> adPlaybackStateCaptor =
|
||||
@ -1640,7 +1705,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
when(mockPlayer.getContentPosition()).thenReturn(21_000L);
|
||||
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 1);
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 1,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE);
|
||||
|
||||
runMainLooperUntil(assetListLoadingListener::failed, TIMEOUT_MS, Clock.DEFAULT);
|
||||
verify(mockAdsLoaderListener)
|
||||
@ -1696,7 +1765,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
.build();
|
||||
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0);
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE);
|
||||
|
||||
runMainLooperUntil(assetListLoadingListener::completed, TIMEOUT_MS, Clock.DEFAULT);
|
||||
verify(mockAdsLoaderListener)
|
||||
@ -1766,7 +1839,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
.build());
|
||||
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0);
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE);
|
||||
|
||||
runMainLooperUntil(assetListLoadingListener::completed, TIMEOUT_MS, Clock.DEFAULT);
|
||||
ArgumentCaptor<AdPlaybackState> adPlaybackStateCaptor =
|
||||
@ -1833,7 +1910,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
/* Looper ignored */ null);
|
||||
when(mockPlayer.createMessage(any())).thenReturn(playerMessage);
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0);
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE);
|
||||
ArgumentCaptor<Player.Listener> listener = ArgumentCaptor.forClass(Player.Listener.class);
|
||||
InOrder inOrder = inOrder(mockPlayer);
|
||||
inOrder.verify(mockPlayer).addListener(listener.capture());
|
||||
@ -1887,7 +1968,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
/* Clock ignored */ null,
|
||||
/* Looper ignored */ null));
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 2);
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 2,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE);
|
||||
when(mockPlayer.getCurrentPeriodIndex()).thenReturn(2);
|
||||
when(mockPlayer.getCurrentMediaItemIndex()).thenReturn(2);
|
||||
InOrder inOrder = inOrder(mockPlayer);
|
||||
@ -1986,7 +2071,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
when(mockPlayer.getContentPosition()).thenReturn(0L);
|
||||
when(mockPlayer.createMessage(any())).thenReturn(midRoll1PlayerMessage);
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 1);
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 1,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE);
|
||||
ArgumentCaptor<Player.Listener> listener = ArgumentCaptor.forClass(Player.Listener.class);
|
||||
InOrder inOrder = inOrder(mockPlayer);
|
||||
inOrder.verify(mockPlayer).addListener(listener.capture());
|
||||
@ -2138,7 +2227,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
/* Looper ignored */ null);
|
||||
when(mockPlayer.createMessage(any())).thenReturn(midRoll1PlayerMessage);
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0);
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE);
|
||||
// Emulate position discontinuity.
|
||||
ArgumentCaptor<Player.Listener> listener = ArgumentCaptor.forClass(Player.Listener.class);
|
||||
verify(mockPlayer).addListener(listener.capture());
|
||||
@ -2232,7 +2325,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
/* Looper ignored */ null);
|
||||
when(mockPlayer.createMessage(any())).thenReturn(playerMessage);
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 1);
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 1,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE);
|
||||
// Emulate position discontinuity to a non-ad media item.
|
||||
MediaItem nonAdMediaItem = MediaItem.fromUri(Uri.parse("http://example.com/no-ad"));
|
||||
ArgumentCaptor<Player.Listener> listener = ArgumentCaptor.forClass(Player.Listener.class);
|
||||
@ -2294,7 +2391,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-1.m3u8\""
|
||||
+ "\n";
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0);
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE);
|
||||
ArgumentCaptor<Player.Listener> listener = ArgumentCaptor.forClass(Player.Listener.class);
|
||||
verify(mockPlayer).addListener(listener.capture());
|
||||
Object windowUid = new Object();
|
||||
@ -2405,7 +2506,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
|
||||
+ "\n";
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0);
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE);
|
||||
reset(mockEventListener);
|
||||
ArgumentCaptor<Player.Listener> listener = ArgumentCaptor.forClass(Player.Listener.class);
|
||||
when(mockPlayer.isPlayingAd()).thenReturn(true);
|
||||
@ -2461,7 +2566,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-1.m3u8\""
|
||||
+ "\n";
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0);
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE);
|
||||
|
||||
adsLoader.handlePrepareError(adsMediaSource, 0, 1, new IOException());
|
||||
adsLoader.handlePrepareError(adsMediaSource, 0, 0, new IOException());
|
||||
@ -2676,7 +2785,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
+ "X-ASSET-URI=\"http://example.com/media-0-0.m3u8\""
|
||||
+ "\n";
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0);
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE);
|
||||
reset(mockPlayer);
|
||||
reset(mockEventListener);
|
||||
reset(mockAdsLoaderListener);
|
||||
@ -2802,7 +2915,11 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
ArgumentCaptor<Player.Listener> listener = ArgumentCaptor.forClass(Player.Listener.class);
|
||||
|
||||
callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
playlistString, adsLoader, /* windowIndex= */ 0);
|
||||
playlistString,
|
||||
adsLoader,
|
||||
/* windowIndex= */ 0,
|
||||
/* windowPositionInPeriodUs= */ 0,
|
||||
/* windowEndPositionInPeriodUs= */ C.TIME_END_OF_SOURCE);
|
||||
adsLoader.handlePrepareError(
|
||||
adsMediaSource, /* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, exception);
|
||||
adsLoader.handlePrepareComplete(
|
||||
@ -2917,8 +3034,16 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
}
|
||||
|
||||
private AdPlaybackState callHandleContentTimelineChangedAndCaptureAdPlaybackState(
|
||||
String playlistString, HlsInterstitialsAdsLoader adsLoader, int windowIndex)
|
||||
String playlistString,
|
||||
HlsInterstitialsAdsLoader adsLoader,
|
||||
int windowIndex,
|
||||
long windowPositionInPeriodUs,
|
||||
long windowEndPositionInPeriodUs)
|
||||
throws IOException {
|
||||
checkArgument(
|
||||
windowPositionInPeriodUs >= 0
|
||||
&& (windowEndPositionInPeriodUs == C.TIME_END_OF_SOURCE
|
||||
|| windowPositionInPeriodUs < windowEndPositionInPeriodUs));
|
||||
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
|
||||
HlsMediaPlaylist contentMediaPlaylist =
|
||||
(HlsMediaPlaylist) new HlsPlaylistParser().parse(Uri.EMPTY, inputStream);
|
||||
@ -2928,7 +3053,17 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
new TimelineWindowDefinition.Builder()
|
||||
.setMediaItem(MediaItem.fromUri("http://example.com/"))
|
||||
.build());
|
||||
initialWindows[windowIndex] = contentWindowDefinition;
|
||||
long durationUs =
|
||||
(windowEndPositionInPeriodUs != C.TIME_END_OF_SOURCE
|
||||
? windowEndPositionInPeriodUs
|
||||
: contentMediaPlaylist.durationUs)
|
||||
- windowPositionInPeriodUs;
|
||||
initialWindows[windowIndex] =
|
||||
contentWindowDefinition
|
||||
.buildUpon()
|
||||
.setDurationUs(durationUs)
|
||||
.setWindowPositionInFirstPeriodUs(windowPositionInPeriodUs)
|
||||
.build();
|
||||
when(mockPlayer.getCurrentTimeline()).thenReturn(new FakeTimeline(initialWindows));
|
||||
when(mockPlayer.getCurrentMediaItem()).thenReturn(contentWindowDefinition.mediaItem);
|
||||
// Set the player.
|
||||
@ -2940,7 +3075,7 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
HlsManifest hlsManifest =
|
||||
new HlsManifest(/* multivariantPlaylist= */ null, contentMediaPlaylist);
|
||||
adsLoader.handleContentTimelineChanged(
|
||||
adsMediaSource, new FakeTimeline(new Object[] {hlsManifest}, contentWindowDefinition));
|
||||
adsMediaSource, new FakeTimeline(new Object[] {hlsManifest}, initialWindows[windowIndex]));
|
||||
|
||||
ArgumentCaptor<AdPlaybackState> adPlaybackState =
|
||||
ArgumentCaptor.forClass(AdPlaybackState.class);
|
||||
@ -2955,8 +3090,9 @@ public class HlsInterstitialsAdsLoaderTest {
|
||||
windowsAfterTimelineChange[windowIndex] =
|
||||
contentWindowDefinition
|
||||
.buildUpon()
|
||||
.setDurationUs(contentMediaPlaylist.durationUs)
|
||||
.setDurationUs(durationUs)
|
||||
.setWindowStartTimeUs(contentMediaPlaylist.startTimeUs)
|
||||
.setWindowPositionInFirstPeriodUs(windowPositionInPeriodUs)
|
||||
.setAdPlaybackStates(ImmutableList.of(adPlaybackState.getValue()))
|
||||
.build();
|
||||
when(mockPlayer.getCurrentTimeline()).thenReturn(new FakeTimeline(windowsAfterTimelineChange));
|
||||
|
@ -69,6 +69,9 @@ import androidx.media3.effect.ScaleAndRotateTransformation;
|
||||
import androidx.media3.effect.SingleInputVideoGraph;
|
||||
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
|
||||
import androidx.media3.exoplayer.mediacodec.MediaCodecUtil;
|
||||
import androidx.media3.exoplayer.video.MediaCodecVideoRenderer;
|
||||
import androidx.media3.exoplayer.video.PlaybackVideoGraphWrapper;
|
||||
import androidx.media3.exoplayer.video.VideoFrameReleaseControl;
|
||||
import androidx.media3.extractor.ExtractorOutput;
|
||||
import androidx.media3.muxer.MuxerException;
|
||||
import androidx.media3.test.utils.BitmapPixelTestUtil;
|
||||
@ -98,6 +101,24 @@ import org.junit.AssumptionViolatedException;
|
||||
|
||||
/** Utilities for instrumentation tests. */
|
||||
public final class AndroidTestUtil {
|
||||
|
||||
/** A {@link MediaCodecVideoRenderer} subclass that supports replaying a frame. */
|
||||
public static class ReplayVideoRenderer extends MediaCodecVideoRenderer {
|
||||
|
||||
public ReplayVideoRenderer(Context context) {
|
||||
super(new Builder(context).setMediaCodecSelector(MediaCodecSelector.DEFAULT));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PlaybackVideoGraphWrapper createPlaybackVideoGraphWrapper(
|
||||
Context context, VideoFrameReleaseControl videoFrameReleaseControl) {
|
||||
return new PlaybackVideoGraphWrapper.Builder(context, videoFrameReleaseControl)
|
||||
.setClock(getClock())
|
||||
.setEnableReplayableCache(true)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private static final String TAG = "AndroidTestUtil";
|
||||
|
||||
/** An {@link Effects} instance that forces video transcoding. */
|
||||
|
@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright 2025 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.transformer.mh;
|
||||
|
||||
import static androidx.media3.common.util.Util.isRunningOnEmulator;
|
||||
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET;
|
||||
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS;
|
||||
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
import android.media.MediaFormat;
|
||||
import android.os.Handler;
|
||||
import android.view.SurfaceView;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.PlaybackException;
|
||||
import androidx.media3.common.VideoFrameProcessor;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.effect.Contrast;
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.exoplayer.Renderer;
|
||||
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
|
||||
import androidx.media3.exoplayer.util.EventLogger;
|
||||
import androidx.media3.exoplayer.video.VideoFrameMetadataListener;
|
||||
import androidx.media3.exoplayer.video.VideoRendererEventListener;
|
||||
import androidx.media3.transformer.AndroidTestUtil.ReplayVideoRenderer;
|
||||
import androidx.media3.transformer.PlayerTestListener;
|
||||
import androidx.media3.transformer.SurfaceTestActivity;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
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.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Instrumentation tests for frame replaying (dynamic effect update). */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ReplayCacheTest {
|
||||
private static final long TEST_TIMEOUT_MS = isRunningOnEmulator() ? 20_000 : 10_000;
|
||||
|
||||
private static final MediaItem VIDEO_MEDIA_ITEM_1 = MediaItem.fromUri(MP4_ASSET.uri);
|
||||
private static final MediaItem VIDEO_MEDIA_ITEM_2 =
|
||||
MediaItem.fromUri(MP4_ASSET_WITH_INCREASING_TIMESTAMPS.uri);
|
||||
|
||||
@Rule
|
||||
public ActivityScenarioRule<SurfaceTestActivity> rule =
|
||||
new ActivityScenarioRule<>(SurfaceTestActivity.class);
|
||||
|
||||
private final Context context = getInstrumentation().getContext().getApplicationContext();
|
||||
private final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||
|
||||
private ExoPlayer exoPlayer;
|
||||
private SurfaceView surfaceView;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
rule.getScenario().onActivity(activity -> surfaceView = activity.getSurfaceView());
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
rule.getScenario().close();
|
||||
getInstrumentation()
|
||||
.runOnMainSync(
|
||||
() -> {
|
||||
if (exoPlayer != null) {
|
||||
exoPlayer.release();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void replayOnEveryFrame_withExoPlayer_succeeds()
|
||||
throws PlaybackException, TimeoutException {
|
||||
assumeTrue(
|
||||
"The MediaCodec decoder's output surface is sometimes dropping frames on emulator despite"
|
||||
+ " using MediaFormat.KEY_ALLOW_FRAME_DROP.",
|
||||
!Util.isRunningOnEmulator());
|
||||
PlayerTestListener playerTestListener = new PlayerTestListener(TEST_TIMEOUT_MS);
|
||||
List<Long> playedFrameTimestampsUs = new ArrayList<>();
|
||||
|
||||
instrumentation.runOnMainSync(
|
||||
() -> {
|
||||
Renderer videoRenderer = new ReplayVideoRenderer(context);
|
||||
exoPlayer =
|
||||
new ExoPlayer.Builder(context)
|
||||
.setRenderersFactory(
|
||||
new DefaultRenderersFactory(context) {
|
||||
@Override
|
||||
protected void buildVideoRenderers(
|
||||
Context context,
|
||||
@ExtensionRendererMode int extensionRendererMode,
|
||||
MediaCodecSelector mediaCodecSelector,
|
||||
boolean enableDecoderFallback,
|
||||
Handler eventHandler,
|
||||
VideoRendererEventListener eventListener,
|
||||
long allowedVideoJoiningTimeMs,
|
||||
ArrayList<Renderer> builtVideoRenderers) {
|
||||
builtVideoRenderers.add(videoRenderer);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
exoPlayer.setVideoSurfaceView(surfaceView);
|
||||
// Adding an EventLogger to use its log output in case the test fails.
|
||||
exoPlayer.addAnalyticsListener(new EventLogger());
|
||||
exoPlayer.addListener(playerTestListener);
|
||||
exoPlayer.setVideoEffects(ImmutableList.of(new Contrast(0.5f)));
|
||||
exoPlayer.setVideoFrameMetadataListener(
|
||||
new VideoFrameMetadataListener() {
|
||||
private final List<Long> replayedFrames = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void onVideoFrameAboutToBeRendered(
|
||||
long presentationTimeUs,
|
||||
long releaseTimeNs,
|
||||
Format format,
|
||||
@Nullable MediaFormat mediaFormat) {
|
||||
playedFrameTimestampsUs.add(presentationTimeUs);
|
||||
if (replayedFrames.contains(presentationTimeUs)) {
|
||||
return;
|
||||
}
|
||||
replayedFrames.add(presentationTimeUs);
|
||||
instrumentation.runOnMainSync(
|
||||
() -> exoPlayer.setVideoEffects(VideoFrameProcessor.REDRAW));
|
||||
}
|
||||
});
|
||||
exoPlayer.setMediaItems(ImmutableList.of(VIDEO_MEDIA_ITEM_1, VIDEO_MEDIA_ITEM_2));
|
||||
exoPlayer.prepare();
|
||||
exoPlayer.play();
|
||||
});
|
||||
|
||||
playerTestListener.waitUntilPlayerEnded();
|
||||
// VIDEO_1 has size 30, VIDEO_2 has size 30, every frame is replayed once, minus one frame that
|
||||
// we don't currently replay (the frame at media item transition).
|
||||
assertThat(playedFrameTimestampsUs).hasSize(119);
|
||||
}
|
||||
}
|
@ -56,10 +56,9 @@ import androidx.media3.exoplayer.Renderer;
|
||||
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
|
||||
import androidx.media3.exoplayer.util.EventLogger;
|
||||
import androidx.media3.exoplayer.video.MediaCodecVideoRenderer;
|
||||
import androidx.media3.exoplayer.video.PlaybackVideoGraphWrapper;
|
||||
import androidx.media3.exoplayer.video.VideoFrameReleaseControl;
|
||||
import androidx.media3.exoplayer.video.VideoRendererEventListener;
|
||||
import androidx.media3.test.utils.BitmapPixelTestUtil;
|
||||
import androidx.media3.transformer.AndroidTestUtil.ReplayVideoRenderer;
|
||||
import androidx.media3.transformer.SurfaceTestActivity;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
@ -294,7 +293,7 @@ public class EffectPlaybackPixelTest {
|
||||
instrumentation.runOnMainSync(
|
||||
() -> {
|
||||
Context context = ApplicationProvider.getApplicationContext();
|
||||
Renderer videoRenderer = new ReplayVideoRenderer(context, MediaCodecSelector.DEFAULT);
|
||||
Renderer videoRenderer = new ReplayVideoRenderer(context);
|
||||
player =
|
||||
new ExoPlayer.Builder(context)
|
||||
.setRenderersFactory(
|
||||
@ -573,22 +572,6 @@ public class EffectPlaybackPixelTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static class ReplayVideoRenderer extends MediaCodecVideoRenderer {
|
||||
|
||||
public ReplayVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector) {
|
||||
super(new Builder(context).setMediaCodecSelector(mediaCodecSelector));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PlaybackVideoGraphWrapper createPlaybackVideoGraphWrapper(
|
||||
Context context, VideoFrameReleaseControl videoFrameReleaseControl) {
|
||||
return new PlaybackVideoGraphWrapper.Builder(context, videoFrameReleaseControl)
|
||||
.setClock(getClock())
|
||||
.setEnableReplayableCache(true)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private static class NoFrameDroppedVideoRenderer extends MediaCodecVideoRenderer {
|
||||
|
||||
public NoFrameDroppedVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user