Add large renderer position offset.

This helps to prevent issues where decoders can't handle negative
timestamps. In particular it avoids issues when the media accidentally
or intentionally starts with small negative timestamps. But it also
helps to prevent other renderer resets at a later point, for example
if a live stream with a large start offset is enqueued in the playlist.

#minor-release

PiperOrigin-RevId: 406786977
This commit is contained in:
tonihei 2021-11-01 10:23:58 +00:00 committed by Ian Baker
parent 7115058ccd
commit 10dcdd1df5
9 changed files with 127 additions and 79 deletions

View File

@ -1274,7 +1274,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
queue.advancePlayingPeriod(); queue.advancePlayingPeriod();
} }
queue.removeAfter(newPlayingPeriodHolder); queue.removeAfter(newPlayingPeriodHolder);
newPlayingPeriodHolder.setRendererOffset(/* rendererPositionOffsetUs= */ 0); newPlayingPeriodHolder.setRendererOffset(
MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US);
enableRenderers(); enableRenderers();
} }
} }
@ -1307,7 +1308,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
MediaPeriodHolder playingMediaPeriod = queue.getPlayingPeriod(); MediaPeriodHolder playingMediaPeriod = queue.getPlayingPeriod();
rendererPositionUs = rendererPositionUs =
playingMediaPeriod == null playingMediaPeriod == null
? periodPositionUs ? MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US + periodPositionUs
: playingMediaPeriod.toRendererTime(periodPositionUs); : playingMediaPeriod.toRendererTime(periodPositionUs);
mediaClock.resetPosition(rendererPositionUs); mediaClock.resetPosition(rendererPositionUs);
for (Renderer renderer : renderers) { for (Renderer renderer : renderers) {
@ -1383,7 +1384,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
pendingRecoverableRendererError = null; pendingRecoverableRendererError = null;
isRebuffering = false; isRebuffering = false;
mediaClock.stop(); mediaClock.stop();
rendererPositionUs = 0; rendererPositionUs = MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US;
for (Renderer renderer : renderers) { for (Renderer renderer : renderers) {
try { try {
disableRenderer(renderer); disableRenderer(renderer);
@ -1971,7 +1972,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
emptyTrackSelectorResult); emptyTrackSelectorResult);
mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs); mediaPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs);
if (queue.getPlayingPeriod() == mediaPeriodHolder) { if (queue.getPlayingPeriod() == mediaPeriodHolder) {
resetRendererPosition(mediaPeriodHolder.getStartPositionRendererTime()); resetRendererPosition(info.startPositionUs);
} }
handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
} }

View File

@ -39,6 +39,26 @@ import com.google.common.collect.ImmutableList;
*/ */
/* package */ final class MediaPeriodQueue { /* package */ final class MediaPeriodQueue {
/**
* Initial renderer position offset used for the first item in the queue, in microseconds.
*
* <p>Choosing a positive value, larger than any reasonable single media duration, ensures three
* things:
*
* <ul>
* <li>Media that accidentally or intentionally starts with small negative timestamps doesn't
* send samples with negative timestamps to decoders. This makes rendering more robust as
* many decoders are known to have problems with negative timestamps.
* <li>Enqueueing media after the initial item with a non-zero start offset (e.g. content after
* ad breaks or live streams) is virtually guaranteed to stay in the positive timestamp
* range even when seeking back. This prevents renderer resets that are required if the
* allowed timestamp range may become negative.
* <li>Choosing a large value with zeros at all relevant digits simplifies debugging as the
* original timestamp of the media is still visible.
* </ul>
*/
public static final long INITIAL_RENDERER_POSITION_OFFSET_US = 1_000_000_000_000L;
/** /**
* Limits the maximum number of periods to buffer ahead of the current playing period. The * Limits the maximum number of periods to buffer ahead of the current playing period. The
* buffering policy normally prevents buffering too far ahead, but the policy could allow too many * buffering policy normally prevents buffering too far ahead, but the policy could allow too many
@ -165,9 +185,7 @@ import com.google.common.collect.ImmutableList;
TrackSelectorResult emptyTrackSelectorResult) { TrackSelectorResult emptyTrackSelectorResult) {
long rendererPositionOffsetUs = long rendererPositionOffsetUs =
loading == null loading == null
? (info.id.isAd() && info.requestedContentPositionUs != C.TIME_UNSET ? INITIAL_RENDERER_POSITION_OFFSET_US
? info.requestedContentPositionUs
: 0)
: (loading.getRendererOffset() + loading.info.durationUs - info.startPositionUs); : (loading.getRendererOffset() + loading.info.durationUs - info.startPositionUs);
MediaPeriodHolder newPeriodHolder = MediaPeriodHolder newPeriodHolder =
new MediaPeriodHolder( new MediaPeriodHolder(

View File

@ -499,10 +499,13 @@ public final class MediaPeriodQueueTest {
// Change position of first ad (= change duration of playing content before first ad). // Change position of first ad (= change duration of playing content before first ad).
updateAdPlaybackStateAndTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US - 2000); updateAdPlaybackStateAndTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US - 2000);
setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0);
long maxRendererReadPositionUs = FIRST_AD_START_TIME_US - 3000; long maxRendererReadPositionUs =
MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US + FIRST_AD_START_TIME_US - 3000;
boolean changeHandled = boolean changeHandled =
mediaPeriodQueue.updateQueuedPeriods( mediaPeriodQueue.updateQueuedPeriods(
playbackInfo.timeline, /* rendererPositionUs= */ 0, maxRendererReadPositionUs); playbackInfo.timeline,
/* rendererPositionUs= */ MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US,
maxRendererReadPositionUs);
assertThat(changeHandled).isTrue(); assertThat(changeHandled).isTrue();
assertThat(getQueueLength()).isEqualTo(1); assertThat(getQueueLength()).isEqualTo(1);
@ -524,10 +527,13 @@ public final class MediaPeriodQueueTest {
// Change position of first ad (= change duration of playing content before first ad). // Change position of first ad (= change duration of playing content before first ad).
updateAdPlaybackStateAndTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US - 2000); updateAdPlaybackStateAndTimeline(/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US - 2000);
setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0);
long maxRendererReadPositionUs = FIRST_AD_START_TIME_US - 1000; long maxRendererReadPositionUs =
MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US + FIRST_AD_START_TIME_US - 1000;
boolean changeHandled = boolean changeHandled =
mediaPeriodQueue.updateQueuedPeriods( mediaPeriodQueue.updateQueuedPeriods(
playbackInfo.timeline, /* rendererPositionUs= */ 0, maxRendererReadPositionUs); playbackInfo.timeline,
/* rendererPositionUs= */ MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US,
maxRendererReadPositionUs);
assertThat(changeHandled).isFalse(); assertThat(changeHandled).isFalse();
assertThat(getQueueLength()).isEqualTo(1); assertThat(getQueueLength()).isEqualTo(1);
@ -558,10 +564,13 @@ public final class MediaPeriodQueueTest {
.withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true); .withIsServerSideInserted(/* adGroupIndex= */ 0, /* isServerSideInserted= */ true);
updateTimeline(); updateTimeline();
setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0);
long maxRendererReadPositionUs = FIRST_AD_START_TIME_US - 1000; long maxRendererReadPositionUs =
MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US + FIRST_AD_START_TIME_US - 1000;
boolean changeHandled = boolean changeHandled =
mediaPeriodQueue.updateQueuedPeriods( mediaPeriodQueue.updateQueuedPeriods(
playbackInfo.timeline, /* rendererPositionUs= */ 0, maxRendererReadPositionUs); playbackInfo.timeline,
/* rendererPositionUs= */ MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US,
maxRendererReadPositionUs);
assertThat(changeHandled).isTrue(); assertThat(changeHandled).isTrue();
assertThat(getQueueLength()).isEqualTo(1); assertThat(getQueueLength()).isEqualTo(1);
@ -589,7 +598,9 @@ public final class MediaPeriodQueueTest {
setAdGroupLoaded(/* adGroupIndex= */ 1); setAdGroupLoaded(/* adGroupIndex= */ 1);
boolean changeHandled = boolean changeHandled =
mediaPeriodQueue.updateQueuedPeriods( mediaPeriodQueue.updateQueuedPeriods(
playbackInfo.timeline, /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ 0); playbackInfo.timeline,
/* rendererPositionUs= */ MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US,
/* maxRendererReadPositionUs= */ MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US);
assertThat(changeHandled).isTrue(); assertThat(changeHandled).isTrue();
assertThat(getQueueLength()).isEqualTo(3); assertThat(getQueueLength()).isEqualTo(3);
@ -614,11 +625,13 @@ public final class MediaPeriodQueueTest {
/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000); /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000);
setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 1); setAdGroupLoaded(/* adGroupIndex= */ 1);
long maxRendererReadPositionUs =
MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US + FIRST_AD_START_TIME_US;
boolean changeHandled = boolean changeHandled =
mediaPeriodQueue.updateQueuedPeriods( mediaPeriodQueue.updateQueuedPeriods(
playbackInfo.timeline, playbackInfo.timeline,
/* rendererPositionUs= */ 0, /* rendererPositionUs= */ MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US,
/* maxRendererReadPositionUs= */ FIRST_AD_START_TIME_US); maxRendererReadPositionUs);
assertThat(changeHandled).isFalse(); assertThat(changeHandled).isFalse();
assertThat(getQueueLength()).isEqualTo(3); assertThat(getQueueLength()).isEqualTo(3);
@ -642,11 +655,14 @@ public final class MediaPeriodQueueTest {
/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000); /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000);
setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 1); setAdGroupLoaded(/* adGroupIndex= */ 1);
long readingPositionAtStartOfContentBetweenAds = FIRST_AD_START_TIME_US + AD_DURATION_US; long readingPositionAtStartOfContentBetweenAds =
MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US
+ FIRST_AD_START_TIME_US
+ AD_DURATION_US;
boolean changeHandled = boolean changeHandled =
mediaPeriodQueue.updateQueuedPeriods( mediaPeriodQueue.updateQueuedPeriods(
playbackInfo.timeline, playbackInfo.timeline,
/* rendererPositionUs= */ 0, /* rendererPositionUs= */ MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US,
/* maxRendererReadPositionUs= */ readingPositionAtStartOfContentBetweenAds); /* maxRendererReadPositionUs= */ readingPositionAtStartOfContentBetweenAds);
assertThat(changeHandled).isTrue(); assertThat(changeHandled).isTrue();
@ -671,11 +687,14 @@ public final class MediaPeriodQueueTest {
/* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000); /* adGroupTimesUs...= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US - 1000);
setAdGroupLoaded(/* adGroupIndex= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0);
setAdGroupLoaded(/* adGroupIndex= */ 1); setAdGroupLoaded(/* adGroupIndex= */ 1);
long readingPositionAtEndOfContentBetweenAds = SECOND_AD_START_TIME_US + AD_DURATION_US; long readingPositionAtEndOfContentBetweenAds =
MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US
+ SECOND_AD_START_TIME_US
+ AD_DURATION_US;
boolean changeHandled = boolean changeHandled =
mediaPeriodQueue.updateQueuedPeriods( mediaPeriodQueue.updateQueuedPeriods(
playbackInfo.timeline, playbackInfo.timeline,
/* rendererPositionUs= */ 0, /* rendererPositionUs= */ MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US,
/* maxRendererReadPositionUs= */ readingPositionAtEndOfContentBetweenAds); /* maxRendererReadPositionUs= */ readingPositionAtEndOfContentBetweenAds);
assertThat(changeHandled).isFalse(); assertThat(changeHandled).isFalse();
@ -703,7 +722,7 @@ public final class MediaPeriodQueueTest {
boolean changeHandled = boolean changeHandled =
mediaPeriodQueue.updateQueuedPeriods( mediaPeriodQueue.updateQueuedPeriods(
playbackInfo.timeline, playbackInfo.timeline,
/* rendererPositionUs= */ 0, /* rendererPositionUs= */ MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US,
/* maxRendererReadPositionUs= */ C.TIME_END_OF_SOURCE); /* maxRendererReadPositionUs= */ C.TIME_END_OF_SOURCE);
assertThat(changeHandled).isFalse(); assertThat(changeHandled).isFalse();

View File

@ -3,89 +3,89 @@ config:
channelCount = 2 channelCount = 2
sampleRate = 48000 sampleRate = 48000
buffer: buffer:
time = 1000 time = 1000000001000
data = 1217833679 data = 1217833679
buffer: buffer:
time = 97000 time = 1000000097000
data = 558614672 data = 558614672
buffer: buffer:
time = 193000 time = 1000000193000
data = -709714787 data = -709714787
buffer: buffer:
time = 289000 time = 1000000289000
data = 1367870571 data = 1367870571
buffer: buffer:
time = 385000 time = 1000000385000
data = -141229457 data = -141229457
buffer: buffer:
time = 481000 time = 1000000481000
data = 1287758361 data = 1287758361
buffer: buffer:
time = 577000 time = 1000000577000
data = 1125289147 data = 1125289147
buffer: buffer:
time = 673000 time = 1000000673000
data = -1677383475 data = -1677383475
buffer: buffer:
time = 769000 time = 1000000769000
data = 2130742861 data = 2130742861
buffer: buffer:
time = 865000 time = 1000000865000
data = -1292320253 data = -1292320253
buffer: buffer:
time = 961000 time = 1000000961000
data = -456587163 data = -456587163
buffer: buffer:
time = 1057000 time = 1000001057000
data = 748981534 data = 748981534
buffer: buffer:
time = 1153000 time = 1000001153000
data = 1550456016 data = 1550456016
buffer: buffer:
time = 1249000 time = 1000001249000
data = 1657906039 data = 1657906039
buffer: buffer:
time = 1345000 time = 1000001345000
data = -762677083 data = -762677083
buffer: buffer:
time = 1441000 time = 1000001441000
data = -1343810763 data = -1343810763
buffer: buffer:
time = 1537000 time = 1000001537000
data = 1137318783 data = 1137318783
buffer: buffer:
time = 1633000 time = 1000001633000
data = -1891318229 data = -1891318229
buffer: buffer:
time = 1729000 time = 1000001729000
data = -472068495 data = -472068495
buffer: buffer:
time = 1825000 time = 1000001825000
data = 832315001 data = 832315001
buffer: buffer:
time = 1921000 time = 1000001921000
data = 2054935175 data = 2054935175
buffer: buffer:
time = 2017000 time = 1000002017000
data = 57921641 data = 57921641
buffer: buffer:
time = 2113000 time = 1000002113000
data = 2132759067 data = 2132759067
buffer: buffer:
time = 2209000 time = 1000002209000
data = -1742540521 data = -1742540521
buffer: buffer:
time = 2305000 time = 1000002305000
data = 1657024301 data = 1657024301
buffer: buffer:
time = 2401000 time = 1000002401000
data = -585080145 data = -585080145
buffer: buffer:
time = 2497000 time = 1000002497000
data = 427271397 data = 427271397
buffer: buffer:
time = 2593000 time = 1000002593000
data = -364201340 data = -364201340
buffer: buffer:
time = 2689000 time = 1000002689000
data = -627965287 data = -627965287

View File

@ -3,89 +3,89 @@ config:
channelCount = 2 channelCount = 2
sampleRate = 48000 sampleRate = 48000
buffer: buffer:
time = 0 time = 1000000000000
data = 225023649 data = 225023649
buffer: buffer:
time = 96000 time = 1000000096000
data = 455106306 data = 455106306
buffer: buffer:
time = 192000 time = 1000000192000
data = 2025727297 data = 2025727297
buffer: buffer:
time = 288000 time = 1000000288000
data = 758514657 data = 758514657
buffer: buffer:
time = 384000 time = 1000000384000
data = 1044986473 data = 1044986473
buffer: buffer:
time = 480000 time = 1000000480000
data = -2030029695 data = -2030029695
buffer: buffer:
time = 576000 time = 1000000576000
data = 1907053281 data = 1907053281
buffer: buffer:
time = 672000 time = 1000000672000
data = -1974954431 data = -1974954431
buffer: buffer:
time = 768000 time = 1000000768000
data = -206248383 data = -206248383
buffer: buffer:
time = 864000 time = 1000000864000
data = 1484984417 data = 1484984417
buffer: buffer:
time = 960000 time = 1000000960000
data = -1306117439 data = -1306117439
buffer: buffer:
time = 1056000 time = 1000001056000
data = 692829792 data = 692829792
buffer: buffer:
time = 1152000 time = 1000001152000
data = 1070563058 data = 1070563058
buffer: buffer:
time = 1248000 time = 1000001248000
data = -1444096479 data = -1444096479
buffer: buffer:
time = 1344000 time = 1000001344000
data = 1753016419 data = 1753016419
buffer: buffer:
time = 1440000 time = 1000001440000
data = 1947797953 data = 1947797953
buffer: buffer:
time = 1536000 time = 1000001536000
data = 266121411 data = 266121411
buffer: buffer:
time = 1632000 time = 1000001632000
data = 1275494369 data = 1275494369
buffer: buffer:
time = 1728000 time = 1000001728000
data = 372077825 data = 372077825
buffer: buffer:
time = 1824000 time = 1000001824000
data = -993079679 data = -993079679
buffer: buffer:
time = 1920000 time = 1000001920000
data = 177307937 data = 177307937
buffer: buffer:
time = 2016000 time = 1000002016000
data = 2037083009 data = 2037083009
buffer: buffer:
time = 2112000 time = 1000002112000
data = -435776287 data = -435776287
buffer: buffer:
time = 2208000 time = 1000002208000
data = 1867447329 data = 1867447329
buffer: buffer:
time = 2304000 time = 1000002304000
data = 1884495937 data = 1884495937
buffer: buffer:
time = 2400000 time = 1000002400000
data = -804673375 data = -804673375
buffer: buffer:
time = 2496000 time = 1000002496000
data = -588531007 data = -588531007
buffer: buffer:
time = 2592000 time = 1000002592000
data = -1064642970 data = -1064642970
buffer: buffer:
time = 2688000 time = 1000002688000
data = -1771406207 data = -1771406207

View File

@ -279,6 +279,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
int result = readSource(getFormatHolder(), decoderInputBuffer, /* readFlags= */ 0); int result = readSource(getFormatHolder(), decoderInputBuffer, /* readFlags= */ 0);
switch (result) { switch (result) {
case C.RESULT_BUFFER_READ: case C.RESULT_BUFFER_READ:
decoderInputBuffer.timeUs -= streamOffsetUs;
mediaClock.updateTimeForTrackType(getTrackType(), decoderInputBuffer.timeUs); mediaClock.updateTimeForTrackType(getTrackType(), decoderInputBuffer.timeUs);
decoderInputBuffer.flip(); decoderInputBuffer.flip();
decoder.queueInputBuffer(decoderInputBuffer); decoder.queueInputBuffer(decoderInputBuffer);

View File

@ -34,6 +34,7 @@ import androidx.media3.exoplayer.RendererCapabilities;
protected final Transformation transformation; protected final Transformation transformation;
protected boolean isRendererStarted; protected boolean isRendererStarted;
protected long streamOffsetUs;
public TransformerBaseRenderer( public TransformerBaseRenderer(
int trackType, int trackType,
@ -46,6 +47,12 @@ import androidx.media3.exoplayer.RendererCapabilities;
this.transformation = transformation; this.transformation = transformation;
} }
@Override
protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs)
throws ExoPlaybackException {
this.streamOffsetUs = offsetUs;
}
@Override @Override
@C.FormatSupport @C.FormatSupport
public final int supportsFormat(Format format) { public final int supportsFormat(Format format) {

View File

@ -117,6 +117,7 @@ import java.nio.ByteBuffer;
muxerWrapper.endTrack(getTrackType()); muxerWrapper.endTrack(getTrackType());
return false; return false;
} }
buffer.timeUs -= streamOffsetUs;
mediaClock.updateTimeForTrackType(getTrackType(), buffer.timeUs); mediaClock.updateTimeForTrackType(getTrackType(), buffer.timeUs);
ByteBuffer data = checkNotNull(buffer.data); ByteBuffer data = checkNotNull(buffer.data);
data.flip(); data.flip();

View File

@ -320,6 +320,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
case C.RESULT_FORMAT_READ: case C.RESULT_FORMAT_READ:
throw new IllegalStateException("Format changes are not supported."); throw new IllegalStateException("Format changes are not supported.");
case C.RESULT_BUFFER_READ: case C.RESULT_BUFFER_READ:
decoderInputBuffer.timeUs -= streamOffsetUs;
mediaClock.updateTimeForTrackType(getTrackType(), decoderInputBuffer.timeUs); mediaClock.updateTimeForTrackType(getTrackType(), decoderInputBuffer.timeUs);
ByteBuffer data = checkNotNull(decoderInputBuffer.data); ByteBuffer data = checkNotNull(decoderInputBuffer.data);
data.flip(); data.flip();