diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c1bb8ca798..8023cf78da 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -41,7 +41,9 @@ * Fix the bitrate being unset on primary track sample formats ([#3297](https://github.com/google/ExoPlayer/issues/3297)). * 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: * Support seeking in MPEG-TS ([#966](https://github.com/google/ExoPlayer/issues/966)). diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index c2acf3990b..81be7994f7 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -4,13 +4,11 @@ "samples": [ { "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", - "extension": "mpd" + "uri": "https://s3-eu-west-1.amazonaws.com/worm-bucket-prod/videos/5b6c35c32aee110004d40559/3DA76C26-A84A-4A53-8AD1-55EBCF5A9A09-ffr.MP4" }, { "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", - "extension": "mpd" + "uri": "https://s3.amazonaws.com/worm-streaming/test/playlist.m3u8" }, { "name": "Google Glass (WebM,VP9)", diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java index e872f730de..68322c60a1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java @@ -26,10 +26,15 @@ import com.google.android.exoplayer2.upstream.DataSpec; public abstract class BaseMediaChunk extends MediaChunk { /** - * The media time from which output will begin, or {@link C#TIME_UNSET} if the whole chunk should - * be output. + * The time from which output will begin, or {@link C#TIME_UNSET} if output will begin from the + * 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 int[] firstSampleIndices; @@ -42,8 +47,10 @@ public abstract class BaseMediaChunk extends MediaChunk { * @param trackSelectionData See {@link #trackSelectionData}. * @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 seekTimeUs The media time from which output will begin, or {@link C#TIME_UNSET} if the - * whole chunk should be output. + * @param clippedStartTimeUs The time in the chunk from which output will begin, or {@link + * 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. */ public BaseMediaChunk( @@ -54,11 +61,13 @@ public abstract class BaseMediaChunk extends MediaChunk { Object trackSelectionData, long startTimeUs, long endTimeUs, - long seekTimeUs, + long clippedStartTimeUs, + long clippedEndTimeUs, long chunkIndex) { super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); - this.seekTimeUs = seekTimeUs; + this.clippedStartTimeUs = clippedStartTimeUs; + this.clippedEndTimeUs = clippedEndTimeUs; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java index a3abc75606..a8676b5a05 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkExtractorWrapper.java @@ -64,6 +64,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { private boolean extractorInitialized; private TrackOutputProvider trackOutputProvider; + private long endTimeUs; private SeekMap seekMap; 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. * * @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 - * whole chunk. + * @param startTimeUs The start position in the new chunk, or {@link C#TIME_UNSET} to output + * 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.endTimeUs = endTimeUs; if (!extractorInitialized) { extractor.init(this); - if (seekTimeUs != C.TIME_UNSET) { - extractor.seek(/* position= */ 0, seekTimeUs); + if (startTimeUs != C.TIME_UNSET) { + extractor.seek(/* position= */ 0, startTimeUs); } extractorInitialized = true; } 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++) { - 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. bindingTrackOutput = new BindingTrackOutput(id, type, type == primaryTrackType ? primaryTrackManifestFormat : null); - bindingTrackOutput.bind(trackOutputProvider); + bindingTrackOutput.bind(trackOutputProvider, endTimeUs); bindingTrackOutputs.put(id, bindingTrackOutput); } return bindingTrackOutput; @@ -158,21 +163,25 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { private final int id; private final int type; private final Format manifestFormat; + private final DummyTrackOutput dummyTrackOutput; public Format sampleFormat; private TrackOutput trackOutput; + private long endTimeUs; public BindingTrackOutput(int id, int type, Format manifestFormat) { this.id = id; this.type = type; this.manifestFormat = manifestFormat; + dummyTrackOutput = new DummyTrackOutput(); } - public void bind(TrackOutputProvider trackOutputProvider) { + public void bind(TrackOutputProvider trackOutputProvider, long endTimeUs) { if (trackOutputProvider == null) { - trackOutput = new DummyTrackOutput(); + trackOutput = dummyTrackOutput; return; } + this.endTimeUs = endTimeUs; trackOutput = trackOutputProvider.track(id, type); if (sampleFormat != null) { trackOutput.format(sampleFormat); @@ -200,6 +209,9 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { @Override public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, CryptoData cryptoData) { + if (endTimeUs != C.TIME_UNSET && timeUs >= endTimeUs) { + trackOutput = dummyTrackOutput; + } trackOutput.sampleMetadata(timeUs, flags, size, offset, cryptoData); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 1f245784e7..5c39e4859f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -290,7 +290,7 @@ public class ChunkSampleStream implements SampleStream, S for (int i = 0; i < mediaChunks.size(); i++) { BaseMediaChunk mediaChunk = mediaChunks.get(i); long mediaChunkStartTimeUs = mediaChunk.startTimeUs; - if (mediaChunkStartTimeUs == positionUs && mediaChunk.seekTimeUs == C.TIME_UNSET) { + if (mediaChunkStartTimeUs == positionUs && mediaChunk.clippedStartTimeUs == C.TIME_UNSET) { seekToMediaChunk = mediaChunk; break; } else if (mediaChunkStartTimeUs > positionUs) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java index 2d5ba3d2e0..10b823d444 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java @@ -50,8 +50,10 @@ public class ContainerMediaChunk extends BaseMediaChunk { * @param trackSelectionData See {@link #trackSelectionData}. * @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 seekTimeUs The media time from which output will begin, or {@link C#TIME_UNSET} if the - * whole chunk should be output. + * @param clippedStartTimeUs The time in the chunk from which output will begin, or {@link + * 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 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 @@ -67,7 +69,8 @@ public class ContainerMediaChunk extends BaseMediaChunk { Object trackSelectionData, long startTimeUs, long endTimeUs, - long seekTimeUs, + long clippedStartTimeUs, + long clippedEndTimeUs, long chunkIndex, int chunkCount, long sampleOffsetUs, @@ -80,7 +83,8 @@ public class ContainerMediaChunk extends BaseMediaChunk { trackSelectionData, startTimeUs, endTimeUs, - seekTimeUs, + clippedStartTimeUs, + clippedEndTimeUs, chunkIndex); this.chunkCount = chunkCount; this.sampleOffsetUs = sampleOffsetUs; @@ -117,7 +121,11 @@ public class ContainerMediaChunk extends BaseMediaChunk { BaseMediaChunkOutput output = getOutput(); output.setSampleOffsetUs(sampleOffsetUs); 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. try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java index d5c0d6f301..eecf471b24 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/InitializationChunk.java @@ -76,7 +76,10 @@ public final class InitializationChunk extends Chunk { ExtractorInput input = new DefaultExtractorInput(dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); 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. try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java index 2c00c7690d..d53caf8e10 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java @@ -68,7 +68,8 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { trackSelectionData, startTimeUs, endTimeUs, - C.TIME_UNSET, + /* clippedStartTimeUs= */ C.TIME_UNSET, + /* clippedEndTimeUs= */ C.TIME_UNSET, chunkIndex); this.trackType = trackType; this.sampleFormat = sampleFormat; diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index a56b36f3fe..37c9e313ae 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -345,13 +345,32 @@ public class DefaultDashChunkSource implements DashChunkSource { } if (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); 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) 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; out.chunk = newMediaChunk( @@ -523,6 +542,11 @@ public class DefaultDashChunkSource implements DashChunkSource { segmentCount++; } 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), segmentUri.start, segmentUri.length, representation.getCacheKey()); long sampleOffsetUs = -representation.presentationTimeOffsetUs; @@ -535,6 +559,7 @@ public class DefaultDashChunkSource implements DashChunkSource { startTimeUs, endTimeUs, seekTimeUs, + clippedEndTimeUs, firstSegmentNum, segmentCount, sampleOffsetUs, diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index c04905010a..9ac376efad 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -288,6 +288,7 @@ public class DefaultSsChunkSource implements SsChunkSource { chunkStartTimeUs, chunkEndTimeUs, chunkSeekTimeUs, + /* clippedEndTimeUs= */ C.TIME_UNSET, chunkIndex, /* chunkCount= */ 1, sampleOffsetUs,