From aab6aef443d49d60eaad0f749a174e6cf93533b5 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 28 Oct 2020 14:10:11 +0000 Subject: [PATCH] Set min/max supported live offset in DashMediaSource. In order to ensure we can update the values for new manifests but still use the user provided override, we need to save the original and the updated MediaItem seperately. And in order to incorporate the existing logic for the min/max supported live offset, which we already use to correct the target offset, also move both places together so that all the adjustment happens in one place. Logical adjustments to the previous min/max supported live offset: - Use the user-provided MediaItem values if set - Use the newly parsed ServiceDescription values if available. - Limit the minimum to 0 if the current time is in the window and we can assume to have low-latency stream. - Add minBufferTime from the manifest to ensure we don't reduce the live offset below this value. Issue: #4904 PiperOrigin-RevId: 339452816 --- .../source/dash/DashMediaSource.java | 143 ++++++++++-------- .../source/dash/DashMediaSourceTest.java | 38 ++++- ...mpd_live_with_complete_service_description | 2 +- ...esentation_delay_2s_min_buffer_time_500ms} | 3 +- 4 files changed, 115 insertions(+), 71 deletions(-) rename testdata/src/test/assets/media/mpd/{sample_mpd_live_with_suggested_presentation_delay_2s => sample_mpd_live_with_suggested_presentation_delay_2s_min_buffer_time_500ms} (91%) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index a9ef160971..ba34fd709d 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -427,6 +427,7 @@ public final class DashMediaSource extends BaseMediaSource { private static final String TAG = "DashMediaSource"; + private final MediaItem originalMediaItem; private final boolean sideloadedManifest; private final DataSource.Factory manifestDataSourceFactory; private final DashChunkSource.Factory chunkSourceFactory; @@ -451,8 +452,7 @@ public final class DashMediaSource extends BaseMediaSource { private IOException manifestFatalError; private Handler handler; - private MediaItem mediaItem; - private MediaItem.PlaybackProperties playbackProperties; + private MediaItem updatedMediaItem; private Uri manifestUri; private Uri initialManifestUri; private DashManifest manifest; @@ -476,10 +476,10 @@ public final class DashMediaSource extends BaseMediaSource { DrmSessionManager drmSessionManager, LoadErrorHandlingPolicy loadErrorHandlingPolicy, long fallbackTargetLiveOffsetMs) { - this.mediaItem = mediaItem; - this.playbackProperties = checkNotNull(mediaItem.playbackProperties); - this.manifestUri = playbackProperties.uri; - this.initialManifestUri = playbackProperties.uri; + this.originalMediaItem = mediaItem; + this.updatedMediaItem = mediaItem; + this.manifestUri = checkNotNull(mediaItem.playbackProperties).uri; + this.initialManifestUri = mediaItem.playbackProperties.uri; this.manifest = manifest; this.manifestDataSourceFactory = manifestDataSourceFactory; this.manifestParser = manifestParser; @@ -531,12 +531,12 @@ public final class DashMediaSource extends BaseMediaSource { @Override @Nullable public Object getTag() { - return playbackProperties.tag; + return castNonNull(updatedMediaItem.playbackProperties).tag; } @Override public MediaItem getMediaItem() { - return mediaItem; + return updatedMediaItem; } @Override @@ -692,9 +692,6 @@ public final class DashMediaSource extends BaseMediaSource { staleManifestReloadAttempt = 0; } - mediaItem = mergeLiveConfiguration(mediaItem, fallbackTargetLiveOffsetMs, newManifest); - playbackProperties = castNonNull(mediaItem.playbackProperties); - manifest = newManifest; manifestLoadPending &= manifest.dynamic; manifestLoadStartTimestampMs = elapsedRealtimeMs - loadDurationMs; @@ -939,13 +936,13 @@ public final class DashMediaSource extends BaseMediaSource { long windowDefaultStartPositionUs = 0; if (manifest.dynamic) { - ensureTargetLiveOffsetIsInLiveWindow( + updateMediaItemLiveConfiguration( /* nowPeriodTimeUs= */ currentStartTimeUs + nowUnixTimeUs - C.msToUs(windowStartTimeMs), /* windowStartPeriodTimeUs= */ currentStartTimeUs, /* windowEndPeriodTimeUs= */ currentEndTimeUs); windowDefaultStartPositionUs = nowUnixTimeUs - - C.msToUs(windowStartTimeMs + mediaItem.liveConfiguration.targetLiveOffsetMs); + - C.msToUs(windowStartTimeMs + updatedMediaItem.liveConfiguration.targetLiveOffsetMs); long minimumDefaultStartPositionUs = min(MIN_LIVE_DEFAULT_START_POSITION_US, windowDurationUs / 2); if (windowDefaultStartPositionUs < minimumDefaultStartPositionUs) { @@ -965,7 +962,7 @@ public final class DashMediaSource extends BaseMediaSource { windowDurationUs, windowDefaultStartPositionUs, manifest, - mediaItem); + updatedMediaItem); refreshSourceInfo(timeline); if (!sideloadedManifest) { @@ -999,24 +996,81 @@ public final class DashMediaSource extends BaseMediaSource { } } - private void ensureTargetLiveOffsetIsInLiveWindow( + private void updateMediaItemLiveConfiguration( long nowPeriodTimeUs, long windowStartPeriodTimeUs, long windowEndPeriodTimeUs) { - long targetLiveOffsetUs = C.msToUs(mediaItem.liveConfiguration.targetLiveOffsetMs); - long minOffsetUs = nowPeriodTimeUs - windowEndPeriodTimeUs; - if (targetLiveOffsetUs < minOffsetUs) { - targetLiveOffsetUs = minOffsetUs; + long maxLiveOffsetMs; + if (originalMediaItem.liveConfiguration.maxLiveOffsetMs != C.TIME_UNSET) { + maxLiveOffsetMs = originalMediaItem.liveConfiguration.maxLiveOffsetMs; + } else if (manifest.serviceDescription != null + && manifest.serviceDescription.maxOffsetMs != C.TIME_UNSET) { + maxLiveOffsetMs = manifest.serviceDescription.maxOffsetMs; + } else { + maxLiveOffsetMs = C.usToMs(nowPeriodTimeUs - windowStartPeriodTimeUs); } - long maxOffsetUs = nowPeriodTimeUs - windowStartPeriodTimeUs; - if (targetLiveOffsetUs > maxOffsetUs) { + long minLiveOffsetMs; + if (originalMediaItem.liveConfiguration.minLiveOffsetMs != C.TIME_UNSET) { + minLiveOffsetMs = originalMediaItem.liveConfiguration.minLiveOffsetMs; + } else if (manifest.serviceDescription != null + && manifest.serviceDescription.minOffsetMs != C.TIME_UNSET) { + minLiveOffsetMs = manifest.serviceDescription.minOffsetMs; + } else { + minLiveOffsetMs = C.usToMs(nowPeriodTimeUs - windowEndPeriodTimeUs); + if (minLiveOffsetMs < 0 && maxLiveOffsetMs > 0) { + // The current time is in the window, so assume all clocks are synchronized and set the + // minimum to a live offset of zero. + minLiveOffsetMs = 0; + } + if (manifest.minBufferTimeMs != C.TIME_UNSET) { + minLiveOffsetMs = min(minLiveOffsetMs + manifest.minBufferTimeMs, maxLiveOffsetMs); + } + } + long targetOffsetMs; + if (updatedMediaItem.liveConfiguration.targetLiveOffsetMs != C.TIME_UNSET) { + // Keep existing target offset even if the media configuration changes. + targetOffsetMs = updatedMediaItem.liveConfiguration.targetLiveOffsetMs; + } else if (manifest.serviceDescription != null + && manifest.serviceDescription.targetOffsetMs != C.TIME_UNSET) { + targetOffsetMs = manifest.serviceDescription.targetOffsetMs; + } else if (manifest.suggestedPresentationDelayMs != C.TIME_UNSET) { + targetOffsetMs = manifest.suggestedPresentationDelayMs; + } else { + targetOffsetMs = fallbackTargetLiveOffsetMs; + } + if (targetOffsetMs < minLiveOffsetMs) { + targetOffsetMs = minLiveOffsetMs; + } + if (targetOffsetMs > maxLiveOffsetMs) { long windowDurationUs = windowEndPeriodTimeUs - windowStartPeriodTimeUs; - targetLiveOffsetUs = - maxOffsetUs - min(MIN_LIVE_DEFAULT_START_POSITION_US, windowDurationUs / 2); + long liveOffsetAtWindowStartUs = nowPeriodTimeUs - windowStartPeriodTimeUs; + long safeDistanceFromWindowStartUs = + min(MIN_LIVE_DEFAULT_START_POSITION_US, windowDurationUs / 2); + long maxTargetOffsetForSafeDistanceToWindowStartMs = + C.usToMs(liveOffsetAtWindowStartUs - safeDistanceFromWindowStartUs); + targetOffsetMs = + Util.constrainValue( + maxTargetOffsetForSafeDistanceToWindowStartMs, minLiveOffsetMs, maxLiveOffsetMs); } - long targetLiveOffsetMs = C.usToMs(targetLiveOffsetUs); - if (mediaItem.liveConfiguration.targetLiveOffsetMs != targetLiveOffsetMs) { - mediaItem = mediaItem.buildUpon().setLiveTargetOffsetMs(targetLiveOffsetMs).build(); - playbackProperties = castNonNull(mediaItem.playbackProperties); + float minPlaybackSpeed = C.RATE_UNSET; + if (originalMediaItem.liveConfiguration.minPlaybackSpeed != C.RATE_UNSET) { + minPlaybackSpeed = originalMediaItem.liveConfiguration.minPlaybackSpeed; + } else if (manifest.serviceDescription != null) { + minPlaybackSpeed = manifest.serviceDescription.minPlaybackSpeed; } + float maxPlaybackSpeed = C.RATE_UNSET; + if (originalMediaItem.liveConfiguration.maxPlaybackSpeed != C.RATE_UNSET) { + maxPlaybackSpeed = originalMediaItem.liveConfiguration.maxPlaybackSpeed; + } else if (manifest.serviceDescription != null) { + maxPlaybackSpeed = manifest.serviceDescription.maxPlaybackSpeed; + } + updatedMediaItem = + originalMediaItem + .buildUpon() + .setLiveTargetOffsetMs(targetOffsetMs) + .setLiveMinOffsetMs(minLiveOffsetMs) + .setLiveMaxOffsetMs(maxLiveOffsetMs) + .setLiveMinPlaybackSpeed(minPlaybackSpeed) + .setLiveMaxPlaybackSpeed(maxPlaybackSpeed) + .build(); } private void scheduleManifestRefresh(long delayUntilNextLoadMs) { @@ -1087,41 +1141,6 @@ public final class DashMediaSource extends BaseMediaSource { return LongMath.divide(intervalUs, 1000, RoundingMode.CEILING); } - private static MediaItem mergeLiveConfiguration( - MediaItem mediaItem, long fallbackTargetLiveOffsetMs, DashManifest manifest) { - // Evaluate live config properties from media item and manifest according to precedence. - long liveTargetOffsetMs; - if (mediaItem.liveConfiguration.targetLiveOffsetMs != C.TIME_UNSET) { - liveTargetOffsetMs = mediaItem.liveConfiguration.targetLiveOffsetMs; - } else if (manifest.serviceDescription != null - && manifest.serviceDescription.targetOffsetMs != C.TIME_UNSET) { - liveTargetOffsetMs = manifest.serviceDescription.targetOffsetMs; - } else if (manifest.suggestedPresentationDelayMs != C.TIME_UNSET) { - liveTargetOffsetMs = manifest.suggestedPresentationDelayMs; - } else { - liveTargetOffsetMs = fallbackTargetLiveOffsetMs; - } - float liveMinPlaybackSpeed = C.RATE_UNSET; - if (mediaItem.liveConfiguration.minPlaybackSpeed != C.RATE_UNSET) { - liveMinPlaybackSpeed = mediaItem.liveConfiguration.minPlaybackSpeed; - } else if (manifest.serviceDescription != null) { - liveMinPlaybackSpeed = manifest.serviceDescription.minPlaybackSpeed; - } - float liveMaxPlaybackSpeed = C.RATE_UNSET; - if (mediaItem.liveConfiguration.maxPlaybackSpeed != C.RATE_UNSET) { - liveMaxPlaybackSpeed = mediaItem.liveConfiguration.maxPlaybackSpeed; - } else if (manifest.serviceDescription != null) { - liveMaxPlaybackSpeed = manifest.serviceDescription.maxPlaybackSpeed; - } - // Update live configuration in the media item. - return mediaItem - .buildUpon() - .setLiveTargetOffsetMs(liveTargetOffsetMs) - .setLiveMinPlaybackSpeed(liveMinPlaybackSpeed) - .setLiveMaxPlaybackSpeed(liveMaxPlaybackSpeed) - .build(); - } - private static final class PeriodSeekInfo { public static PeriodSeekInfo createPeriodSeekInfo( diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java index a4b3c2c562..7e5beb7de4 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java @@ -55,8 +55,9 @@ public final class DashMediaSourceTest { private static final String SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION = "media/mpd/sample_mpd_live_without_live_configuration"; - private static final String SAMPLE_MPD_LIVE_WITH_SUGGESTED_PRESENTATION_DELAY_2S = - "media/mpd/sample_mpd_live_with_suggested_presentation_delay_2s"; + private static final String + SAMPLE_MPD_LIVE_WITH_SUGGESTED_PRESENTATION_DELAY_2S_MIN_BUFFER_TIME_500MS = + "media/mpd/sample_mpd_live_with_suggested_presentation_delay_2s_min_buffer_time_500ms"; private static final String SAMPLE_MPD_LIVE_WITH_COMPLETE_SERVICE_DESCRIPTION = "media/mpd/sample_mpd_live_with_complete_service_description"; private static final String SAMPLE_MPD_LIVE_WITH_OFFSET_INSIDE_WINDOW = @@ -290,6 +291,8 @@ public final class DashMediaSourceTest { assertThat(mediaItemFromSource.liveConfiguration.targetLiveOffsetMs) .isEqualTo(DashMediaSource.DEFAULT_FALLBACK_TARGET_LIVE_OFFSET_MS); + assertThat(mediaItemFromSource.liveConfiguration.minLiveOffsetMs).isEqualTo(0L); + assertThat(mediaItemFromSource.liveConfiguration.maxLiveOffsetMs).isEqualTo(58_000L); assertThat(mediaItemFromSource.liveConfiguration.minPlaybackSpeed).isEqualTo(C.RATE_UNSET); assertThat(mediaItemFromSource.liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET); } @@ -306,6 +309,8 @@ public final class DashMediaSourceTest { MediaItem mediaItemFromSource = prepareAndWaitForTimelineRefresh(mediaSource).mediaItem; assertThat(mediaItemFromSource.liveConfiguration.targetLiveOffsetMs).isEqualTo(1234L); + assertThat(mediaItemFromSource.liveConfiguration.minLiveOffsetMs).isEqualTo(0L); + assertThat(mediaItemFromSource.liveConfiguration.maxLiveOffsetMs).isEqualTo(58_000L); assertThat(mediaItemFromSource.liveConfiguration.minPlaybackSpeed).isEqualTo(C.RATE_UNSET); assertThat(mediaItemFromSource.liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET); } @@ -319,6 +324,8 @@ public final class DashMediaSourceTest { .setLiveTargetOffsetMs(876L) .setLiveMinPlaybackSpeed(23f) .setLiveMaxPlaybackSpeed(42f) + .setLiveMinOffsetMs(500L) + .setLiveMaxOffsetMs(20_000L) .build(); DashMediaSource mediaSource = new DashMediaSource.Factory( @@ -329,47 +336,58 @@ public final class DashMediaSourceTest { MediaItem mediaItemFromSource = prepareAndWaitForTimelineRefresh(mediaSource).mediaItem; assertThat(mediaItemFromSource.liveConfiguration.targetLiveOffsetMs).isEqualTo(876L); + assertThat(mediaItemFromSource.liveConfiguration.minLiveOffsetMs).isEqualTo(500L); + assertThat(mediaItemFromSource.liveConfiguration.maxLiveOffsetMs).isEqualTo(20_000L); assertThat(mediaItemFromSource.liveConfiguration.minPlaybackSpeed).isEqualTo(23f); assertThat(mediaItemFromSource.liveConfiguration.maxPlaybackSpeed).isEqualTo(42f); } @Test - public void prepare_withSuggestedPresentationDelay_usesManifestValue() + public void prepare_withSuggestedPresentationDelayAndMinBufferTime_usesManifestValue() throws InterruptedException { DashMediaSource mediaSource = new DashMediaSource.Factory( () -> - createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITH_SUGGESTED_PRESENTATION_DELAY_2S)) + createSampleMpdDataSource( + SAMPLE_MPD_LIVE_WITH_SUGGESTED_PRESENTATION_DELAY_2S_MIN_BUFFER_TIME_500MS)) .setFallbackTargetLiveOffsetMs(1234L) .createMediaSource(MediaItem.fromUri(Uri.EMPTY)); MediaItem mediaItem = prepareAndWaitForTimelineRefresh(mediaSource).mediaItem; assertThat(mediaItem.liveConfiguration.targetLiveOffsetMs).isEqualTo(2_000L); + assertThat(mediaItem.liveConfiguration.minLiveOffsetMs).isEqualTo(500L); + assertThat(mediaItem.liveConfiguration.maxLiveOffsetMs).isEqualTo(58_000L); assertThat(mediaItem.liveConfiguration.minPlaybackSpeed).isEqualTo(C.RATE_UNSET); assertThat(mediaItem.liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET); } @Test - public void prepare_withSuggestedPresentationDelay_withMediaItemLiveProperties_usesMediaItem() - throws InterruptedException { + public void + prepare_withSuggestedPresentationDelayAndMinBufferTime_withMediaItemLiveProperties_usesMediaItem() + throws InterruptedException { MediaItem mediaItem = new MediaItem.Builder() .setUri(Uri.EMPTY) .setLiveTargetOffsetMs(876L) .setLiveMinPlaybackSpeed(23f) .setLiveMaxPlaybackSpeed(42f) + .setLiveMinOffsetMs(200L) + .setLiveMaxOffsetMs(999L) .build(); DashMediaSource mediaSource = new DashMediaSource.Factory( () -> - createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITH_SUGGESTED_PRESENTATION_DELAY_2S)) + createSampleMpdDataSource( + SAMPLE_MPD_LIVE_WITH_SUGGESTED_PRESENTATION_DELAY_2S_MIN_BUFFER_TIME_500MS)) .setFallbackTargetLiveOffsetMs(1234L) .createMediaSource(mediaItem); MediaItem mediaItemFromSource = prepareAndWaitForTimelineRefresh(mediaSource).mediaItem; assertThat(mediaItemFromSource.liveConfiguration.targetLiveOffsetMs).isEqualTo(876L); + assertThat(mediaItem.liveConfiguration.minLiveOffsetMs).isEqualTo(200L); + assertThat(mediaItem.liveConfiguration.maxLiveOffsetMs).isEqualTo(999L); assertThat(mediaItemFromSource.liveConfiguration.minPlaybackSpeed).isEqualTo(23f); assertThat(mediaItemFromSource.liveConfiguration.maxPlaybackSpeed).isEqualTo(42f); } @@ -386,6 +404,8 @@ public final class DashMediaSourceTest { MediaItem mediaItem = prepareAndWaitForTimelineRefresh(mediaSource).mediaItem; assertThat(mediaItem.liveConfiguration.targetLiveOffsetMs).isEqualTo(4_000L); + assertThat(mediaItem.liveConfiguration.minLiveOffsetMs).isEqualTo(2_000L); + assertThat(mediaItem.liveConfiguration.maxLiveOffsetMs).isEqualTo(6_000L); assertThat(mediaItem.liveConfiguration.minPlaybackSpeed).isEqualTo(0.96f); assertThat(mediaItem.liveConfiguration.maxPlaybackSpeed).isEqualTo(1.04f); } @@ -399,6 +419,8 @@ public final class DashMediaSourceTest { .setLiveTargetOffsetMs(876L) .setLiveMinPlaybackSpeed(23f) .setLiveMaxPlaybackSpeed(42f) + .setLiveMinOffsetMs(100L) + .setLiveMaxOffsetMs(999L) .build(); DashMediaSource mediaSource = new DashMediaSource.Factory( @@ -409,6 +431,8 @@ public final class DashMediaSourceTest { MediaItem mediaItemFromSource = prepareAndWaitForTimelineRefresh(mediaSource).mediaItem; assertThat(mediaItemFromSource.liveConfiguration.targetLiveOffsetMs).isEqualTo(876L); + assertThat(mediaItemFromSource.liveConfiguration.minLiveOffsetMs).isEqualTo(100L); + assertThat(mediaItemFromSource.liveConfiguration.maxLiveOffsetMs).isEqualTo(999L); assertThat(mediaItemFromSource.liveConfiguration.minPlaybackSpeed).isEqualTo(23f); assertThat(mediaItemFromSource.liveConfiguration.maxPlaybackSpeed).isEqualTo(42f); } diff --git a/testdata/src/test/assets/media/mpd/sample_mpd_live_with_complete_service_description b/testdata/src/test/assets/media/mpd/sample_mpd_live_with_complete_service_description index f0e6e61255..f53268d38a 100644 --- a/testdata/src/test/assets/media/mpd/sample_mpd_live_with_complete_service_description +++ b/testdata/src/test/assets/media/mpd/sample_mpd_live_with_complete_service_description @@ -9,7 +9,7 @@ schemeIdUri="urn:mpeg:dash:utc:direct:2014" value="2020-01-01T01:00:00Z" /> - + diff --git a/testdata/src/test/assets/media/mpd/sample_mpd_live_with_suggested_presentation_delay_2s b/testdata/src/test/assets/media/mpd/sample_mpd_live_with_suggested_presentation_delay_2s_min_buffer_time_500ms similarity index 91% rename from testdata/src/test/assets/media/mpd/sample_mpd_live_with_suggested_presentation_delay_2s rename to testdata/src/test/assets/media/mpd/sample_mpd_live_with_suggested_presentation_delay_2s_min_buffer_time_500ms index e79977ba79..efc7c5c21b 100644 --- a/testdata/src/test/assets/media/mpd/sample_mpd_live_with_suggested_presentation_delay_2s +++ b/testdata/src/test/assets/media/mpd/sample_mpd_live_with_suggested_presentation_delay_2s_min_buffer_time_500ms @@ -2,9 +2,10 @@ + timeShiftBufferDepth="PT1M">