From 71591782f0e3597f2fa6f8b12c5825db4c4a5f3d Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 19 Jul 2023 16:02:16 +0100 Subject: [PATCH] Adjust periodEnded logic further Periods that are not the last in the manifest are always ended and non-dynamic periods are also always ended by definition. Also, some periods may not be able to declare their final duration accurately and we need to account for some inaccuracy. This can be done by testing whether no further full segment within the duration can be expected. --- .../dash/DefaultDashChunkSource.java | 13 ++- .../dash/DefaultDashChunkSourceTest.java | 99 +++++++++++++++++++ .../mpd/sample_mpd_live_known_duration_ended | 20 ++++ .../sample_mpd_live_known_duration_not_ended | 20 ++++ 4 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 libraries/test_data/src/test/assets/media/mpd/sample_mpd_live_known_duration_ended create mode 100644 libraries/test_data/src/test/assets/media/mpd/sample_mpd_live_known_duration_not_ended diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java index 2d9a34af67..6e58e0f0e1 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DefaultDashChunkSource.java @@ -410,7 +410,9 @@ public class DefaultDashChunkSource implements DashChunkSource { } long periodDurationUs = representationHolder.periodDurationUs; - boolean periodEnded = periodDurationUs != C.TIME_UNSET; + boolean isLastPeriodInDynamicManifest = + manifest.dynamic && periodIndex == manifest.getPeriodCount() - 1; + boolean periodEnded = !isLastPeriodInDynamicManifest || periodDurationUs != C.TIME_UNSET; if (representationHolder.getSegmentCount() == 0) { // The index doesn't define any segments. @@ -420,10 +422,15 @@ public class DefaultDashChunkSource implements DashChunkSource { long firstAvailableSegmentNum = representationHolder.getFirstAvailableSegmentNum(nowUnixTimeUs); long lastAvailableSegmentNum = representationHolder.getLastAvailableSegmentNum(nowUnixTimeUs); - if (manifest.dynamic) { + if (isLastPeriodInDynamicManifest) { long lastAvailableSegmentEndTimeUs = representationHolder.getSegmentEndTimeUs(lastAvailableSegmentNum); - periodEnded &= (lastAvailableSegmentEndTimeUs >= periodDurationUs); + long lastSegmentDurationUs = + lastAvailableSegmentEndTimeUs + - representationHolder.getSegmentStartTimeUs(lastAvailableSegmentNum); + // Account for some inaccuracy in the overall period duration value by assuming that the + // period is finished once no further full sample fits into the overall duration. + periodEnded &= (lastAvailableSegmentEndTimeUs + lastSegmentDurationUs >= periodDurationUs); } long segmentNum = getSegmentNum( diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java index bd913aa183..c21c8a967f 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultDashChunkSourceTest.java @@ -38,6 +38,7 @@ import androidx.media3.exoplayer.source.MediaLoadData; import androidx.media3.exoplayer.source.chunk.BundledChunkExtractor; import androidx.media3.exoplayer.source.chunk.Chunk; import androidx.media3.exoplayer.source.chunk.ChunkHolder; +import androidx.media3.exoplayer.source.chunk.MediaChunk; import androidx.media3.exoplayer.trackselection.AdaptiveTrackSelection; import androidx.media3.exoplayer.trackselection.FixedTrackSelection; import androidx.media3.exoplayer.upstream.CmcdConfiguration; @@ -414,6 +415,104 @@ public class DefaultDashChunkSourceTest { "key4=5.0"); } + @Test + public void + getNextChunk_afterLastAvailableButBeforeEndOfLiveManifestWithKnownDuration_doesNotReturnEndOfStream() + throws Exception { + DashManifest manifest = + new DashManifestParser() + .parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), + "media/mpd/sample_mpd_live_known_duration_not_ended")); + DefaultDashChunkSource chunkSource = + new DefaultDashChunkSource( + BundledChunkExtractor.FACTORY, + new LoaderErrorThrower.Placeholder(), + manifest, + new BaseUrlExclusionList(), + /* periodIndex= */ 0, + /* adaptationSetIndices= */ new int[] {0}, + new FixedTrackSelection(new TrackGroup(new Format.Builder().build()), /* track= */ 0), + C.TRACK_TYPE_VIDEO, + new FakeDataSource(), + /* elapsedRealtimeOffsetMs= */ 0, + /* maxSegmentsPerLoad= */ 1, + /* enableEventMessageTrack= */ false, + /* closedCaptionFormats= */ ImmutableList.of(), + /* playerTrackEmsgHandler= */ null, + PlayerId.UNSET, + /* cmcdConfiguration= */ null); + ChunkHolder output = new ChunkHolder(); + // Populate with last available media chunk + chunkSource.getNextChunk( + /* playbackPositionUs= */ 0, + /* loadPositionUs= */ 0, + /* queue= */ ImmutableList.of(), + output); + Chunk previousChunk = output.chunk; + output.clear(); + + // Request another chunk + chunkSource.getNextChunk( + /* playbackPositionUs= */ 0, + /* loadPositionUs= */ 4_000_000, + /* queue= */ ImmutableList.of((MediaChunk) previousChunk), + output); + + assertThat(output.endOfStream).isFalse(); + assertThat(output.chunk).isNull(); + } + + @Test + public void getNextChunk_atEndOfLiveManifestWithKnownDuration_returnsEndOfStream() + throws Exception { + DashManifest manifest = + new DashManifestParser() + .parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), + "media/mpd/sample_mpd_live_known_duration_ended")); + DefaultDashChunkSource chunkSource = + new DefaultDashChunkSource( + BundledChunkExtractor.FACTORY, + new LoaderErrorThrower.Placeholder(), + manifest, + new BaseUrlExclusionList(), + /* periodIndex= */ 0, + /* adaptationSetIndices= */ new int[] {0}, + new FixedTrackSelection(new TrackGroup(new Format.Builder().build()), /* track= */ 0), + C.TRACK_TYPE_VIDEO, + new FakeDataSource(), + /* elapsedRealtimeOffsetMs= */ 0, + /* maxSegmentsPerLoad= */ 1, + /* enableEventMessageTrack= */ false, + /* closedCaptionFormats= */ ImmutableList.of(), + /* playerTrackEmsgHandler= */ null, + PlayerId.UNSET, + /* cmcdConfiguration= */ null); + ChunkHolder output = new ChunkHolder(); + // Populate with last media chunk + chunkSource.getNextChunk( + /* playbackPositionUs= */ 0, + /* loadPositionUs= */ 4_000_000, + /* queue= */ ImmutableList.of(), + output); + Chunk previousChunk = output.chunk; + output.clear(); + + // Request next chunk + chunkSource.getNextChunk( + /* playbackPositionUs= */ 0, + /* loadPositionUs= */ 8_000_000, + /* queue= */ ImmutableList.of((MediaChunk) previousChunk), + output); + + assertThat(output.endOfStream).isTrue(); + } + private DashChunkSource createDashChunkSource( int numberOfTracks, @Nullable CmcdConfiguration cmcdConfiguration) throws IOException { Assertions.checkArgument(numberOfTracks < 6); diff --git a/libraries/test_data/src/test/assets/media/mpd/sample_mpd_live_known_duration_ended b/libraries/test_data/src/test/assets/media/mpd/sample_mpd_live_known_duration_ended new file mode 100644 index 0000000000..cad0e8da01 --- /dev/null +++ b/libraries/test_data/src/test/assets/media/mpd/sample_mpd_live_known_duration_ended @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/libraries/test_data/src/test/assets/media/mpd/sample_mpd_live_known_duration_not_ended b/libraries/test_data/src/test/assets/media/mpd/sample_mpd_live_known_duration_not_ended new file mode 100644 index 0000000000..ddf8882e87 --- /dev/null +++ b/libraries/test_data/src/test/assets/media/mpd/sample_mpd_live_known_duration_not_ended @@ -0,0 +1,20 @@ + + + + + + + + + + + + + +