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