Compare commits

...

4 Commits

Author SHA1 Message Date
Googler
c8a3361cc8 internal change
PiperOrigin-RevId: 745226864
2025-04-08 11:36:50 -07:00
rohks
9182b413dc Ensure chunk duration is set in CmcdData for HLS media
Previously, encrypted media segments did not have the chunk duration set,
causing an assertion failure during `CmcdData` creation. With this change,
the chunk duration is always set, while `CmcdData` ensures it is applied
only for media chunks.

Issue: androidx/media#2312
PiperOrigin-RevId: 745196718
2025-04-08 10:21:36 -07:00
claincly
e11a8a1b19 Fix playback not transition into next mediaItem
This is because the replay cache needs to clear the cache after one mediaItem is fully played.

That is, if the last two frames are cached, we need to wait until they are both rendered before
receiving inputs from the next input stream because the texture size might change. And when the
texture size changes, we teardown all previous used textures, and this causes a state confusion
in the shader program in that, the cache thinks one texture id is in-use (because it's not released)
but the baseGlShaderProgram texturePool thinks it's already free (as a result of size change)

Also fixes an issue that, if replaying a frame after EOS is signalled, the EOS
signal is lost because we flush the pipeline.

PiperOrigin-RevId: 745191032
2025-04-08 10:08:34 -07:00
bachinger
2f1fc4773c Allow testing of clipped VOD windows by adding windowPositionInPeriodUs
This is a no-op change. Preparing for follow up CLs to test
clipped VOD windows.

PiperOrigin-RevId: 745172140
2025-04-08 09:16:41 -07:00
11 changed files with 389 additions and 55 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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. */

View File

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

View File

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