diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 582527a796..872ab9349e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -101,6 +101,9 @@ * Cronet Extension: * RTMP Extension: * HLS Extension: +* DASH Extension: + * Add support for periods starting in the middle of a segment + ([#1440](https://github.com/androidx/media/issues/1440)). * Smooth Streaming Extension: * RTSP Extension: * Decoder Extensions (FFmpeg, VP9, AV1, etc.): diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/chunk/ChunkSampleStream.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/chunk/ChunkSampleStream.java index 24b6bd9ff7..a98750f4f6 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/chunk/ChunkSampleStream.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/chunk/ChunkSampleStream.java @@ -22,6 +22,7 @@ import static java.lang.Math.min; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; @@ -95,6 +96,8 @@ public class ChunkSampleStream private long lastSeekPositionUs; private int nextNotifyPrimaryFormatMediaChunkIndex; @Nullable private BaseMediaChunk canceledMediaChunk; + private boolean canReportInitialDiscontinuity; + private boolean hasInitialDiscontinuity; /* package */ boolean loadingFinished; @@ -114,6 +117,8 @@ public class ChunkSampleStream * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}. * @param mediaSourceEventDispatcher A dispatcher to notify of {@link MediaSourceEventListener} * events. + * @param canReportInitialDiscontinuity Whether the stream can report an initial discontinuity if + * the first chunk can't start at the beginning and needs to preroll data. */ public ChunkSampleStream( @C.TrackType int primaryTrackType, @@ -126,7 +131,8 @@ public class ChunkSampleStream DrmSessionManager drmSessionManager, DrmSessionEventListener.EventDispatcher drmEventDispatcher, LoadErrorHandlingPolicy loadErrorHandlingPolicy, - MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher) { + MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher, + boolean canReportInitialDiscontinuity) { this.primaryTrackType = primaryTrackType; this.embeddedTrackTypes = embeddedTrackTypes == null ? new int[0] : embeddedTrackTypes; this.embeddedTrackFormats = embeddedTrackFormats == null ? new Format[0] : embeddedTrackFormats; @@ -134,6 +140,7 @@ public class ChunkSampleStream this.callback = callback; this.mediaSourceEventDispatcher = mediaSourceEventDispatcher; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + this.canReportInitialDiscontinuity = canReportInitialDiscontinuity; loader = new Loader("ChunkSampleStream"); nextChunkHolder = new ChunkHolder(); mediaChunks = new ArrayList<>(); @@ -258,6 +265,7 @@ public class ChunkSampleStream */ public void seekToUs(long positionUs) { lastSeekPositionUs = positionUs; + canReportInitialDiscontinuity = false; if (isPendingReset()) { // A reset is already pending. We only need to update its position. pendingResetPositionUs = positionUs; @@ -600,12 +608,22 @@ public class ChunkSampleStream // seeking to a chunk boundary then we want the queue to pass through all of the samples in // the chunk. Doing this ensures we'll always output the keyframe at the start of the chunk, // even if its timestamp is slightly earlier than the advertised chunk start time. - if (mediaChunk.startTimeUs != pendingResetPositionUs) { + if (mediaChunk.startTimeUs < pendingResetPositionUs) { primarySampleQueue.setStartTimeUs(pendingResetPositionUs); for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { embeddedSampleQueue.setStartTimeUs(pendingResetPositionUs); } + if (canReportInitialDiscontinuity) { + // Only report it as discontinuity if the SampleQueue can't skip the samples directly. + boolean sampleQueueCanSkipSamples = + MimeTypes.allSamplesAreSyncSamples( + mediaChunk.trackFormat.sampleMimeType, mediaChunk.trackFormat.codecs); + hasInitialDiscontinuity = !sampleQueueCanSkipSamples; + } } + // Once we started loading the first media chunk, no more initial discontinuities can be + // reported. + canReportInitialDiscontinuity = false; pendingResetPositionUs = C.TIME_UNSET; } mediaChunk.init(chunkOutput); @@ -670,6 +688,19 @@ public class ChunkSampleStream } } + /** + * Consumes a pending initial discontinuity. + * + * @return Whether the stream had an initial discontinuity. + */ + public boolean consumeInitialDiscontinuity() { + try { + return hasInitialDiscontinuity; + } finally { + hasInitialDiscontinuity = false; + } + } + private void discardUpstream(int preferredQueueSize) { Assertions.checkState(!loader.isLoading()); diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaPeriod.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaPeriod.java index 5a3e4fb5ca..37676922c0 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaPeriod.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaPeriod.java @@ -114,6 +114,8 @@ import java.util.regex.Pattern; private DashManifest manifest; private int periodIndex; private List eventStreams; + private boolean canReportInitialDiscontinuity; + private long initialStartTimeUs; public DashMediaPeriod( int id, @@ -149,6 +151,7 @@ import java.util.regex.Pattern; this.allocator = allocator; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.playerId = playerId; + this.canReportInitialDiscontinuity = true; playerEmsgHandler = new PlayerEmsgHandler(manifest, playerEmsgCallback, allocator); sampleStreams = newSampleStreamArray(0); eventSampleStreams = new EventSampleStream[0]; @@ -305,6 +308,10 @@ import java.util.regex.Pattern; compositeSequenceableLoaderFactory.create( sampleStreamList, Lists.transform(sampleStreamList, s -> ImmutableList.of(s.primaryTrackType))); + if (canReportInitialDiscontinuity) { + canReportInitialDiscontinuity = false; + initialStartTimeUs = positionUs; + } return positionUs; } @@ -337,6 +344,11 @@ import java.util.regex.Pattern; @Override public long readDiscontinuity() { + for (ChunkSampleStream sampleStream : sampleStreams) { + if (sampleStream.consumeInitialDiscontinuity()) { + return initialStartTimeUs; + } + } return C.TIME_UNSET; } @@ -824,7 +836,8 @@ import java.util.regex.Pattern; drmSessionManager, drmEventDispatcher, loadErrorHandlingPolicy, - mediaSourceEventDispatcher); + mediaSourceEventDispatcher, + canReportInitialDiscontinuity); synchronized (this) { // The map is also accessed on the loading thread so synchronize access. trackEmsgHandlerBySampleStream.put(stream, trackPlayerEmsgHandler); diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/e2etest/DashPlaybackTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/e2etest/DashPlaybackTest.java index f34d6aa896..6e3ba63b87 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/e2etest/DashPlaybackTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/e2etest/DashPlaybackTest.java @@ -440,4 +440,27 @@ public final class DashPlaybackTest { // TODO: b/352276461 - The last frame might not be rendered. When the bug is fixed, // assert on the full playback dump. } + + @Test + public void multiPeriod_withOffsetInSegment() throws Exception { + Context applicationContext = ApplicationProvider.getApplicationContext(); + CapturingRenderersFactory capturingRenderersFactory = + new CapturingRenderersFactory(applicationContext); + ExoPlayer player = + new ExoPlayer.Builder(applicationContext, capturingRenderersFactory) + .setClock(new FakeClock(/* isAutoAdvancing= */ true)) + .build(); + player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1))); + PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory); + + player.setMediaItem( + MediaItem.fromUri("asset:///media/dash/multi-period-with-offset/sample.mpd")); + player.prepare(); + player.play(); + TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED); + player.release(); + + DumpFileAsserts.assertOutput( + applicationContext, playbackOutput, "playbackdumps/dash/multi-period-with-offset.dump"); + } } diff --git a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaPeriod.java b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaPeriod.java index 6b9f147017..ea9c8ba161 100644 --- a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaPeriod.java +++ b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaPeriod.java @@ -264,7 +264,8 @@ import java.util.List; drmSessionManager, drmEventDispatcher, loadErrorHandlingPolicy, - mediaSourceEventDispatcher); + mediaSourceEventDispatcher, + /* canReportInitialDiscontinuity= */ false); } private static TrackGroupArray buildTrackGroups( diff --git a/libraries/test_data/src/test/assets/media/dash/multi-period-with-offset/sample.mpd b/libraries/test_data/src/test/assets/media/dash/multi-period-with-offset/sample.mpd new file mode 100644 index 0000000000..09a74a67e0 --- /dev/null +++ b/libraries/test_data/src/test/assets/media/dash/multi-period-with-offset/sample.mpd @@ -0,0 +1,23 @@ + + + + + + sample.video.mp4 + + + + + + + + + + sample.video.mp4 + + + + + + + diff --git a/libraries/test_data/src/test/assets/media/dash/multi-period-with-offset/sample.video.mp4 b/libraries/test_data/src/test/assets/media/dash/multi-period-with-offset/sample.video.mp4 new file mode 100644 index 0000000000..7f456f85f9 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/dash/multi-period-with-offset/sample.video.mp4 differ diff --git a/libraries/test_data/src/test/assets/playbackdumps/dash/multi-period-with-offset.dump b/libraries/test_data/src/test/assets/playbackdumps/dash/multi-period-with-offset.dump new file mode 100644 index 0000000000..811eab6f4e --- /dev/null +++ b/libraries/test_data/src/test/assets/playbackdumps/dash/multi-period-with-offset.dump @@ -0,0 +1,321 @@ +MediaCodecAdapter (exotest.video.avc): + inputBuffers: + count = 46 + input buffer #0: + timeUs = 1000000000000 + contents = length 36692, hash D216076E + input buffer #1: + timeUs = 1000000066733 + contents = length 5312, hash D45D3CA0 + input buffer #2: + timeUs = 1000000033366 + contents = length 599, hash 1BE7812D + input buffer #3: + timeUs = 1000000200200 + contents = length 7735, hash 4490F110 + input buffer #4: + timeUs = 1000000133466 + contents = length 987, hash 560B5036 + input buffer #5: + timeUs = 1000000100100 + contents = length 673, hash ED7CD8C7 + input buffer #6: + timeUs = 1000000166833 + contents = length 523, hash 3020DF50 + input buffer #7: + timeUs = 1000000333666 + contents = length 6061, hash 736C72B2 + input buffer #8: + timeUs = 1000000266933 + contents = length 992, hash FE132F23 + input buffer #9: + timeUs = 1000000233566 + contents = length 623, hash 5B2C1816 + input buffer #10: + timeUs = 1000000300300 + contents = length 421, hash 742E69C1 + input buffer #11: + timeUs = 1000000433766 + contents = length 4899, hash F72F86A1 + input buffer #12: + timeUs = 1000000400400 + contents = length 568, hash 519A8E50 + input buffer #13: + timeUs = 1000000367033 + contents = length 620, hash 3990AA39 + input buffer #14: + timeUs = 0 + flags = 4 + contents = length 0, hash 1 + input buffer #15: + timeUs = 1000000000000 + contents = length 36692, hash D216076E + input buffer #16: + timeUs = 1000000066733 + contents = length 5312, hash D45D3CA0 + input buffer #17: + timeUs = 1000000033366 + contents = length 599, hash 1BE7812D + input buffer #18: + timeUs = 1000000200200 + contents = length 7735, hash 4490F110 + input buffer #19: + timeUs = 1000000133466 + contents = length 987, hash 560B5036 + input buffer #20: + timeUs = 1000000100100 + contents = length 673, hash ED7CD8C7 + input buffer #21: + timeUs = 1000000166833 + contents = length 523, hash 3020DF50 + input buffer #22: + timeUs = 1000000333666 + contents = length 6061, hash 736C72B2 + input buffer #23: + timeUs = 1000000266933 + contents = length 992, hash FE132F23 + input buffer #24: + timeUs = 1000000233566 + contents = length 623, hash 5B2C1816 + input buffer #25: + timeUs = 1000000300300 + contents = length 421, hash 742E69C1 + input buffer #26: + timeUs = 1000000433766 + contents = length 4899, hash F72F86A1 + input buffer #27: + timeUs = 1000000400400 + contents = length 568, hash 519A8E50 + input buffer #28: + timeUs = 1000000367033 + contents = length 620, hash 3990AA39 + input buffer #29: + timeUs = 1000000567233 + contents = length 5450, hash F06EC4AA + input buffer #30: + timeUs = 1000000500500 + contents = length 1051, hash 92DFA63A + input buffer #31: + timeUs = 1000000467133 + contents = length 874, hash 69587FB4 + input buffer #32: + timeUs = 1000000533866 + contents = length 781, hash 36BE495B + input buffer #33: + timeUs = 1000000700700 + contents = length 4725, hash AC0C8CD3 + input buffer #34: + timeUs = 1000000633966 + contents = length 1022, hash 5D8BFF34 + input buffer #35: + timeUs = 1000000600600 + contents = length 790, hash 99413A99 + input buffer #36: + timeUs = 1000000667333 + contents = length 610, hash 5E129290 + input buffer #37: + timeUs = 1000000834166 + contents = length 2751, hash 769974CB + input buffer #38: + timeUs = 1000000767433 + contents = length 745, hash B78A477A + input buffer #39: + timeUs = 1000000734066 + contents = length 621, hash CF741E7A + input buffer #40: + timeUs = 1000000800800 + contents = length 505, hash 1DB4894E + input buffer #41: + timeUs = 1000000967633 + contents = length 1268, hash C15348DC + input buffer #42: + timeUs = 1000000900900 + contents = length 880, hash C2DE85D0 + input buffer #43: + timeUs = 1000000867533 + contents = length 530, hash C98BC6A8 + input buffer #44: + timeUs = 1000000934266 + contents = length 568, hash 4FE5C8EA + input buffer #45: + timeUs = 0 + flags = 4 + contents = length 0, hash 1 + outputBuffers: + count = 44 + output buffer #0: + timeUs = 1000000000000 + size = 36692 + rendered = true + output buffer #1: + timeUs = 1000000066733 + size = 5312 + rendered = true + output buffer #2: + timeUs = 1000000033366 + size = 599 + rendered = true + output buffer #3: + timeUs = 1000000200200 + size = 7735 + rendered = true + output buffer #4: + timeUs = 1000000133466 + size = 987 + rendered = true + output buffer #5: + timeUs = 1000000100100 + size = 673 + rendered = true + output buffer #6: + timeUs = 1000000166833 + size = 523 + rendered = true + output buffer #7: + timeUs = 1000000333666 + size = 6061 + rendered = true + output buffer #8: + timeUs = 1000000266933 + size = 992 + rendered = true + output buffer #9: + timeUs = 1000000233566 + size = 623 + rendered = true + output buffer #10: + timeUs = 1000000300300 + size = 421 + rendered = true + output buffer #11: + timeUs = 1000000433766 + size = 4899 + rendered = true + output buffer #12: + timeUs = 1000000400400 + size = 568 + rendered = true + output buffer #13: + timeUs = 1000000367033 + size = 620 + rendered = true + output buffer #14: + timeUs = 1000000000000 + size = 36692 + rendered = false + output buffer #15: + timeUs = 1000000066733 + size = 5312 + rendered = false + output buffer #16: + timeUs = 1000000033366 + size = 599 + rendered = false + output buffer #17: + timeUs = 1000000200200 + size = 7735 + rendered = false + output buffer #18: + timeUs = 1000000133466 + size = 987 + rendered = false + output buffer #19: + timeUs = 1000000100100 + size = 673 + rendered = false + output buffer #20: + timeUs = 1000000166833 + size = 523 + rendered = false + output buffer #21: + timeUs = 1000000333666 + size = 6061 + rendered = false + output buffer #22: + timeUs = 1000000266933 + size = 992 + rendered = false + output buffer #23: + timeUs = 1000000233566 + size = 623 + rendered = false + output buffer #24: + timeUs = 1000000300300 + size = 421 + rendered = false + output buffer #25: + timeUs = 1000000433766 + size = 4899 + rendered = false + output buffer #26: + timeUs = 1000000400400 + size = 568 + rendered = false + output buffer #27: + timeUs = 1000000367033 + size = 620 + rendered = false + output buffer #28: + timeUs = 1000000567233 + size = 5450 + rendered = true + output buffer #29: + timeUs = 1000000500500 + size = 1051 + rendered = true + output buffer #30: + timeUs = 1000000467133 + size = 874 + rendered = false + output buffer #31: + timeUs = 1000000533866 + size = 781 + rendered = true + output buffer #32: + timeUs = 1000000700700 + size = 4725 + rendered = true + output buffer #33: + timeUs = 1000000633966 + size = 1022 + rendered = true + output buffer #34: + timeUs = 1000000600600 + size = 790 + rendered = true + output buffer #35: + timeUs = 1000000667333 + size = 610 + rendered = true + output buffer #36: + timeUs = 1000000834166 + size = 2751 + rendered = true + output buffer #37: + timeUs = 1000000767433 + size = 745 + rendered = true + output buffer #38: + timeUs = 1000000734066 + size = 621 + rendered = true + output buffer #39: + timeUs = 1000000800800 + size = 505 + rendered = true + output buffer #40: + timeUs = 1000000967633 + size = 1268 + rendered = true + output buffer #41: + timeUs = 1000000900900 + size = 880 + rendered = true + output buffer #42: + timeUs = 1000000867533 + size = 530 + rendered = true + output buffer #43: + timeUs = 1000000934266 + size = 568 + rendered = true diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeAdaptiveMediaPeriod.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeAdaptiveMediaPeriod.java index 5d3a4f839b..5ef6781067 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeAdaptiveMediaPeriod.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeAdaptiveMediaPeriod.java @@ -187,7 +187,8 @@ public class FakeAdaptiveMediaPeriod DrmSessionManager.DRM_UNSUPPORTED, new DrmSessionEventListener.EventDispatcher(), new DefaultLoadErrorHandlingPolicy(/* minimumLoadableRetryCount= */ 3), - mediaSourceEventDispatcher); + mediaSourceEventDispatcher, + /* canReportInitialDiscontinuity= */ false); streams[i] = sampleStream; sampleStreams.add(sampleStream); streamResetFlags[i] = true;