Clip DASH periods to their durations

Issue: #4185

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=212619419
This commit is contained in:
olly 2018-09-12 04:51:51 -07:00 committed by Oliver Woodman
parent 35c230f3c6
commit a5a7e988e3
10 changed files with 90 additions and 31 deletions

View File

@ -41,7 +41,9 @@
* Fix the bitrate being unset on primary track sample formats * Fix the bitrate being unset on primary track sample formats
([#3297](https://github.com/google/ExoPlayer/issues/3297)). ([#3297](https://github.com/google/ExoPlayer/issues/3297)).
* DASH: * DASH:
* Support messageData attribute of in-manifest events. * Support `messageData` attribute for in-manifest event streams.
* Clip periods to their specified durations
([#4185](https://github.com/google/ExoPlayer/issues/4185)).
* Improve seeking support for progressive streams: * Improve seeking support for progressive streams:
* Support seeking in MPEG-TS * Support seeking in MPEG-TS
([#966](https://github.com/google/ExoPlayer/issues/966)). ([#966](https://github.com/google/ExoPlayer/issues/966)).

View File

@ -4,13 +4,11 @@
"samples": [ "samples": [
{ {
"name": "Google Glass (MP4,H264)", "name": "Google Glass (MP4,H264)",
"uri": "https://www.youtube.com/api/manifest/dash/id/bf5bb2419360daf1/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=51AF5F39AB0CEC3E5497CD9C900EBFEAECCCB5C7.8506521BFC350652163895D4C26DEE124209AA9E&key=ik0", "uri": "https://s3-eu-west-1.amazonaws.com/worm-bucket-prod/videos/5b6c35c32aee110004d40559/3DA76C26-A84A-4A53-8AD1-55EBCF5A9A09-ffr.MP4"
"extension": "mpd"
}, },
{ {
"name": "Google Play (MP4,H264)", "name": "Google Play (MP4,H264)",
"uri": "https://www.youtube.com/api/manifest/dash/id/3aa39fa2cc27967f/source/youtube?as=fmp4_audio_clear,fmp4_sd_hd_clear&sparams=ip,ipbits,expire,source,id,as&ip=0.0.0.0&ipbits=0&expire=19000000000&signature=A2716F75795F5D2AF0E88962FFCD10DB79384F29.84308FF04844498CE6FBCE4731507882B8307798&key=ik0", "uri": "https://s3.amazonaws.com/worm-streaming/test/playlist.m3u8"
"extension": "mpd"
}, },
{ {
"name": "Google Glass (WebM,VP9)", "name": "Google Glass (WebM,VP9)",

View File

@ -26,10 +26,15 @@ import com.google.android.exoplayer2.upstream.DataSpec;
public abstract class BaseMediaChunk extends MediaChunk { public abstract class BaseMediaChunk extends MediaChunk {
/** /**
* The media time from which output will begin, or {@link C#TIME_UNSET} if the whole chunk should * The time from which output will begin, or {@link C#TIME_UNSET} if output will begin from the
* be output. * start of the chunk.
*/ */
public final long seekTimeUs; public final long clippedStartTimeUs;
/**
* The time from which output will end, or {@link C#TIME_UNSET} if output will end at the end of
* the chunk.
*/
public final long clippedEndTimeUs;
private BaseMediaChunkOutput output; private BaseMediaChunkOutput output;
private int[] firstSampleIndices; private int[] firstSampleIndices;
@ -42,8 +47,10 @@ public abstract class BaseMediaChunk extends MediaChunk {
* @param trackSelectionData See {@link #trackSelectionData}. * @param trackSelectionData See {@link #trackSelectionData}.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param seekTimeUs The media time from which output will begin, or {@link C#TIME_UNSET} if the * @param clippedStartTimeUs The time in the chunk from which output will begin, or {@link
* whole chunk should be output. * C#TIME_UNSET} to output from the start of the chunk.
* @param clippedEndTimeUs The time in the chunk from which output will end, or {@link
* C#TIME_UNSET} to output to the end of the chunk.
* @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known. * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known.
*/ */
public BaseMediaChunk( public BaseMediaChunk(
@ -54,11 +61,13 @@ public abstract class BaseMediaChunk extends MediaChunk {
Object trackSelectionData, Object trackSelectionData,
long startTimeUs, long startTimeUs,
long endTimeUs, long endTimeUs,
long seekTimeUs, long clippedStartTimeUs,
long clippedEndTimeUs,
long chunkIndex) { long chunkIndex) {
super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs,
endTimeUs, chunkIndex); endTimeUs, chunkIndex);
this.seekTimeUs = seekTimeUs; this.clippedStartTimeUs = clippedStartTimeUs;
this.clippedEndTimeUs = clippedEndTimeUs;
} }
/** /**

View File

@ -64,6 +64,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput {
private boolean extractorInitialized; private boolean extractorInitialized;
private TrackOutputProvider trackOutputProvider; private TrackOutputProvider trackOutputProvider;
private long endTimeUs;
private SeekMap seekMap; private SeekMap seekMap;
private Format[] sampleFormats; private Format[] sampleFormats;
@ -101,21 +102,25 @@ public final class ChunkExtractorWrapper implements ExtractorOutput {
* TrackOutputProvider}, and configures the extractor to receive data from a new chunk. * TrackOutputProvider}, and configures the extractor to receive data from a new chunk.
* *
* @param trackOutputProvider The provider of {@link TrackOutput}s that will receive sample data. * @param trackOutputProvider The provider of {@link TrackOutput}s that will receive sample data.
* @param seekTimeUs The seek position within the new chunk, or {@link C#TIME_UNSET} to output the * @param startTimeUs The start position in the new chunk, or {@link C#TIME_UNSET} to output
* whole chunk. * samples from the start of the chunk.
* @param endTimeUs The end position in the new chunk, or {@link C#TIME_UNSET} to output samples
* to the end of the chunk.
*/ */
public void init(@Nullable TrackOutputProvider trackOutputProvider, long seekTimeUs) { public void init(
@Nullable TrackOutputProvider trackOutputProvider, long startTimeUs, long endTimeUs) {
this.trackOutputProvider = trackOutputProvider; this.trackOutputProvider = trackOutputProvider;
this.endTimeUs = endTimeUs;
if (!extractorInitialized) { if (!extractorInitialized) {
extractor.init(this); extractor.init(this);
if (seekTimeUs != C.TIME_UNSET) { if (startTimeUs != C.TIME_UNSET) {
extractor.seek(/* position= */ 0, seekTimeUs); extractor.seek(/* position= */ 0, startTimeUs);
} }
extractorInitialized = true; extractorInitialized = true;
} else { } else {
extractor.seek(/* position= */ 0, seekTimeUs == C.TIME_UNSET ? 0 : seekTimeUs); extractor.seek(/* position= */ 0, startTimeUs == C.TIME_UNSET ? 0 : startTimeUs);
for (int i = 0; i < bindingTrackOutputs.size(); i++) { for (int i = 0; i < bindingTrackOutputs.size(); i++) {
bindingTrackOutputs.valueAt(i).bind(trackOutputProvider); bindingTrackOutputs.valueAt(i).bind(trackOutputProvider, endTimeUs);
} }
} }
} }
@ -131,7 +136,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput {
// TODO: Manifest formats for embedded tracks should also be passed here. // TODO: Manifest formats for embedded tracks should also be passed here.
bindingTrackOutput = new BindingTrackOutput(id, type, bindingTrackOutput = new BindingTrackOutput(id, type,
type == primaryTrackType ? primaryTrackManifestFormat : null); type == primaryTrackType ? primaryTrackManifestFormat : null);
bindingTrackOutput.bind(trackOutputProvider); bindingTrackOutput.bind(trackOutputProvider, endTimeUs);
bindingTrackOutputs.put(id, bindingTrackOutput); bindingTrackOutputs.put(id, bindingTrackOutput);
} }
return bindingTrackOutput; return bindingTrackOutput;
@ -158,21 +163,25 @@ public final class ChunkExtractorWrapper implements ExtractorOutput {
private final int id; private final int id;
private final int type; private final int type;
private final Format manifestFormat; private final Format manifestFormat;
private final DummyTrackOutput dummyTrackOutput;
public Format sampleFormat; public Format sampleFormat;
private TrackOutput trackOutput; private TrackOutput trackOutput;
private long endTimeUs;
public BindingTrackOutput(int id, int type, Format manifestFormat) { public BindingTrackOutput(int id, int type, Format manifestFormat) {
this.id = id; this.id = id;
this.type = type; this.type = type;
this.manifestFormat = manifestFormat; this.manifestFormat = manifestFormat;
dummyTrackOutput = new DummyTrackOutput();
} }
public void bind(TrackOutputProvider trackOutputProvider) { public void bind(TrackOutputProvider trackOutputProvider, long endTimeUs) {
if (trackOutputProvider == null) { if (trackOutputProvider == null) {
trackOutput = new DummyTrackOutput(); trackOutput = dummyTrackOutput;
return; return;
} }
this.endTimeUs = endTimeUs;
trackOutput = trackOutputProvider.track(id, type); trackOutput = trackOutputProvider.track(id, type);
if (sampleFormat != null) { if (sampleFormat != null) {
trackOutput.format(sampleFormat); trackOutput.format(sampleFormat);
@ -200,6 +209,9 @@ public final class ChunkExtractorWrapper implements ExtractorOutput {
@Override @Override
public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
CryptoData cryptoData) { CryptoData cryptoData) {
if (endTimeUs != C.TIME_UNSET && timeUs >= endTimeUs) {
trackOutput = dummyTrackOutput;
}
trackOutput.sampleMetadata(timeUs, flags, size, offset, cryptoData); trackOutput.sampleMetadata(timeUs, flags, size, offset, cryptoData);
} }

View File

@ -290,7 +290,7 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
for (int i = 0; i < mediaChunks.size(); i++) { for (int i = 0; i < mediaChunks.size(); i++) {
BaseMediaChunk mediaChunk = mediaChunks.get(i); BaseMediaChunk mediaChunk = mediaChunks.get(i);
long mediaChunkStartTimeUs = mediaChunk.startTimeUs; long mediaChunkStartTimeUs = mediaChunk.startTimeUs;
if (mediaChunkStartTimeUs == positionUs && mediaChunk.seekTimeUs == C.TIME_UNSET) { if (mediaChunkStartTimeUs == positionUs && mediaChunk.clippedStartTimeUs == C.TIME_UNSET) {
seekToMediaChunk = mediaChunk; seekToMediaChunk = mediaChunk;
break; break;
} else if (mediaChunkStartTimeUs > positionUs) { } else if (mediaChunkStartTimeUs > positionUs) {

View File

@ -50,8 +50,10 @@ public class ContainerMediaChunk extends BaseMediaChunk {
* @param trackSelectionData See {@link #trackSelectionData}. * @param trackSelectionData See {@link #trackSelectionData}.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
* @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param seekTimeUs The media time from which output will begin, or {@link C#TIME_UNSET} if the * @param clippedStartTimeUs The time in the chunk from which output will begin, or {@link
* whole chunk should be output. * C#TIME_UNSET} to output from the start of the chunk.
* @param clippedEndTimeUs The time in the chunk from which output will end, or {@link
* C#TIME_UNSET} to output to the end of the chunk.
* @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known. * @param chunkIndex The index of the chunk, or {@link C#INDEX_UNSET} if it is not known.
* @param chunkCount The number of chunks in the underlying media that are spanned by this * @param chunkCount The number of chunks in the underlying media that are spanned by this
* instance. Normally equal to one, but may be larger if multiple chunks as defined by the * instance. Normally equal to one, but may be larger if multiple chunks as defined by the
@ -67,7 +69,8 @@ public class ContainerMediaChunk extends BaseMediaChunk {
Object trackSelectionData, Object trackSelectionData,
long startTimeUs, long startTimeUs,
long endTimeUs, long endTimeUs,
long seekTimeUs, long clippedStartTimeUs,
long clippedEndTimeUs,
long chunkIndex, long chunkIndex,
int chunkCount, int chunkCount,
long sampleOffsetUs, long sampleOffsetUs,
@ -80,7 +83,8 @@ public class ContainerMediaChunk extends BaseMediaChunk {
trackSelectionData, trackSelectionData,
startTimeUs, startTimeUs,
endTimeUs, endTimeUs,
seekTimeUs, clippedStartTimeUs,
clippedEndTimeUs,
chunkIndex); chunkIndex);
this.chunkCount = chunkCount; this.chunkCount = chunkCount;
this.sampleOffsetUs = sampleOffsetUs; this.sampleOffsetUs = sampleOffsetUs;
@ -117,7 +121,11 @@ public class ContainerMediaChunk extends BaseMediaChunk {
BaseMediaChunkOutput output = getOutput(); BaseMediaChunkOutput output = getOutput();
output.setSampleOffsetUs(sampleOffsetUs); output.setSampleOffsetUs(sampleOffsetUs);
extractorWrapper.init( extractorWrapper.init(
output, seekTimeUs == C.TIME_UNSET ? 0 : (seekTimeUs - sampleOffsetUs)); output,
clippedStartTimeUs == C.TIME_UNSET
? C.TIME_UNSET
: (clippedStartTimeUs - sampleOffsetUs),
clippedEndTimeUs == C.TIME_UNSET ? C.TIME_UNSET : (clippedEndTimeUs - sampleOffsetUs));
} }
// Load and decode the sample data. // Load and decode the sample data.
try { try {

View File

@ -76,7 +76,10 @@ public final class InitializationChunk extends Chunk {
ExtractorInput input = new DefaultExtractorInput(dataSource, ExtractorInput input = new DefaultExtractorInput(dataSource,
loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));
if (nextLoadPosition == 0) { if (nextLoadPosition == 0) {
extractorWrapper.init(/* trackOutputProvider= */ null, C.TIME_UNSET); extractorWrapper.init(
/* trackOutputProvider= */ null,
/* startTimeUs= */ C.TIME_UNSET,
/* endTimeUs= */ C.TIME_UNSET);
} }
// Load and decode the initialization data. // Load and decode the initialization data.
try { try {

View File

@ -68,7 +68,8 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk {
trackSelectionData, trackSelectionData,
startTimeUs, startTimeUs,
endTimeUs, endTimeUs,
C.TIME_UNSET, /* clippedStartTimeUs= */ C.TIME_UNSET,
/* clippedEndTimeUs= */ C.TIME_UNSET,
chunkIndex); chunkIndex);
this.trackType = trackType; this.trackType = trackType;
this.sampleFormat = sampleFormat; this.sampleFormat = sampleFormat;

View File

@ -345,13 +345,32 @@ public class DefaultDashChunkSource implements DashChunkSource {
} }
if (segmentNum > lastAvailableSegmentNum if (segmentNum > lastAvailableSegmentNum
|| (missingLastSegment && segmentNum >= lastAvailableSegmentNum)) { || (missingLastSegment && segmentNum >= lastAvailableSegmentNum)) {
// This is beyond the last chunk in the current manifest. // The segment is beyond the end of the period. We know the period will not be extended if the
// manifest is static, or if there's a period after this one.
out.endOfStream = !manifest.dynamic || (periodIndex < manifest.getPeriodCount() - 1); out.endOfStream = !manifest.dynamic || (periodIndex < manifest.getPeriodCount() - 1);
return; return;
} }
long periodDurationUs = representationHolder.periodDurationUs;
if (periodDurationUs != C.TIME_UNSET
&& representationHolder.getSegmentStartTimeUs(segmentNum) >= periodDurationUs) {
// The period duration clips the period to a position before the segment.
out.endOfStream = true;
return;
}
int maxSegmentCount = int maxSegmentCount =
(int) Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1); (int) Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1);
if (periodDurationUs != C.TIME_UNSET) {
while (maxSegmentCount > 1
&& representationHolder.getSegmentStartTimeUs(segmentNum + maxSegmentCount - 1)
>= periodDurationUs) {
// The period duration clips the period to a position before the last segment in the range
// [segmentNum, segmentNum + maxSegmentCount - 1]. Reduce maxSegmentCount.
maxSegmentCount--;
}
}
long seekTimeUs = queue.isEmpty() ? loadPositionUs : C.TIME_UNSET; long seekTimeUs = queue.isEmpty() ? loadPositionUs : C.TIME_UNSET;
out.chunk = out.chunk =
newMediaChunk( newMediaChunk(
@ -523,6 +542,11 @@ public class DefaultDashChunkSource implements DashChunkSource {
segmentCount++; segmentCount++;
} }
long endTimeUs = representationHolder.getSegmentEndTimeUs(firstSegmentNum + segmentCount - 1); long endTimeUs = representationHolder.getSegmentEndTimeUs(firstSegmentNum + segmentCount - 1);
long periodDurationUs = representationHolder.periodDurationUs;
long clippedEndTimeUs =
periodDurationUs != C.TIME_UNSET && periodDurationUs < endTimeUs
? periodDurationUs
: C.TIME_UNSET;
DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl), DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl),
segmentUri.start, segmentUri.length, representation.getCacheKey()); segmentUri.start, segmentUri.length, representation.getCacheKey());
long sampleOffsetUs = -representation.presentationTimeOffsetUs; long sampleOffsetUs = -representation.presentationTimeOffsetUs;
@ -535,6 +559,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
startTimeUs, startTimeUs,
endTimeUs, endTimeUs,
seekTimeUs, seekTimeUs,
clippedEndTimeUs,
firstSegmentNum, firstSegmentNum,
segmentCount, segmentCount,
sampleOffsetUs, sampleOffsetUs,

View File

@ -288,6 +288,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
chunkStartTimeUs, chunkStartTimeUs,
chunkEndTimeUs, chunkEndTimeUs,
chunkSeekTimeUs, chunkSeekTimeUs,
/* clippedEndTimeUs= */ C.TIME_UNSET,
chunkIndex, chunkIndex,
/* chunkCount= */ 1, /* chunkCount= */ 1,
sampleOffsetUs, sampleOffsetUs,