Report initial discontinuity for DASH periods that require preroll

DASH periods don't have to start at the beginning of a segment. In
these cases, they should report an initial discontinuity to let the
player know it needs to expect preroll data (e.g. to flush renderers)

This information is only available in the ChunkSampleStream after
loading the initialization data, so we need to check the sample
streams and tell them to only report discontinuities at the very
beginning of playback. All other position resets are triggered by
the player itself and don't need this method.

Issue: androidx/media#1440
PiperOrigin-RevId: 668831563
This commit is contained in:
tonihei 2024-08-29 01:54:10 -07:00 committed by Copybara-Service
parent 8367e420ad
commit e8664dbc8e
9 changed files with 421 additions and 5 deletions

View File

@ -101,6 +101,9 @@
* Cronet Extension:
* RTMP Extension:
* HLS Extension:
* DASH Extension:
* Add support for periods starting in the middle of a segment
([#1440](https://github.com/androidx/media/issues/1440)).
* Smooth Streaming Extension:
* RTSP Extension:
* Decoder Extensions (FFmpeg, VP9, AV1, etc.):

View File

@ -22,6 +22,7 @@ import static java.lang.Math.min;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
@ -95,6 +96,8 @@ public class ChunkSampleStream<T extends ChunkSource>
private long lastSeekPositionUs;
private int nextNotifyPrimaryFormatMediaChunkIndex;
@Nullable private BaseMediaChunk canceledMediaChunk;
private boolean canReportInitialDiscontinuity;
private boolean hasInitialDiscontinuity;
/* package */ boolean loadingFinished;
@ -114,6 +117,8 @@ public class ChunkSampleStream<T extends ChunkSource>
* @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy}.
* @param mediaSourceEventDispatcher A dispatcher to notify of {@link MediaSourceEventListener}
* events.
* @param canReportInitialDiscontinuity Whether the stream can report an initial discontinuity if
* the first chunk can't start at the beginning and needs to preroll data.
*/
public ChunkSampleStream(
@C.TrackType int primaryTrackType,
@ -126,7 +131,8 @@ public class ChunkSampleStream<T extends ChunkSource>
DrmSessionManager drmSessionManager,
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher) {
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
boolean canReportInitialDiscontinuity) {
this.primaryTrackType = primaryTrackType;
this.embeddedTrackTypes = embeddedTrackTypes == null ? new int[0] : embeddedTrackTypes;
this.embeddedTrackFormats = embeddedTrackFormats == null ? new Format[0] : embeddedTrackFormats;
@ -134,6 +140,7 @@ public class ChunkSampleStream<T extends ChunkSource>
this.callback = callback;
this.mediaSourceEventDispatcher = mediaSourceEventDispatcher;
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
this.canReportInitialDiscontinuity = canReportInitialDiscontinuity;
loader = new Loader("ChunkSampleStream");
nextChunkHolder = new ChunkHolder();
mediaChunks = new ArrayList<>();
@ -258,6 +265,7 @@ public class ChunkSampleStream<T extends ChunkSource>
*/
public void seekToUs(long positionUs) {
lastSeekPositionUs = positionUs;
canReportInitialDiscontinuity = false;
if (isPendingReset()) {
// A reset is already pending. We only need to update its position.
pendingResetPositionUs = positionUs;
@ -600,12 +608,22 @@ public class ChunkSampleStream<T extends ChunkSource>
// seeking to a chunk boundary then we want the queue to pass through all of the samples in
// the chunk. Doing this ensures we'll always output the keyframe at the start of the chunk,
// even if its timestamp is slightly earlier than the advertised chunk start time.
if (mediaChunk.startTimeUs != pendingResetPositionUs) {
if (mediaChunk.startTimeUs < pendingResetPositionUs) {
primarySampleQueue.setStartTimeUs(pendingResetPositionUs);
for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
embeddedSampleQueue.setStartTimeUs(pendingResetPositionUs);
}
if (canReportInitialDiscontinuity) {
// Only report it as discontinuity if the SampleQueue can't skip the samples directly.
boolean sampleQueueCanSkipSamples =
MimeTypes.allSamplesAreSyncSamples(
mediaChunk.trackFormat.sampleMimeType, mediaChunk.trackFormat.codecs);
hasInitialDiscontinuity = !sampleQueueCanSkipSamples;
}
}
// Once we started loading the first media chunk, no more initial discontinuities can be
// reported.
canReportInitialDiscontinuity = false;
pendingResetPositionUs = C.TIME_UNSET;
}
mediaChunk.init(chunkOutput);
@ -670,6 +688,19 @@ public class ChunkSampleStream<T extends ChunkSource>
}
}
/**
* Consumes a pending initial discontinuity.
*
* @return Whether the stream had an initial discontinuity.
*/
public boolean consumeInitialDiscontinuity() {
try {
return hasInitialDiscontinuity;
} finally {
hasInitialDiscontinuity = false;
}
}
private void discardUpstream(int preferredQueueSize) {
Assertions.checkState(!loader.isLoading());

View File

@ -114,6 +114,8 @@ import java.util.regex.Pattern;
private DashManifest manifest;
private int periodIndex;
private List<EventStream> eventStreams;
private boolean canReportInitialDiscontinuity;
private long initialStartTimeUs;
public DashMediaPeriod(
int id,
@ -149,6 +151,7 @@ import java.util.regex.Pattern;
this.allocator = allocator;
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
this.playerId = playerId;
this.canReportInitialDiscontinuity = true;
playerEmsgHandler = new PlayerEmsgHandler(manifest, playerEmsgCallback, allocator);
sampleStreams = newSampleStreamArray(0);
eventSampleStreams = new EventSampleStream[0];
@ -305,6 +308,10 @@ import java.util.regex.Pattern;
compositeSequenceableLoaderFactory.create(
sampleStreamList,
Lists.transform(sampleStreamList, s -> ImmutableList.of(s.primaryTrackType)));
if (canReportInitialDiscontinuity) {
canReportInitialDiscontinuity = false;
initialStartTimeUs = positionUs;
}
return positionUs;
}
@ -337,6 +344,11 @@ import java.util.regex.Pattern;
@Override
public long readDiscontinuity() {
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
if (sampleStream.consumeInitialDiscontinuity()) {
return initialStartTimeUs;
}
}
return C.TIME_UNSET;
}
@ -824,7 +836,8 @@ import java.util.regex.Pattern;
drmSessionManager,
drmEventDispatcher,
loadErrorHandlingPolicy,
mediaSourceEventDispatcher);
mediaSourceEventDispatcher,
canReportInitialDiscontinuity);
synchronized (this) {
// The map is also accessed on the loading thread so synchronize access.
trackEmsgHandlerBySampleStream.put(stream, trackPlayerEmsgHandler);

View File

@ -440,4 +440,27 @@ public final class DashPlaybackTest {
// TODO: b/352276461 - The last frame might not be rendered. When the bug is fixed,
// assert on the full playback dump.
}
@Test
public void multiPeriod_withOffsetInSegment() throws Exception {
Context applicationContext = ApplicationProvider.getApplicationContext();
CapturingRenderersFactory capturingRenderersFactory =
new CapturingRenderersFactory(applicationContext);
ExoPlayer player =
new ExoPlayer.Builder(applicationContext, capturingRenderersFactory)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1)));
PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory);
player.setMediaItem(
MediaItem.fromUri("asset:///media/dash/multi-period-with-offset/sample.mpd"));
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
DumpFileAsserts.assertOutput(
applicationContext, playbackOutput, "playbackdumps/dash/multi-period-with-offset.dump");
}
}

View File

@ -264,7 +264,8 @@ import java.util.List;
drmSessionManager,
drmEventDispatcher,
loadErrorHandlingPolicy,
mediaSourceEventDispatcher);
mediaSourceEventDispatcher,
/* canReportInitialDiscontinuity= */ false);
}
private static TrackGroupArray buildTrackGroups(

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" minBufferTime="PT2S" type="static" mediaPresentationDuration="PT1.0010000467300415S">
<Period id='0' duration='PT0.5S'>
<AdaptationSet id="1" contentType="video" width="1080" height="720" frameRate="30000/1001" subsegmentAlignment="true" par="3:2">
<Representation id="1" bandwidth="721967" codecs="avc1.64001f" mimeType="video/mp4" sar="1:1">
<BaseURL>sample.video.mp4</BaseURL>
<SegmentBase indexRange="862-905" timescale="30000">
<Initialization range="0-861"/>
</SegmentBase>
</Representation>
</AdaptationSet>
</Period>
<Period id='1'>
<AdaptationSet id="1" contentType="video" width="1080" height="720" frameRate="30000/1001" subsegmentAlignment="true" par="3:2">
<Representation id="1" bandwidth="721967" codecs="avc1.64001f" mimeType="video/mp4" sar="1:1">
<BaseURL>sample.video.mp4</BaseURL>
<SegmentBase indexRange="862-905" timescale="30000" presentationTimeOffset="15000">
<Initialization range="0-861"/>
</SegmentBase>
</Representation>
</AdaptationSet>
</Period>
</MPD>

View File

@ -0,0 +1,321 @@
MediaCodecAdapter (exotest.video.avc):
inputBuffers:
count = 46
input buffer #0:
timeUs = 1000000000000
contents = length 36692, hash D216076E
input buffer #1:
timeUs = 1000000066733
contents = length 5312, hash D45D3CA0
input buffer #2:
timeUs = 1000000033366
contents = length 599, hash 1BE7812D
input buffer #3:
timeUs = 1000000200200
contents = length 7735, hash 4490F110
input buffer #4:
timeUs = 1000000133466
contents = length 987, hash 560B5036
input buffer #5:
timeUs = 1000000100100
contents = length 673, hash ED7CD8C7
input buffer #6:
timeUs = 1000000166833
contents = length 523, hash 3020DF50
input buffer #7:
timeUs = 1000000333666
contents = length 6061, hash 736C72B2
input buffer #8:
timeUs = 1000000266933
contents = length 992, hash FE132F23
input buffer #9:
timeUs = 1000000233566
contents = length 623, hash 5B2C1816
input buffer #10:
timeUs = 1000000300300
contents = length 421, hash 742E69C1
input buffer #11:
timeUs = 1000000433766
contents = length 4899, hash F72F86A1
input buffer #12:
timeUs = 1000000400400
contents = length 568, hash 519A8E50
input buffer #13:
timeUs = 1000000367033
contents = length 620, hash 3990AA39
input buffer #14:
timeUs = 0
flags = 4
contents = length 0, hash 1
input buffer #15:
timeUs = 1000000000000
contents = length 36692, hash D216076E
input buffer #16:
timeUs = 1000000066733
contents = length 5312, hash D45D3CA0
input buffer #17:
timeUs = 1000000033366
contents = length 599, hash 1BE7812D
input buffer #18:
timeUs = 1000000200200
contents = length 7735, hash 4490F110
input buffer #19:
timeUs = 1000000133466
contents = length 987, hash 560B5036
input buffer #20:
timeUs = 1000000100100
contents = length 673, hash ED7CD8C7
input buffer #21:
timeUs = 1000000166833
contents = length 523, hash 3020DF50
input buffer #22:
timeUs = 1000000333666
contents = length 6061, hash 736C72B2
input buffer #23:
timeUs = 1000000266933
contents = length 992, hash FE132F23
input buffer #24:
timeUs = 1000000233566
contents = length 623, hash 5B2C1816
input buffer #25:
timeUs = 1000000300300
contents = length 421, hash 742E69C1
input buffer #26:
timeUs = 1000000433766
contents = length 4899, hash F72F86A1
input buffer #27:
timeUs = 1000000400400
contents = length 568, hash 519A8E50
input buffer #28:
timeUs = 1000000367033
contents = length 620, hash 3990AA39
input buffer #29:
timeUs = 1000000567233
contents = length 5450, hash F06EC4AA
input buffer #30:
timeUs = 1000000500500
contents = length 1051, hash 92DFA63A
input buffer #31:
timeUs = 1000000467133
contents = length 874, hash 69587FB4
input buffer #32:
timeUs = 1000000533866
contents = length 781, hash 36BE495B
input buffer #33:
timeUs = 1000000700700
contents = length 4725, hash AC0C8CD3
input buffer #34:
timeUs = 1000000633966
contents = length 1022, hash 5D8BFF34
input buffer #35:
timeUs = 1000000600600
contents = length 790, hash 99413A99
input buffer #36:
timeUs = 1000000667333
contents = length 610, hash 5E129290
input buffer #37:
timeUs = 1000000834166
contents = length 2751, hash 769974CB
input buffer #38:
timeUs = 1000000767433
contents = length 745, hash B78A477A
input buffer #39:
timeUs = 1000000734066
contents = length 621, hash CF741E7A
input buffer #40:
timeUs = 1000000800800
contents = length 505, hash 1DB4894E
input buffer #41:
timeUs = 1000000967633
contents = length 1268, hash C15348DC
input buffer #42:
timeUs = 1000000900900
contents = length 880, hash C2DE85D0
input buffer #43:
timeUs = 1000000867533
contents = length 530, hash C98BC6A8
input buffer #44:
timeUs = 1000000934266
contents = length 568, hash 4FE5C8EA
input buffer #45:
timeUs = 0
flags = 4
contents = length 0, hash 1
outputBuffers:
count = 44
output buffer #0:
timeUs = 1000000000000
size = 36692
rendered = true
output buffer #1:
timeUs = 1000000066733
size = 5312
rendered = true
output buffer #2:
timeUs = 1000000033366
size = 599
rendered = true
output buffer #3:
timeUs = 1000000200200
size = 7735
rendered = true
output buffer #4:
timeUs = 1000000133466
size = 987
rendered = true
output buffer #5:
timeUs = 1000000100100
size = 673
rendered = true
output buffer #6:
timeUs = 1000000166833
size = 523
rendered = true
output buffer #7:
timeUs = 1000000333666
size = 6061
rendered = true
output buffer #8:
timeUs = 1000000266933
size = 992
rendered = true
output buffer #9:
timeUs = 1000000233566
size = 623
rendered = true
output buffer #10:
timeUs = 1000000300300
size = 421
rendered = true
output buffer #11:
timeUs = 1000000433766
size = 4899
rendered = true
output buffer #12:
timeUs = 1000000400400
size = 568
rendered = true
output buffer #13:
timeUs = 1000000367033
size = 620
rendered = true
output buffer #14:
timeUs = 1000000000000
size = 36692
rendered = false
output buffer #15:
timeUs = 1000000066733
size = 5312
rendered = false
output buffer #16:
timeUs = 1000000033366
size = 599
rendered = false
output buffer #17:
timeUs = 1000000200200
size = 7735
rendered = false
output buffer #18:
timeUs = 1000000133466
size = 987
rendered = false
output buffer #19:
timeUs = 1000000100100
size = 673
rendered = false
output buffer #20:
timeUs = 1000000166833
size = 523
rendered = false
output buffer #21:
timeUs = 1000000333666
size = 6061
rendered = false
output buffer #22:
timeUs = 1000000266933
size = 992
rendered = false
output buffer #23:
timeUs = 1000000233566
size = 623
rendered = false
output buffer #24:
timeUs = 1000000300300
size = 421
rendered = false
output buffer #25:
timeUs = 1000000433766
size = 4899
rendered = false
output buffer #26:
timeUs = 1000000400400
size = 568
rendered = false
output buffer #27:
timeUs = 1000000367033
size = 620
rendered = false
output buffer #28:
timeUs = 1000000567233
size = 5450
rendered = true
output buffer #29:
timeUs = 1000000500500
size = 1051
rendered = true
output buffer #30:
timeUs = 1000000467133
size = 874
rendered = false
output buffer #31:
timeUs = 1000000533866
size = 781
rendered = true
output buffer #32:
timeUs = 1000000700700
size = 4725
rendered = true
output buffer #33:
timeUs = 1000000633966
size = 1022
rendered = true
output buffer #34:
timeUs = 1000000600600
size = 790
rendered = true
output buffer #35:
timeUs = 1000000667333
size = 610
rendered = true
output buffer #36:
timeUs = 1000000834166
size = 2751
rendered = true
output buffer #37:
timeUs = 1000000767433
size = 745
rendered = true
output buffer #38:
timeUs = 1000000734066
size = 621
rendered = true
output buffer #39:
timeUs = 1000000800800
size = 505
rendered = true
output buffer #40:
timeUs = 1000000967633
size = 1268
rendered = true
output buffer #41:
timeUs = 1000000900900
size = 880
rendered = true
output buffer #42:
timeUs = 1000000867533
size = 530
rendered = true
output buffer #43:
timeUs = 1000000934266
size = 568
rendered = true

View File

@ -187,7 +187,8 @@ public class FakeAdaptiveMediaPeriod
DrmSessionManager.DRM_UNSUPPORTED,
new DrmSessionEventListener.EventDispatcher(),
new DefaultLoadErrorHandlingPolicy(/* minimumLoadableRetryCount= */ 3),
mediaSourceEventDispatcher);
mediaSourceEventDispatcher,
/* canReportInitialDiscontinuity= */ false);
streams[i] = sampleStream;
sampleStreams.add(sampleStream);
streamResetFlags[i] = true;