From d7009901984b7bd94fa6b6fc45395e68158c24b7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 11 Feb 2021 18:10:39 +0000 Subject: [PATCH] Fix available end time calculation for multi-period DASH live streams The available end time was accidentally substracted by the start time of the last period. To avoid similar time reference confusion in the future, also renaming many variables and methods to clearly reflect the time reference point. And to avoid constant conversion, the processManifest method also attempts to converge to time relative to the start of the window as quickly as possible. Issue: #8537 PiperOrigin-RevId: 357001624 --- RELEASENOTES.md | 3 + .../source/dash/DashMediaSource.java | 136 ++++++++---------- .../source/dash/manifest/Period.java | 4 +- 3 files changed, 62 insertions(+), 81 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d0407b4c02..d1adef8cf7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -8,6 +8,9 @@ * Fix a bug where setting playback parameters while using video tunneling would cause an error to be thrown ([#8570](https://github.com/google/ExoPlayer/issues/8570)). +* DASH: + * Fix playback issue for multi-period DASH live streams + ([#8537](https://github.com/google/ExoPlayer/issues/8537)). * UI: * Add builder for `PlayerNotificationManager`. * IMA extension: 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 8f9d6a98bc..3e83c32ecd 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 @@ -901,77 +901,54 @@ public final class DashMediaSource extends BaseMediaSource { } } // Update the window. - boolean windowChangingImplicitly = false; + Period firstPeriod = manifest.getPeriod(0); int lastPeriodIndex = manifest.getPeriodCount() - 1; Period lastPeriod = manifest.getPeriod(lastPeriodIndex); long lastPeriodDurationUs = manifest.getPeriodDurationUs(lastPeriodIndex); long nowUnixTimeUs = C.msToUs(Util.getNowUnixTimeMs(elapsedRealtimeOffsetMs)); - // Get the period-relative start/end times. - long currentStartTimeUs = - getAvailableStartTimeUs( - manifest.getPeriod(0), manifest.getPeriodDurationUs(0), nowUnixTimeUs); - long currentEndTimeUs = getAvailableEndTimeUs(lastPeriod, lastPeriodDurationUs, nowUnixTimeUs); - if (manifest.dynamic && !isIndexExplicit(lastPeriod)) { - // The manifest describes an incomplete live stream. Update the start/end times to reflect the - // live stream duration and the manifest's time shift buffer depth. - long liveStreamEndPositionInLastPeriodUs = currentEndTimeUs - C.msToUs(lastPeriod.startMs); - currentEndTimeUs = min(liveStreamEndPositionInLastPeriodUs, currentEndTimeUs); - if (manifest.timeShiftBufferDepthMs != C.TIME_UNSET) { - long timeShiftBufferDepthUs = C.msToUs(manifest.timeShiftBufferDepthMs); - long offsetInPeriodUs = currentEndTimeUs - timeShiftBufferDepthUs; - int periodIndex = lastPeriodIndex; - while (offsetInPeriodUs < 0 && periodIndex > 0) { - offsetInPeriodUs += manifest.getPeriodDurationUs(--periodIndex); - } - if (periodIndex == 0) { - currentStartTimeUs = max(currentStartTimeUs, offsetInPeriodUs); - } else { - // The time shift buffer starts after the earliest period. - // TODO: Does this ever happen? - currentStartTimeUs = manifest.getPeriodDurationUs(0); - } - } - windowChangingImplicitly = true; + long windowStartTimeInManifestUs = + getAvailableStartTimeInManifestUs( + firstPeriod, manifest.getPeriodDurationUs(0), nowUnixTimeUs); + long windowEndTimeInManifestUs = + getAvailableEndTimeInManifestUs(lastPeriod, lastPeriodDurationUs, nowUnixTimeUs); + boolean windowChangingImplicitly = manifest.dynamic && !isIndexExplicit(lastPeriod); + if (windowChangingImplicitly && manifest.timeShiftBufferDepthMs != C.TIME_UNSET) { + // Update the available start time to reflect the manifest's time shift buffer depth. + long timeShiftBufferStartTimeInManifestUs = + windowEndTimeInManifestUs - C.msToUs(manifest.timeShiftBufferDepthMs); + windowStartTimeInManifestUs = + max(windowStartTimeInManifestUs, timeShiftBufferStartTimeInManifestUs); } - long windowDurationUs = currentEndTimeUs - currentStartTimeUs; - for (int i = 0; i < manifest.getPeriodCount() - 1; i++) { - windowDurationUs += manifest.getPeriodDurationUs(i); - } - - long windowStartTimeMs = C.TIME_UNSET; - if (manifest.availabilityStartTimeMs != C.TIME_UNSET) { - windowStartTimeMs = - manifest.availabilityStartTimeMs - + manifest.getPeriod(0).startMs - + C.usToMs(currentStartTimeUs); - } - - long windowDefaultStartPositionUs = 0; + long windowDurationUs = windowEndTimeInManifestUs - windowStartTimeInManifestUs; + long windowStartUnixTimeMs = C.TIME_UNSET; + long windowDefaultPositionUs = 0; if (manifest.dynamic) { - updateMediaItemLiveConfiguration( - /* nowPeriodTimeUs= */ currentStartTimeUs + nowUnixTimeUs - C.msToUs(windowStartTimeMs), - /* windowStartPeriodTimeUs= */ currentStartTimeUs, - /* windowEndPeriodTimeUs= */ currentEndTimeUs); - windowDefaultStartPositionUs = - nowUnixTimeUs - C.msToUs(windowStartTimeMs + liveConfiguration.targetOffsetMs); - long minimumDefaultStartPositionUs = + checkState(manifest.availabilityStartTimeMs != C.TIME_UNSET); + long nowInWindowUs = + nowUnixTimeUs - C.msToUs(manifest.availabilityStartTimeMs) - windowStartTimeInManifestUs; + updateMediaItemLiveConfiguration(nowInWindowUs, windowDurationUs); + windowStartUnixTimeMs = + manifest.availabilityStartTimeMs + C.usToMs(windowStartTimeInManifestUs); + windowDefaultPositionUs = nowInWindowUs - C.msToUs(liveConfiguration.targetOffsetMs); + long minimumWindowDefaultPositionUs = min(MIN_LIVE_DEFAULT_START_POSITION_US, windowDurationUs / 2); - if (windowDefaultStartPositionUs < minimumDefaultStartPositionUs) { - // The default start position is too close to the start of the live window. Set it to the - // minimum default start position provided the window is at least twice as big. Else set - // it to the middle of the window. - windowDefaultStartPositionUs = minimumDefaultStartPositionUs; + if (windowDefaultPositionUs < minimumWindowDefaultPositionUs) { + // The default position is too close to the start of the live window. Set it to the minimum + // default position provided the window is at least twice as big. Else set it to the middle + // of the window. + windowDefaultPositionUs = minimumWindowDefaultPositionUs; } } + long offsetInFirstPeriodUs = windowStartTimeInManifestUs - C.msToUs(firstPeriod.startMs); DashTimeline timeline = new DashTimeline( manifest.availabilityStartTimeMs, - windowStartTimeMs, + windowStartUnixTimeMs, elapsedRealtimeOffsetMs, firstPeriodId, - /* offsetInFirstPeriodUs= */ currentStartTimeUs, + offsetInFirstPeriodUs, windowDurationUs, - windowDefaultStartPositionUs, + windowDefaultPositionUs, manifest, mediaItem, manifest.dynamic ? liveConfiguration : null); @@ -1008,8 +985,7 @@ public final class DashMediaSource extends BaseMediaSource { } } - private void updateMediaItemLiveConfiguration( - long nowPeriodTimeUs, long windowStartPeriodTimeUs, long windowEndPeriodTimeUs) { + private void updateMediaItemLiveConfiguration(long nowInWindowUs, long windowDurationUs) { long maxLiveOffsetMs; if (mediaItem.liveConfiguration.maxOffsetMs != C.TIME_UNSET) { maxLiveOffsetMs = mediaItem.liveConfiguration.maxOffsetMs; @@ -1017,7 +993,7 @@ public final class DashMediaSource extends BaseMediaSource { && manifest.serviceDescription.maxOffsetMs != C.TIME_UNSET) { maxLiveOffsetMs = manifest.serviceDescription.maxOffsetMs; } else { - maxLiveOffsetMs = C.usToMs(nowPeriodTimeUs - windowStartPeriodTimeUs); + maxLiveOffsetMs = C.usToMs(nowInWindowUs); } long minLiveOffsetMs; if (mediaItem.liveConfiguration.minOffsetMs != C.TIME_UNSET) { @@ -1026,7 +1002,7 @@ public final class DashMediaSource extends BaseMediaSource { && manifest.serviceDescription.minOffsetMs != C.TIME_UNSET) { minLiveOffsetMs = manifest.serviceDescription.minOffsetMs; } else { - minLiveOffsetMs = C.usToMs(nowPeriodTimeUs - windowEndPeriodTimeUs); + minLiveOffsetMs = C.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. @@ -1052,12 +1028,10 @@ public final class DashMediaSource extends BaseMediaSource { targetOffsetMs = minLiveOffsetMs; } if (targetOffsetMs > maxLiveOffsetMs) { - long windowDurationUs = windowEndPeriodTimeUs - windowStartPeriodTimeUs; - long liveOffsetAtWindowStartUs = nowPeriodTimeUs - windowStartPeriodTimeUs; long safeDistanceFromWindowStartUs = min(MIN_LIVE_DEFAULT_START_POSITION_US, windowDurationUs / 2); long maxTargetOffsetForSafeDistanceToWindowStartMs = - C.usToMs(liveOffsetAtWindowStartUs - safeDistanceFromWindowStartUs); + C.usToMs(nowInWindowUs - safeDistanceFromWindowStartUs); targetOffsetMs = Util.constrainValue( maxTargetOffsetForSafeDistanceToWindowStartMs, minLiveOffsetMs, maxLiveOffsetMs); @@ -1147,9 +1121,10 @@ public final class DashMediaSource extends BaseMediaSource { return LongMath.divide(intervalUs, 1000, RoundingMode.CEILING); } - private static long getAvailableStartTimeUs( + private static long getAvailableStartTimeInManifestUs( Period period, long periodDurationUs, long nowUnixTimeUs) { - long availableStartTimeUs = 0; + long periodStartTimeInManifestUs = C.msToUs(period.startMs); + long availableStartTimeInManifestUs = periodStartTimeInManifestUs; boolean haveAudioVideoAdaptationSets = hasVideoOrAudioAdaptationSets(period); for (int i = 0; i < period.adaptationSets.size(); i++) { AdaptationSet adaptationSet = period.adaptationSets.get(i); @@ -1162,23 +1137,26 @@ public final class DashMediaSource extends BaseMediaSource { } @Nullable DashSegmentIndex index = representations.get(0).getIndex(); if (index == null) { - return 0; + return periodStartTimeInManifestUs; } int availableSegmentCount = index.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs); if (availableSegmentCount == 0) { - return 0; + return periodStartTimeInManifestUs; } long firstAvailableSegmentNum = index.getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs); - long adaptationSetAvailableStartTimeUs = index.getTimeUs(firstAvailableSegmentNum); - availableStartTimeUs = max(availableStartTimeUs, adaptationSetAvailableStartTimeUs); + long adaptationSetAvailableStartTimeInManifestUs = + periodStartTimeInManifestUs + index.getTimeUs(firstAvailableSegmentNum); + availableStartTimeInManifestUs = + max(availableStartTimeInManifestUs, adaptationSetAvailableStartTimeInManifestUs); } - return availableStartTimeUs; + return availableStartTimeInManifestUs; } - private static long getAvailableEndTimeUs( + private static long getAvailableEndTimeInManifestUs( Period period, long periodDurationUs, long nowUnixTimeUs) { - long availableEndTimeUs = Long.MAX_VALUE; + long periodStartTimeInManifestUs = C.msToUs(period.startMs); + long availableEndTimeInManifestUs = Long.MAX_VALUE; boolean haveAudioVideoAdaptationSets = hasVideoOrAudioAdaptationSets(period); for (int i = 0; i < period.adaptationSets.size(); i++) { AdaptationSet adaptationSet = period.adaptationSets.get(i); @@ -1191,21 +1169,23 @@ public final class DashMediaSource extends BaseMediaSource { } @Nullable DashSegmentIndex index = representations.get(0).getIndex(); if (index == null) { - return periodDurationUs; + return periodStartTimeInManifestUs + periodDurationUs; } int availableSegmentCount = index.getAvailableSegmentCount(periodDurationUs, nowUnixTimeUs); if (availableSegmentCount == 0) { - return 0; + return periodStartTimeInManifestUs; } long firstAvailableSegmentNum = index.getFirstAvailableSegmentNum(periodDurationUs, nowUnixTimeUs); long lastAvailableSegmentNum = firstAvailableSegmentNum + availableSegmentCount - 1; - long adaptationSetAvailableEndTimeUs = - index.getTimeUs(lastAvailableSegmentNum) + long adaptationSetAvailableEndTimeInManifestUs = + periodStartTimeInManifestUs + + index.getTimeUs(lastAvailableSegmentNum) + index.getDurationUs(lastAvailableSegmentNum, periodDurationUs); - availableEndTimeUs = min(availableEndTimeUs, adaptationSetAvailableEndTimeUs); + availableEndTimeInManifestUs = + min(availableEndTimeInManifestUs, adaptationSetAvailableEndTimeInManifestUs); } - return availableEndTimeUs; + return availableEndTimeInManifestUs; } private static boolean isIndexExplicit(Period period) { diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java index b472aed50c..b5b852ed7e 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java @@ -30,9 +30,7 @@ public class Period { */ @Nullable public final String id; - /** - * The start time of the period in milliseconds. - */ + /** The start time of the period in milliseconds, relative to the start of the manifest. */ public final long startMs; /**