Add some correctness checks to min/max live latency values.

For DASH manifests, we merge min/max live latency values from various
sources and they may not be consistent with each other. To ensure we
use a sensible configuration in all cases, we can add more correctness
checks:
 1. Limit the min/max values to fall into the available live window.
 2. Ensure that maxLatency >= minLatency in all cases.

Issue: google/ExoPlayer#9750
PiperOrigin-RevId: 415282938
This commit is contained in:
tonihei 2021-12-09 17:24:00 +00:00 committed by Ian Baker
parent df8a3dc362
commit 422a003a03
4 changed files with 68 additions and 23 deletions

View File

@ -2,6 +2,10 @@
{ {
"name": "Clear DASH", "name": "Clear DASH",
"samples": [ "samples": [
{
"name": "Test",
"uri": "https://01-str01-3201-prod.tv.cetin.rs/bpk-token/1AG@lk5yp2xm3d5rp5eik2lnd4r5y5s0h4d5lkhyurda/bpk-tv/RTS_1_2145/output1/manifest.mpd"
},
{ {
"name": "HD (MP4, H264)", "name": "HD (MP4, H264)",
"uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd" "uri": "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd"

View File

@ -17,6 +17,8 @@ package androidx.media3.exoplayer.dash;
import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.constrainValue;
import static androidx.media3.common.util.Util.usToMs;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min; import static java.lang.Math.min;
@ -998,31 +1000,43 @@ public final class DashMediaSource extends BaseMediaSource {
} }
private void updateMediaItemLiveConfiguration(long nowInWindowUs, long windowDurationUs) { private void updateMediaItemLiveConfiguration(long nowInWindowUs, long windowDurationUs) {
long maxLiveOffsetMs; // Default maximum offset: start of window.
long maxPossibleLiveOffsetMs = usToMs(nowInWindowUs);
long maxLiveOffsetMs = maxPossibleLiveOffsetMs;
// Override maximum offset with user or media defined values if they are smaller.
if (mediaItem.liveConfiguration.maxOffsetMs != C.TIME_UNSET) { if (mediaItem.liveConfiguration.maxOffsetMs != C.TIME_UNSET) {
maxLiveOffsetMs = mediaItem.liveConfiguration.maxOffsetMs; maxLiveOffsetMs = min(maxLiveOffsetMs, mediaItem.liveConfiguration.maxOffsetMs);
} else if (manifest.serviceDescription != null } else if (manifest.serviceDescription != null
&& manifest.serviceDescription.maxOffsetMs != C.TIME_UNSET) { && manifest.serviceDescription.maxOffsetMs != C.TIME_UNSET) {
maxLiveOffsetMs = manifest.serviceDescription.maxOffsetMs; maxLiveOffsetMs = min(maxLiveOffsetMs, manifest.serviceDescription.maxOffsetMs);
} else {
maxLiveOffsetMs = Util.usToMs(nowInWindowUs);
} }
long minLiveOffsetMs; // Default minimum offset: end of window.
long minLiveOffsetMs = usToMs(nowInWindowUs - windowDurationUs);
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) {
// Ensure to leave one GOP as minimum and don't exceed the maximum possible offset.
minLiveOffsetMs = min(minLiveOffsetMs + manifest.minBufferTimeMs, maxPossibleLiveOffsetMs);
}
// Override minimum offset with user and media defined values if they are larger, but don't
// exceed the maximum possible offset.
if (mediaItem.liveConfiguration.minOffsetMs != C.TIME_UNSET) { if (mediaItem.liveConfiguration.minOffsetMs != C.TIME_UNSET) {
minLiveOffsetMs = mediaItem.liveConfiguration.minOffsetMs; minLiveOffsetMs =
constrainValue(
mediaItem.liveConfiguration.minOffsetMs, minLiveOffsetMs, maxPossibleLiveOffsetMs);
} else if (manifest.serviceDescription != null } else if (manifest.serviceDescription != null
&& manifest.serviceDescription.minOffsetMs != C.TIME_UNSET) { && manifest.serviceDescription.minOffsetMs != C.TIME_UNSET) {
minLiveOffsetMs = manifest.serviceDescription.minOffsetMs; minLiveOffsetMs =
} else { constrainValue(
minLiveOffsetMs = Util.usToMs(nowInWindowUs - windowDurationUs); manifest.serviceDescription.minOffsetMs, minLiveOffsetMs, maxPossibleLiveOffsetMs);
if (minLiveOffsetMs < 0 && maxLiveOffsetMs > 0) { }
// The current time is in the window, so assume all clocks are synchronized and set the if (minLiveOffsetMs > maxLiveOffsetMs) {
// minimum to a live offset of zero. // The values can be set by different sources and may disagree. Prefer the maximum offset
minLiveOffsetMs = 0; // under the assumption that it is safer for playback.
} maxLiveOffsetMs = minLiveOffsetMs;
if (manifest.minBufferTimeMs != C.TIME_UNSET) {
minLiveOffsetMs = min(minLiveOffsetMs + manifest.minBufferTimeMs, maxLiveOffsetMs);
}
} }
long targetOffsetMs; long targetOffsetMs;
if (liveConfiguration.targetOffsetMs != C.TIME_UNSET) { if (liveConfiguration.targetOffsetMs != C.TIME_UNSET) {
@ -1043,9 +1057,9 @@ public final class DashMediaSource extends BaseMediaSource {
long safeDistanceFromWindowStartUs = long safeDistanceFromWindowStartUs =
min(MIN_LIVE_DEFAULT_START_POSITION_US, windowDurationUs / 2); min(MIN_LIVE_DEFAULT_START_POSITION_US, windowDurationUs / 2);
long maxTargetOffsetForSafeDistanceToWindowStartMs = long maxTargetOffsetForSafeDistanceToWindowStartMs =
Util.usToMs(nowInWindowUs - safeDistanceFromWindowStartUs); usToMs(nowInWindowUs - safeDistanceFromWindowStartUs);
targetOffsetMs = targetOffsetMs =
Util.constrainValue( constrainValue(
maxTargetOffsetForSafeDistanceToWindowStartMs, minLiveOffsetMs, maxLiveOffsetMs); maxTargetOffsetForSafeDistanceToWindowStartMs, minLiveOffsetMs, maxLiveOffsetMs);
} }
float minPlaybackSpeed = C.RATE_UNSET; float minPlaybackSpeed = C.RATE_UNSET;

View File

@ -353,7 +353,7 @@ public final class DashMediaSourceTest {
.setTargetOffsetMs(876L) .setTargetOffsetMs(876L)
.setMinPlaybackSpeed(23f) .setMinPlaybackSpeed(23f)
.setMaxPlaybackSpeed(42f) .setMaxPlaybackSpeed(42f)
.setMinOffsetMs(200L) .setMinOffsetMs(600L)
.setMaxOffsetMs(999L) .setMaxOffsetMs(999L)
.build()) .build())
.build(); .build();
@ -369,7 +369,7 @@ public final class DashMediaSourceTest {
prepareAndWaitForTimelineRefresh(mediaSource).liveConfiguration; prepareAndWaitForTimelineRefresh(mediaSource).liveConfiguration;
assertThat(liveConfiguration.targetOffsetMs).isEqualTo(876L); assertThat(liveConfiguration.targetOffsetMs).isEqualTo(876L);
assertThat(liveConfiguration.minOffsetMs).isEqualTo(200L); assertThat(liveConfiguration.minOffsetMs).isEqualTo(600L);
assertThat(liveConfiguration.maxOffsetMs).isEqualTo(999L); assertThat(liveConfiguration.maxOffsetMs).isEqualTo(999L);
assertThat(liveConfiguration.minPlaybackSpeed).isEqualTo(23f); assertThat(liveConfiguration.minPlaybackSpeed).isEqualTo(23f);
assertThat(liveConfiguration.maxPlaybackSpeed).isEqualTo(42f); assertThat(liveConfiguration.maxPlaybackSpeed).isEqualTo(42f);
@ -425,6 +425,33 @@ public final class DashMediaSourceTest {
assertThat(liveConfiguration.maxPlaybackSpeed).isEqualTo(42f); assertThat(liveConfiguration.maxPlaybackSpeed).isEqualTo(42f);
} }
@Test
public void
prepare_withMinMaxOffsetOverridesOutsideOfLiveWindow_adjustsOverridesToBeWithinWindow()
throws Exception {
MediaItem mediaItem =
new MediaItem.Builder()
.setUri(Uri.EMPTY)
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder()
.setMinOffsetMs(0L)
.setMaxOffsetMs(1_000_000_000L)
.build())
.build();
DashMediaSource mediaSource =
new DashMediaSource.Factory(
() ->
createSampleMpdDataSource(
SAMPLE_MPD_LIVE_WITH_SUGGESTED_PRESENTATION_DELAY_2S_MIN_BUFFER_TIME_500MS))
.createMediaSource(mediaItem);
MediaItem.LiveConfiguration liveConfiguration =
prepareAndWaitForTimelineRefresh(mediaSource).liveConfiguration;
assertThat(liveConfiguration.minOffsetMs).isEqualTo(500L);
assertThat(liveConfiguration.maxOffsetMs).isEqualTo(58_000L);
}
@Test @Test
public void prepare_targetLiveOffsetInWindow_manifestTargetOffsetAndAlignedWindowStartPosition() public void prepare_targetLiveOffsetInWindow_manifestTargetOffsetAndAlignedWindowStartPosition()
throws InterruptedException { throws InterruptedException {

View File

@ -4,7 +4,7 @@
suggestedPresentationDelay="PT2S" suggestedPresentationDelay="PT2S"
availabilityStartTime="2020-01-01T00:00:00Z" availabilityStartTime="2020-01-01T00:00:00Z"
minimumUpdatePeriod="PT4M" minimumUpdatePeriod="PT4M"
timeShiftBufferDepth="PT6.0S"> timeShiftBufferDepth="PT8.0S">
<UTCTiming <UTCTiming
schemeIdUri="urn:mpeg:dash:utc:direct:2014" schemeIdUri="urn:mpeg:dash:utc:direct:2014"
value="2020-01-01T01:00:00Z" /> value="2020-01-01T01:00:00Z" />