From 74b43e26bd38431ed93db9a222844c308d2e5013 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 18 Jul 2016 11:17:09 -0700 Subject: [PATCH] Allow injection of custom ChunkSources ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=127737169 --- .../exoplayer2/demo/PlayerActivity.java | 23 +- .../source/dash/DashChunkSourceTest.java | 2 +- .../source/dash/DashChunkSource.java | 453 +--------------- .../source/dash/DashMediaPeriod.java | 23 +- .../source/dash/DashMediaSource.java | 27 +- .../source/dash/DefaultDashChunkSource.java | 499 ++++++++++++++++++ .../DefaultSmoothStreamingChunkSource.java | 305 +++++++++++ .../SmoothStreamingChunkSource.java | 256 +-------- .../SmoothStreamingMediaSource.java | 30 +- .../playbacktests/gts/DashTest.java | 8 +- 10 files changed, 873 insertions(+), 753 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSmoothStreamingChunkSource.java diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index cdde3337b3..a394aab02b 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -40,7 +40,9 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.chunk.FormatEvaluator; import com.google.android.exoplayer2.source.chunk.FormatEvaluator.AdaptiveEvaluator; import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.DefaultSmoothStreamingChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.SmoothStreamingMediaSource; import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.text.Cue; @@ -131,7 +133,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, private SubtitleLayout subtitleLayout; private Button retryButton; - private DataSource.Factory dataSourceFactory; + private DataSource.Factory manifestDataSourceFactory; + private DataSource.Factory mediaDataSourceFactory; private FormatEvaluator.Factory formatEvaluatorFactory; private SimpleExoPlayer player; private MappingTrackSelector trackSelector; @@ -148,8 +151,9 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String userAgent = Util.getUserAgent(this, "ExoPlayerDemo"); + manifestDataSourceFactory = new DefaultDataSourceFactory(this, userAgent); BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); - dataSourceFactory = new DefaultDataSourceFactory(this, userAgent, bandwidthMeter); + mediaDataSourceFactory = new DefaultDataSourceFactory(this, userAgent, bandwidthMeter); formatEvaluatorFactory = new AdaptiveEvaluator.Factory(bandwidthMeter); mainHandler = new Handler(); @@ -343,16 +347,21 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, int type = Util.inferContentType(lastPathSegment); switch (type) { case Util.TYPE_SS: - return new SmoothStreamingMediaSource(uri, dataSourceFactory, formatEvaluatorFactory, - mainHandler, eventLogger); + DefaultSmoothStreamingChunkSource.Factory factory = + new DefaultSmoothStreamingChunkSource.Factory(mediaDataSourceFactory, + formatEvaluatorFactory); + return new SmoothStreamingMediaSource(uri, manifestDataSourceFactory, factory, mainHandler, + eventLogger); case Util.TYPE_DASH: - return new DashMediaSource(uri, dataSourceFactory, formatEvaluatorFactory, mainHandler, + DefaultDashChunkSource.Factory factory2 = new DefaultDashChunkSource.Factory( + mediaDataSourceFactory, formatEvaluatorFactory); + return new DashMediaSource(uri, mediaDataSourceFactory, factory2, mainHandler, eventLogger); case Util.TYPE_HLS: - return new HlsMediaSource(uri, dataSourceFactory, formatEvaluatorFactory, mainHandler, + return new HlsMediaSource(uri, mediaDataSourceFactory, formatEvaluatorFactory, mainHandler, eventLogger); case Util.TYPE_OTHER: - return new ExtractorMediaSource(uri, dataSourceFactory, new DefaultExtractorsFactory(), + return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(), mainHandler, eventLogger); default: throw new IllegalStateException("Unsupported type: " + type); diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashChunkSourceTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashChunkSourceTest.java index 710b439ae0..05e02826cd 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashChunkSourceTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashChunkSourceTest.java @@ -22,7 +22,7 @@ import com.google.android.exoplayer2.util.MimeTypes; import android.test.InstrumentationTestCase; /** - * Tests {@link DashChunkSource}. + * Tests {@link DefaultDashChunkSource}. */ public class DashChunkSourceTest extends InstrumentationTestCase { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java index 01e041a5a9..1015731cfc 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java @@ -15,463 +15,24 @@ */ package com.google.android.exoplayer2.source.dash; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.Format.DecreasingBandwidthComparator; -import com.google.android.exoplayer2.extractor.ChunkIndex; -import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; -import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; -import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.chunk.Chunk; -import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper; -import com.google.android.exoplayer2.source.chunk.ChunkHolder; import com.google.android.exoplayer2.source.chunk.ChunkSource; -import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk; -import com.google.android.exoplayer2.source.chunk.FormatEvaluator; -import com.google.android.exoplayer2.source.chunk.FormatEvaluator.Evaluation; -import com.google.android.exoplayer2.source.chunk.InitializationChunk; -import com.google.android.exoplayer2.source.chunk.MediaChunk; -import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk; import com.google.android.exoplayer2.source.dash.mpd.MediaPresentationDescription; -import com.google.android.exoplayer2.source.dash.mpd.RangedUri; -import com.google.android.exoplayer2.source.dash.mpd.Representation; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException; import com.google.android.exoplayer2.upstream.Loader; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.Util; - -import android.os.SystemClock; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; /** * An {@link ChunkSource} for DASH streams. */ -public class DashChunkSource implements ChunkSource { +public interface DashChunkSource extends ChunkSource { - private final Loader manifestLoader; - private final int adaptationSetIndex; - private final TrackGroup trackGroup; - private final RepresentationHolder[] representationHolders; - private final Format[] enabledFormats; - private final boolean[] adaptiveFormatBlacklistFlags; - private final DataSource dataSource; - private final FormatEvaluator adaptiveFormatEvaluator; - private final long elapsedRealtimeOffsetUs; - private final Evaluation evaluation; + interface Factory { - private MediaPresentationDescription manifest; - - private boolean lastChunkWasInitialization; - private IOException fatalError; - private boolean missingLastSegment; - - /** - * @param manifestLoader The {@link Loader} being used to load manifests. - * @param manifest The initial manifest. - * @param periodIndex The index of the period in the manifest. - * @param adaptationSetIndex The index of the adaptation set in the period. - * @param trackGroup The track group corresponding to the adaptation set. - * @param tracks The indices of the selected tracks within the adaptation set. - * @param dataSource A {@link DataSource} suitable for loading the media data. - * @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats. - * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between - * server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified - * as the server's unix time minus the local elapsed time. If unknown, set to 0. - */ - public DashChunkSource(Loader manifestLoader, MediaPresentationDescription manifest, - int periodIndex, int adaptationSetIndex, TrackGroup trackGroup, int[] tracks, - DataSource dataSource, FormatEvaluator adaptiveFormatEvaluator, - long elapsedRealtimeOffsetMs) { - this.manifestLoader = manifestLoader; - this.manifest = manifest; - this.adaptationSetIndex = adaptationSetIndex; - this.trackGroup = trackGroup; - this.dataSource = dataSource; - this.adaptiveFormatEvaluator = adaptiveFormatEvaluator; - this.elapsedRealtimeOffsetUs = elapsedRealtimeOffsetMs * 1000; - this.evaluation = new Evaluation(); - - long periodDurationUs = getPeriodDurationUs(periodIndex); - List representations = getRepresentations(periodIndex); - representationHolders = new RepresentationHolder[representations.size()]; - - for (int i = 0; i < representations.size(); i++) { - Representation representation = representations.get(i); - representationHolders[i] = new RepresentationHolder(periodDurationUs, representation); - } - enabledFormats = new Format[tracks.length]; - for (int i = 0; i < tracks.length; i++) { - enabledFormats[i] = trackGroup.getFormat(tracks[i]); - } - Arrays.sort(enabledFormats, new DecreasingBandwidthComparator()); - if (adaptiveFormatEvaluator != null) { - adaptiveFormatEvaluator.enable(enabledFormats); - adaptiveFormatBlacklistFlags = new boolean[tracks.length]; - } else { - adaptiveFormatBlacklistFlags = null; - } - } - - public void updateManifest(MediaPresentationDescription newManifest, int periodIndex) { - try { - manifest = newManifest; - long periodDurationUs = getPeriodDurationUs(periodIndex); - List representations = getRepresentations(periodIndex); - for (int i = 0; i < representationHolders.length; i++) { - Representation representation = representations.get(i); - representationHolders[i].updateRepresentation(periodDurationUs, representation); - } - } catch (BehindLiveWindowException e) { - fatalError = e; - } - } - - private List getRepresentations(int periodIndex) { - return manifest.getPeriod(periodIndex).adaptationSets.get(adaptationSetIndex).representations; - } - - // ChunkSource implementation. - - @Override - public void maybeThrowError() throws IOException { - if (fatalError != null) { - throw fatalError; - } else { - manifestLoader.maybeThrowError(); - } - } - - @Override - public int getPreferredQueueSize(long playbackPositionUs, List queue) { - if (fatalError != null || enabledFormats.length < 2) { - return queue.size(); - } - return adaptiveFormatEvaluator.evaluateQueueSize(playbackPositionUs, queue, - adaptiveFormatBlacklistFlags); - } - - @Override - public final void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) { - if (fatalError != null) { - return; - } - - if (evaluation.format == null || !lastChunkWasInitialization) { - if (enabledFormats.length > 1) { - long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; - adaptiveFormatEvaluator.evaluateFormat(bufferedDurationUs, adaptiveFormatBlacklistFlags, - evaluation); - } else { - evaluation.format = enabledFormats[0]; - evaluation.trigger = FormatEvaluator.TRIGGER_UNKNOWN; - evaluation.data = null; - } - } - - Format selectedFormat = evaluation.format; - if (selectedFormat == null) { - return; - } - - RepresentationHolder representationHolder = - representationHolders[getTrackIndex(selectedFormat)]; - Representation selectedRepresentation = representationHolder.representation; - DashSegmentIndex segmentIndex = representationHolder.segmentIndex; - - RangedUri pendingInitializationUri = null; - RangedUri pendingIndexUri = null; - Format sampleFormat = representationHolder.sampleFormat; - if (sampleFormat == null) { - pendingInitializationUri = selectedRepresentation.getInitializationUri(); - } - if (segmentIndex == null) { - pendingIndexUri = selectedRepresentation.getIndexUri(); - } - if (pendingInitializationUri != null || pendingIndexUri != null) { - // We have initialization and/or index requests to make. - Chunk initializationChunk = newInitializationChunk(representationHolder, dataSource, - selectedFormat, pendingInitializationUri, pendingIndexUri, evaluation.trigger, - evaluation.data); - lastChunkWasInitialization = true; - out.chunk = initializationChunk; - return; - } - - long nowUs = getNowUnixTimeUs(); - int firstAvailableSegmentNum = representationHolder.getFirstSegmentNum(); - int lastAvailableSegmentNum = representationHolder.getLastSegmentNum(); - boolean indexUnbounded = lastAvailableSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED; - if (indexUnbounded) { - // The index is itself unbounded. We need to use the current time to calculate the range of - // available segments. - long liveEdgeTimestampUs = nowUs - manifest.availabilityStartTime * 1000; - if (manifest.timeShiftBufferDepth != -1) { - long bufferDepthUs = manifest.timeShiftBufferDepth * 1000; - firstAvailableSegmentNum = Math.max(firstAvailableSegmentNum, - representationHolder.getSegmentNum(liveEdgeTimestampUs - bufferDepthUs)); - } - // getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the - // index of the last completed segment. - lastAvailableSegmentNum = representationHolder.getSegmentNum(liveEdgeTimestampUs) - 1; - } - - int segmentNum; - if (previous == null) { - segmentNum = Util.constrainValue(representationHolder.getSegmentNum(playbackPositionUs), - firstAvailableSegmentNum, lastAvailableSegmentNum); - } else { - segmentNum = previous.getNextChunkIndex(); - if (segmentNum < firstAvailableSegmentNum) { - // This is before the first chunk in the current manifest. - fatalError = new BehindLiveWindowException(); - return; - } - } - - if (segmentNum > lastAvailableSegmentNum - || (missingLastSegment && segmentNum >= lastAvailableSegmentNum)) { - // This is beyond the last chunk in the current manifest. - out.endOfStream = !manifest.dynamic; - return; - } - - Chunk nextMediaChunk = newMediaChunk(representationHolder, dataSource, selectedFormat, - sampleFormat, segmentNum, evaluation.trigger, evaluation.data); - lastChunkWasInitialization = false; - out.chunk = nextMediaChunk; - } - - @Override - public void onChunkLoadCompleted(Chunk chunk) { - if (chunk instanceof InitializationChunk) { - InitializationChunk initializationChunk = (InitializationChunk) chunk; - RepresentationHolder representationHolder = - representationHolders[getTrackIndex(initializationChunk.format)]; - Format sampleFormat = initializationChunk.getSampleFormat(); - if (sampleFormat != null) { - representationHolder.setSampleFormat(sampleFormat); - } - // The null check avoids overwriting an index obtained from the manifest with one obtained - // from the stream. If the manifest defines an index then the stream shouldn't, but in cases - // where it does we should ignore it. - if (representationHolder.segmentIndex == null) { - SeekMap seekMap = initializationChunk.getSeekMap(); - if (seekMap != null) { - representationHolder.segmentIndex = new DashWrappingSegmentIndex((ChunkIndex) seekMap, - initializationChunk.dataSpec.uri.toString()); - } - } - } - } - - @Override - public boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e) { - // Workaround for missing segment at the end of the period - if (cancelable && !manifest.dynamic && chunk instanceof MediaChunk - && e instanceof InvalidResponseCodeException - && ((InvalidResponseCodeException) e).responseCode == 404) { - RepresentationHolder representationHolder = - representationHolders[getTrackIndex(chunk.format)]; - int lastAvailableSegmentNum = representationHolder.getLastSegmentNum(); - if (((MediaChunk) chunk).chunkIndex >= lastAvailableSegmentNum) { - missingLastSegment = true; - return true; - } - } - // TODO: Consider implementing representation blacklisting. - return false; - } - - @Override - public void release() { - if (adaptiveFormatEvaluator != null) { - adaptiveFormatEvaluator.disable(); - } - } - - // Private methods. - - private long getNowUnixTimeUs() { - if (elapsedRealtimeOffsetUs != 0) { - return (SystemClock.elapsedRealtime() * 1000) + elapsedRealtimeOffsetUs; - } else { - return System.currentTimeMillis() * 1000; - } - } - - private Chunk newInitializationChunk(RepresentationHolder representationHolder, - DataSource dataSource, Format trackFormat, RangedUri initializationUri, RangedUri indexUri, - int formatEvaluatorTrigger, Object formatEvaluatorData) { - RangedUri requestUri; - if (initializationUri != null) { - // It's common for initialization and index data to be stored adjacently. Attempt to merge - // the two requests together to request both at once. - requestUri = initializationUri.attemptMerge(indexUri); - if (requestUri == null) { - requestUri = initializationUri; - } - } else { - requestUri = indexUri; - } - DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length, - representationHolder.representation.getCacheKey()); - return new InitializationChunk(dataSource, dataSpec, trackFormat, - formatEvaluatorTrigger, formatEvaluatorData, representationHolder.extractorWrapper); - } - - private Chunk newMediaChunk(RepresentationHolder representationHolder, DataSource dataSource, - Format trackFormat, Format sampleFormat, int segmentNum, int formatEvaluatorTrigger, - Object formatEvaluatorData) { - Representation representation = representationHolder.representation; - long startTimeUs = representationHolder.getSegmentStartTimeUs(segmentNum); - long endTimeUs = representationHolder.getSegmentEndTimeUs(segmentNum); - RangedUri segmentUri = representationHolder.getSegmentUrl(segmentNum); - DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length, - representation.getCacheKey()); - - if (representationHolder.extractorWrapper == null) { - return new SingleSampleMediaChunk(dataSource, dataSpec, trackFormat, formatEvaluatorTrigger, - formatEvaluatorData, startTimeUs, endTimeUs, segmentNum, trackFormat); - } else { - long sampleOffsetUs = -representation.presentationTimeOffsetUs; - return new ContainerMediaChunk(dataSource, dataSpec, trackFormat, formatEvaluatorTrigger, - formatEvaluatorData, startTimeUs, endTimeUs, segmentNum, sampleOffsetUs, - representationHolder.extractorWrapper, sampleFormat); - } - } - - private int getTrackIndex(Format format) { - for (int i = 0; i < trackGroup.length; i++) { - if (trackGroup.getFormat(i) == format) { - return i; - } - } - // Should never happen. - throw new IllegalStateException("Invalid format: " + format); - } - - private long getPeriodDurationUs(int periodIndex) { - long durationMs = manifest.getPeriodDuration(periodIndex); - if (durationMs == -1) { - return C.UNSET_TIME_US; - } else { - return durationMs * 1000; - } - } - - // Protected classes. - - protected static final class RepresentationHolder { - - public final ChunkExtractorWrapper extractorWrapper; - - public Representation representation; - public DashSegmentIndex segmentIndex; - public Format sampleFormat; - - private long periodDurationUs; - private int segmentNumShift; - - public RepresentationHolder(long periodDurationUs, Representation representation) { - this.periodDurationUs = periodDurationUs; - this.representation = representation; - String containerMimeType = representation.format.containerMimeType; - // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, - // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. - extractorWrapper = mimeTypeIsRawText(containerMimeType) ? null : new ChunkExtractorWrapper( - mimeTypeIsWebm(containerMimeType) ? new MatroskaExtractor() - : new FragmentedMp4Extractor(), - representation.format, true /* preferManifestDrmInitData */); - segmentIndex = representation.getIndex(); - } - - public void setSampleFormat(Format sampleFormat) { - this.sampleFormat = sampleFormat; - } - - public void updateRepresentation(long newPeriodDurationUs, Representation newRepresentation) - throws BehindLiveWindowException{ - DashSegmentIndex oldIndex = representation.getIndex(); - DashSegmentIndex newIndex = newRepresentation.getIndex(); - - periodDurationUs = newPeriodDurationUs; - representation = newRepresentation; - if (oldIndex == null) { - // Segment numbers cannot shift if the index isn't defined by the manifest. - return; - } - - segmentIndex = newIndex; - if (!oldIndex.isExplicit()) { - // Segment numbers cannot shift if the index isn't explicit. - return; - } - - int oldIndexLastSegmentNum = oldIndex.getLastSegmentNum(periodDurationUs); - long oldIndexEndTimeUs = oldIndex.getTimeUs(oldIndexLastSegmentNum) - + oldIndex.getDurationUs(oldIndexLastSegmentNum, periodDurationUs); - int newIndexFirstSegmentNum = newIndex.getFirstSegmentNum(); - long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum); - if (oldIndexEndTimeUs == newIndexStartTimeUs) { - // The new index continues where the old one ended, with no overlap. - segmentNumShift += oldIndex.getLastSegmentNum(periodDurationUs) + 1 - - newIndexFirstSegmentNum; - } else if (oldIndexEndTimeUs < newIndexStartTimeUs) { - // There's a gap between the old index and the new one which means we've slipped behind the - // live window and can't proceed. - throw new BehindLiveWindowException(); - } else { - // The new index overlaps with the old one. - segmentNumShift += oldIndex.getSegmentNum(newIndexStartTimeUs, periodDurationUs) - - newIndexFirstSegmentNum; - } - } - - public int getFirstSegmentNum() { - return segmentIndex.getFirstSegmentNum() + segmentNumShift; - } - - public int getLastSegmentNum() { - int lastSegmentNum = segmentIndex.getLastSegmentNum(periodDurationUs); - if (lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED) { - return DashSegmentIndex.INDEX_UNBOUNDED; - } - return lastSegmentNum + segmentNumShift; - } - - public long getSegmentStartTimeUs(int segmentNum) { - return segmentIndex.getTimeUs(segmentNum - segmentNumShift); - } - - public long getSegmentEndTimeUs(int segmentNum) { - return getSegmentStartTimeUs(segmentNum) - + segmentIndex.getDurationUs(segmentNum - segmentNumShift, periodDurationUs); - } - - public int getSegmentNum(long positionUs) { - return segmentIndex.getSegmentNum(positionUs, periodDurationUs) + segmentNumShift; - } - - public RangedUri getSegmentUrl(int segmentNum) { - return segmentIndex.getSegmentUrl(segmentNum - segmentNumShift); - } - - private static boolean mimeTypeIsWebm(String mimeType) { - return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM) - || mimeType.startsWith(MimeTypes.APPLICATION_WEBM); - } - - private static boolean mimeTypeIsRawText(String mimeType) { - return MimeTypes.isText(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType); - } + DashChunkSource createDashChunkSource(Loader manifestLoader, + MediaPresentationDescription manifest, int periodIndex, int adaptationSetIndex, + TrackGroup trackGroup, int[] tracks, long elapsedRealtimeOffsetMs); } + void updateManifest(MediaPresentationDescription newManifest, int periodIndex); + } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index 4abda3bda9..a9d57127e3 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -25,14 +25,12 @@ import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.chunk.ChunkSampleStream; -import com.google.android.exoplayer2.source.chunk.FormatEvaluator; import com.google.android.exoplayer2.source.dash.mpd.AdaptationSet; import com.google.android.exoplayer2.source.dash.mpd.MediaPresentationDescription; import com.google.android.exoplayer2.source.dash.mpd.Period; import com.google.android.exoplayer2.source.dash.mpd.Representation; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.Loader; import android.util.Pair; @@ -47,8 +45,7 @@ import java.util.List; /* package */ final class DashMediaPeriod implements MediaPeriod, SequenceableLoader.Callback> { - private final DataSource.Factory dataSourceFactory; - private final FormatEvaluator.Factory formatEvaluatorFactory; + private final DashChunkSource.Factory chunkSourceFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; private final long elapsedRealtimeOffset; @@ -66,13 +63,11 @@ import java.util.List; private Period period; public DashMediaPeriod(MediaPresentationDescription manifest, int index, - DataSource.Factory dataSourceFactory, FormatEvaluator.Factory formatEvaluatorFactory, - int minLoadableRetryCount, EventDispatcher eventDispatcher, long elapsedRealtimeOffset, - Loader loader) { + DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, + EventDispatcher eventDispatcher, long elapsedRealtimeOffset, Loader loader) { this.manifest = manifest; this.index = index; - this.dataSourceFactory = dataSourceFactory; - this.formatEvaluatorFactory = formatEvaluatorFactory; + this.chunkSourceFactory = chunkSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.eventDispatcher = eventDispatcher; this.elapsedRealtimeOffset = elapsedRealtimeOffset; @@ -242,16 +237,12 @@ import java.util.List; private ChunkSampleStream buildSampleStream(TrackSelection selection, long positionUs) { int[] selectedTracks = selection.getTracks(); - FormatEvaluator adaptiveEvaluator = selectedTracks.length > 1 - ? formatEvaluatorFactory.createFormatEvaluator() : null; int adaptationSetIndex = trackGroupAdaptationSetIndices[selection.group]; AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex); - int adaptationSetType = adaptationSet.type; - DataSource dataSource = dataSourceFactory.createDataSource(); - DashChunkSource chunkSource = new DashChunkSource(loader, manifest, index, adaptationSetIndex, - trackGroups.get(selection.group), selectedTracks, dataSource, adaptiveEvaluator, + DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource(loader, manifest, index, + adaptationSetIndex, trackGroups.get(selection.group), selectedTracks, elapsedRealtimeOffset); - return new ChunkSampleStream<>(adaptationSetType, chunkSource, this, allocator, positionUs, + return new ChunkSampleStream<>(adaptationSet.type, chunkSource, this, allocator, positionUs, minLoadableRetryCount, eventDispatcher); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 5ea33925f1..14cad6688c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -21,7 +21,6 @@ import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.chunk.FormatEvaluator; import com.google.android.exoplayer2.source.dash.mpd.MediaPresentationDescription; import com.google.android.exoplayer2.source.dash.mpd.MediaPresentationDescriptionParser; import com.google.android.exoplayer2.source.dash.mpd.UtcTimingElement; @@ -56,8 +55,8 @@ public final class DashMediaSource implements MediaSource { private static final String TAG = "DashMediaSource"; - private final DataSource.Factory dataSourceFactory; - private final FormatEvaluator.Factory formatEvaluatorFactory; + private final DataSource.Factory manifestDataSourceFactory; + private final DashChunkSource.Factory chunkSourceFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; private final MediaPresentationDescriptionParser manifestParser; @@ -74,19 +73,19 @@ public final class DashMediaSource implements MediaSource { private DashMediaPeriod[] periods; private long elapsedRealtimeOffset; - public DashMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, - FormatEvaluator.Factory formatEvaluatorFactory, Handler eventHandler, + public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, + DashChunkSource.Factory chunkSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { - this(manifestUri, dataSourceFactory, formatEvaluatorFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, - eventHandler, eventListener); + this(manifestUri, manifestDataSourceFactory, chunkSourceFactory, + DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, eventListener); } - public DashMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, - FormatEvaluator.Factory formatEvaluatorFactory, int minLoadableRetryCount, + public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, + DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this.manifestUri = manifestUri; - this.dataSourceFactory = dataSourceFactory; - this.formatEvaluatorFactory = formatEvaluatorFactory; + this.manifestDataSourceFactory = manifestDataSourceFactory; + this.chunkSourceFactory = chunkSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; eventDispatcher = new EventDispatcher(eventHandler, eventListener); manifestParser = new MediaPresentationDescriptionParser(); @@ -97,7 +96,7 @@ public final class DashMediaSource implements MediaSource { @Override public void prepareSource() { - dataSource = dataSourceFactory.createDataSource(); + dataSource = manifestDataSourceFactory.createDataSource(); loader = new Loader("Loader:DashMediaSource"); manifestRefreshHandler = new Handler(); startLoadingManifest(); @@ -244,8 +243,8 @@ public final class DashMediaSource implements MediaSource { int periodCount = manifest.getPeriodCount(); periods = new DashMediaPeriod[periodCount]; for (int i = 0; i < periodCount; i++) { - periods[i] = new DashMediaPeriod(manifest, i, dataSourceFactory, formatEvaluatorFactory, - minLoadableRetryCount, eventDispatcher, elapsedRealtimeOffset, loader); + periods[i] = new DashMediaPeriod(manifest, i, chunkSourceFactory, minLoadableRetryCount, + eventDispatcher, elapsedRealtimeOffset, loader); } scheduleManifestRefresh(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java new file mode 100644 index 0000000000..9d3dfdb16b --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -0,0 +1,499 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.dash; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.Format.DecreasingBandwidthComparator; +import com.google.android.exoplayer2.extractor.ChunkIndex; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; +import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; +import com.google.android.exoplayer2.source.BehindLiveWindowException; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.chunk.Chunk; +import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper; +import com.google.android.exoplayer2.source.chunk.ChunkHolder; +import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk; +import com.google.android.exoplayer2.source.chunk.FormatEvaluator; +import com.google.android.exoplayer2.source.chunk.FormatEvaluator.Evaluation; +import com.google.android.exoplayer2.source.chunk.InitializationChunk; +import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk; +import com.google.android.exoplayer2.source.dash.mpd.MediaPresentationDescription; +import com.google.android.exoplayer2.source.dash.mpd.RangedUri; +import com.google.android.exoplayer2.source.dash.mpd.Representation; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException; +import com.google.android.exoplayer2.upstream.Loader; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; + +import android.os.SystemClock; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +/** + * A default {@link DashChunkSource} implementation. + */ +public class DefaultDashChunkSource implements DashChunkSource { + + public static final class Factory implements DashChunkSource.Factory { + + private final FormatEvaluator.Factory formatEvaluatorFactory; + private final DataSource.Factory dataSourceFactory; + + public Factory(DataSource.Factory dataSourceFactory, + FormatEvaluator.Factory formatEvaluatorFactory) { + this.dataSourceFactory = dataSourceFactory; + this.formatEvaluatorFactory = formatEvaluatorFactory; + } + + @Override + public DashChunkSource createDashChunkSource(Loader manifestLoader, + MediaPresentationDescription manifest, int periodIndex, int adaptationSetIndex, + TrackGroup trackGroup, int[] tracks, long elapsedRealtimeOffsetMs) { + FormatEvaluator adaptiveEvaluator = tracks.length > 1 + ? formatEvaluatorFactory.createFormatEvaluator() : null; + DataSource dataSource = dataSourceFactory.createDataSource(); + return new DefaultDashChunkSource(manifestLoader, manifest, periodIndex, adaptationSetIndex, + trackGroup, tracks, dataSource, adaptiveEvaluator, elapsedRealtimeOffsetMs); + } + + } + + private final Loader manifestLoader; + private final int adaptationSetIndex; + private final TrackGroup trackGroup; + private final RepresentationHolder[] representationHolders; + private final Format[] enabledFormats; + private final boolean[] adaptiveFormatBlacklistFlags; + private final DataSource dataSource; + private final FormatEvaluator adaptiveFormatEvaluator; + private final long elapsedRealtimeOffsetUs; + private final Evaluation evaluation; + + private MediaPresentationDescription manifest; + + private boolean lastChunkWasInitialization; + private IOException fatalError; + private boolean missingLastSegment; + + /** + * @param manifestLoader The {@link Loader} being used to load manifests. + * @param manifest The initial manifest. + * @param periodIndex The index of the period in the manifest. + * @param adaptationSetIndex The index of the adaptation set in the period. + * @param trackGroup The track group corresponding to the adaptation set. + * @param tracks The indices of the selected tracks within the adaptation set. + * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats. + * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between + * server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified + * as the server's unix time minus the local elapsed time. If unknown, set to 0. + */ + public DefaultDashChunkSource(Loader manifestLoader, MediaPresentationDescription manifest, + int periodIndex, int adaptationSetIndex, TrackGroup trackGroup, int[] tracks, + DataSource dataSource, FormatEvaluator adaptiveFormatEvaluator, + long elapsedRealtimeOffsetMs) { + this.manifestLoader = manifestLoader; + this.manifest = manifest; + this.adaptationSetIndex = adaptationSetIndex; + this.trackGroup = trackGroup; + this.dataSource = dataSource; + this.adaptiveFormatEvaluator = adaptiveFormatEvaluator; + this.elapsedRealtimeOffsetUs = elapsedRealtimeOffsetMs * 1000; + this.evaluation = new Evaluation(); + + long periodDurationUs = getPeriodDurationUs(periodIndex); + List representations = getRepresentations(periodIndex); + representationHolders = new RepresentationHolder[representations.size()]; + + for (int i = 0; i < representations.size(); i++) { + Representation representation = representations.get(i); + representationHolders[i] = new RepresentationHolder(periodDurationUs, representation); + } + enabledFormats = new Format[tracks.length]; + for (int i = 0; i < tracks.length; i++) { + enabledFormats[i] = trackGroup.getFormat(tracks[i]); + } + Arrays.sort(enabledFormats, new DecreasingBandwidthComparator()); + if (adaptiveFormatEvaluator != null) { + adaptiveFormatEvaluator.enable(enabledFormats); + adaptiveFormatBlacklistFlags = new boolean[tracks.length]; + } else { + adaptiveFormatBlacklistFlags = null; + } + } + + @Override + public void updateManifest(MediaPresentationDescription newManifest, int periodIndex) { + try { + manifest = newManifest; + long periodDurationUs = getPeriodDurationUs(periodIndex); + List representations = getRepresentations(periodIndex); + for (int i = 0; i < representationHolders.length; i++) { + Representation representation = representations.get(i); + representationHolders[i].updateRepresentation(periodDurationUs, representation); + } + } catch (BehindLiveWindowException e) { + fatalError = e; + } + } + + @Override + public void maybeThrowError() throws IOException { + if (fatalError != null) { + throw fatalError; + } else { + manifestLoader.maybeThrowError(); + } + } + + @Override + public int getPreferredQueueSize(long playbackPositionUs, List queue) { + if (fatalError != null || enabledFormats.length < 2) { + return queue.size(); + } + return adaptiveFormatEvaluator.evaluateQueueSize(playbackPositionUs, queue, + adaptiveFormatBlacklistFlags); + } + + @Override + public final void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) { + if (fatalError != null) { + return; + } + + if (evaluation.format == null || !lastChunkWasInitialization) { + if (enabledFormats.length > 1) { + long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; + adaptiveFormatEvaluator.evaluateFormat(bufferedDurationUs, adaptiveFormatBlacklistFlags, + evaluation); + } else { + evaluation.format = enabledFormats[0]; + evaluation.trigger = FormatEvaluator.TRIGGER_UNKNOWN; + evaluation.data = null; + } + } + + Format selectedFormat = evaluation.format; + if (selectedFormat == null) { + return; + } + + RepresentationHolder representationHolder = + representationHolders[getTrackIndex(selectedFormat)]; + Representation selectedRepresentation = representationHolder.representation; + DashSegmentIndex segmentIndex = representationHolder.segmentIndex; + + RangedUri pendingInitializationUri = null; + RangedUri pendingIndexUri = null; + Format sampleFormat = representationHolder.sampleFormat; + if (sampleFormat == null) { + pendingInitializationUri = selectedRepresentation.getInitializationUri(); + } + if (segmentIndex == null) { + pendingIndexUri = selectedRepresentation.getIndexUri(); + } + if (pendingInitializationUri != null || pendingIndexUri != null) { + // We have initialization and/or index requests to make. + Chunk initializationChunk = newInitializationChunk(representationHolder, dataSource, + selectedFormat, pendingInitializationUri, pendingIndexUri, evaluation.trigger, + evaluation.data); + lastChunkWasInitialization = true; + out.chunk = initializationChunk; + return; + } + + long nowUs = getNowUnixTimeUs(); + int firstAvailableSegmentNum = representationHolder.getFirstSegmentNum(); + int lastAvailableSegmentNum = representationHolder.getLastSegmentNum(); + boolean indexUnbounded = lastAvailableSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED; + if (indexUnbounded) { + // The index is itself unbounded. We need to use the current time to calculate the range of + // available segments. + long liveEdgeTimestampUs = nowUs - manifest.availabilityStartTime * 1000; + if (manifest.timeShiftBufferDepth != -1) { + long bufferDepthUs = manifest.timeShiftBufferDepth * 1000; + firstAvailableSegmentNum = Math.max(firstAvailableSegmentNum, + representationHolder.getSegmentNum(liveEdgeTimestampUs - bufferDepthUs)); + } + // getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the + // index of the last completed segment. + lastAvailableSegmentNum = representationHolder.getSegmentNum(liveEdgeTimestampUs) - 1; + } + + int segmentNum; + if (previous == null) { + segmentNum = Util.constrainValue(representationHolder.getSegmentNum(playbackPositionUs), + firstAvailableSegmentNum, lastAvailableSegmentNum); + } else { + segmentNum = previous.getNextChunkIndex(); + if (segmentNum < firstAvailableSegmentNum) { + // This is before the first chunk in the current manifest. + fatalError = new BehindLiveWindowException(); + return; + } + } + + if (segmentNum > lastAvailableSegmentNum + || (missingLastSegment && segmentNum >= lastAvailableSegmentNum)) { + // This is beyond the last chunk in the current manifest. + out.endOfStream = !manifest.dynamic; + return; + } + + Chunk nextMediaChunk = newMediaChunk(representationHolder, dataSource, selectedFormat, + sampleFormat, segmentNum, evaluation.trigger, evaluation.data); + lastChunkWasInitialization = false; + out.chunk = nextMediaChunk; + } + + @Override + public void onChunkLoadCompleted(Chunk chunk) { + if (chunk instanceof InitializationChunk) { + InitializationChunk initializationChunk = (InitializationChunk) chunk; + RepresentationHolder representationHolder = + representationHolders[getTrackIndex(initializationChunk.format)]; + Format sampleFormat = initializationChunk.getSampleFormat(); + if (sampleFormat != null) { + representationHolder.setSampleFormat(sampleFormat); + } + // The null check avoids overwriting an index obtained from the manifest with one obtained + // from the stream. If the manifest defines an index then the stream shouldn't, but in cases + // where it does we should ignore it. + if (representationHolder.segmentIndex == null) { + SeekMap seekMap = initializationChunk.getSeekMap(); + if (seekMap != null) { + representationHolder.segmentIndex = new DashWrappingSegmentIndex((ChunkIndex) seekMap, + initializationChunk.dataSpec.uri.toString()); + } + } + } + } + + @Override + public boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e) { + // Workaround for missing segment at the end of the period + if (cancelable && !manifest.dynamic && chunk instanceof MediaChunk + && e instanceof InvalidResponseCodeException + && ((InvalidResponseCodeException) e).responseCode == 404) { + RepresentationHolder representationHolder = + representationHolders[getTrackIndex(chunk.format)]; + int lastAvailableSegmentNum = representationHolder.getLastSegmentNum(); + if (((MediaChunk) chunk).chunkIndex >= lastAvailableSegmentNum) { + missingLastSegment = true; + return true; + } + } + // TODO: Consider implementing representation blacklisting. + return false; + } + + @Override + public void release() { + if (adaptiveFormatEvaluator != null) { + adaptiveFormatEvaluator.disable(); + } + } + + // Private methods. + + private List getRepresentations(int periodIndex) { + return manifest.getPeriod(periodIndex).adaptationSets.get(adaptationSetIndex).representations; + } + + private long getNowUnixTimeUs() { + if (elapsedRealtimeOffsetUs != 0) { + return (SystemClock.elapsedRealtime() * 1000) + elapsedRealtimeOffsetUs; + } else { + return System.currentTimeMillis() * 1000; + } + } + + private Chunk newInitializationChunk(RepresentationHolder representationHolder, + DataSource dataSource, Format trackFormat, RangedUri initializationUri, RangedUri indexUri, + int formatEvaluatorTrigger, Object formatEvaluatorData) { + RangedUri requestUri; + if (initializationUri != null) { + // It's common for initialization and index data to be stored adjacently. Attempt to merge + // the two requests together to request both at once. + requestUri = initializationUri.attemptMerge(indexUri); + if (requestUri == null) { + requestUri = initializationUri; + } + } else { + requestUri = indexUri; + } + DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length, + representationHolder.representation.getCacheKey()); + return new InitializationChunk(dataSource, dataSpec, trackFormat, + formatEvaluatorTrigger, formatEvaluatorData, representationHolder.extractorWrapper); + } + + private Chunk newMediaChunk(RepresentationHolder representationHolder, DataSource dataSource, + Format trackFormat, Format sampleFormat, int segmentNum, int formatEvaluatorTrigger, + Object formatEvaluatorData) { + Representation representation = representationHolder.representation; + long startTimeUs = representationHolder.getSegmentStartTimeUs(segmentNum); + long endTimeUs = representationHolder.getSegmentEndTimeUs(segmentNum); + RangedUri segmentUri = representationHolder.getSegmentUrl(segmentNum); + DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length, + representation.getCacheKey()); + + if (representationHolder.extractorWrapper == null) { + return new SingleSampleMediaChunk(dataSource, dataSpec, trackFormat, formatEvaluatorTrigger, + formatEvaluatorData, startTimeUs, endTimeUs, segmentNum, trackFormat); + } else { + long sampleOffsetUs = -representation.presentationTimeOffsetUs; + return new ContainerMediaChunk(dataSource, dataSpec, trackFormat, formatEvaluatorTrigger, + formatEvaluatorData, startTimeUs, endTimeUs, segmentNum, sampleOffsetUs, + representationHolder.extractorWrapper, sampleFormat); + } + } + + private int getTrackIndex(Format format) { + for (int i = 0; i < trackGroup.length; i++) { + if (trackGroup.getFormat(i) == format) { + return i; + } + } + // Should never happen. + throw new IllegalStateException("Invalid format: " + format); + } + + private long getPeriodDurationUs(int periodIndex) { + long durationMs = manifest.getPeriodDuration(periodIndex); + if (durationMs == -1) { + return C.UNSET_TIME_US; + } else { + return durationMs * 1000; + } + } + + // Protected classes. + + protected static final class RepresentationHolder { + + public final ChunkExtractorWrapper extractorWrapper; + + public Representation representation; + public DashSegmentIndex segmentIndex; + public Format sampleFormat; + + private long periodDurationUs; + private int segmentNumShift; + + public RepresentationHolder(long periodDurationUs, Representation representation) { + this.periodDurationUs = periodDurationUs; + this.representation = representation; + String containerMimeType = representation.format.containerMimeType; + // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, + // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. + extractorWrapper = mimeTypeIsRawText(containerMimeType) ? null : new ChunkExtractorWrapper( + mimeTypeIsWebm(containerMimeType) ? new MatroskaExtractor() + : new FragmentedMp4Extractor(), + representation.format, true /* preferManifestDrmInitData */); + segmentIndex = representation.getIndex(); + } + + public void setSampleFormat(Format sampleFormat) { + this.sampleFormat = sampleFormat; + } + + public void updateRepresentation(long newPeriodDurationUs, Representation newRepresentation) + throws BehindLiveWindowException{ + DashSegmentIndex oldIndex = representation.getIndex(); + DashSegmentIndex newIndex = newRepresentation.getIndex(); + + periodDurationUs = newPeriodDurationUs; + representation = newRepresentation; + if (oldIndex == null) { + // Segment numbers cannot shift if the index isn't defined by the manifest. + return; + } + + segmentIndex = newIndex; + if (!oldIndex.isExplicit()) { + // Segment numbers cannot shift if the index isn't explicit. + return; + } + + int oldIndexLastSegmentNum = oldIndex.getLastSegmentNum(periodDurationUs); + long oldIndexEndTimeUs = oldIndex.getTimeUs(oldIndexLastSegmentNum) + + oldIndex.getDurationUs(oldIndexLastSegmentNum, periodDurationUs); + int newIndexFirstSegmentNum = newIndex.getFirstSegmentNum(); + long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum); + if (oldIndexEndTimeUs == newIndexStartTimeUs) { + // The new index continues where the old one ended, with no overlap. + segmentNumShift += oldIndex.getLastSegmentNum(periodDurationUs) + 1 + - newIndexFirstSegmentNum; + } else if (oldIndexEndTimeUs < newIndexStartTimeUs) { + // There's a gap between the old index and the new one which means we've slipped behind the + // live window and can't proceed. + throw new BehindLiveWindowException(); + } else { + // The new index overlaps with the old one. + segmentNumShift += oldIndex.getSegmentNum(newIndexStartTimeUs, periodDurationUs) + - newIndexFirstSegmentNum; + } + } + + public int getFirstSegmentNum() { + return segmentIndex.getFirstSegmentNum() + segmentNumShift; + } + + public int getLastSegmentNum() { + int lastSegmentNum = segmentIndex.getLastSegmentNum(periodDurationUs); + if (lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED) { + return DashSegmentIndex.INDEX_UNBOUNDED; + } + return lastSegmentNum + segmentNumShift; + } + + public long getSegmentStartTimeUs(int segmentNum) { + return segmentIndex.getTimeUs(segmentNum - segmentNumShift); + } + + public long getSegmentEndTimeUs(int segmentNum) { + return getSegmentStartTimeUs(segmentNum) + + segmentIndex.getDurationUs(segmentNum - segmentNumShift, periodDurationUs); + } + + public int getSegmentNum(long positionUs) { + return segmentIndex.getSegmentNum(positionUs, periodDurationUs) + segmentNumShift; + } + + public RangedUri getSegmentUrl(int segmentNum) { + return segmentIndex.getSegmentUrl(segmentNum - segmentNumShift); + } + + private static boolean mimeTypeIsWebm(String mimeType) { + return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM) + || mimeType.startsWith(MimeTypes.APPLICATION_WEBM); + } + + private static boolean mimeTypeIsRawText(String mimeType) { + return MimeTypes.isText(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType); + } + + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSmoothStreamingChunkSource.java new file mode 100644 index 0000000000..f65a10f26b --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSmoothStreamingChunkSource.java @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.smoothstreaming; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.Format.DecreasingBandwidthComparator; +import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; +import com.google.android.exoplayer2.extractor.mp4.Track; +import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; +import com.google.android.exoplayer2.source.BehindLiveWindowException; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.chunk.Chunk; +import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper; +import com.google.android.exoplayer2.source.chunk.ChunkHolder; +import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk; +import com.google.android.exoplayer2.source.chunk.FormatEvaluator; +import com.google.android.exoplayer2.source.chunk.FormatEvaluator.Evaluation; +import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.source.smoothstreaming.SmoothStreamingManifest.StreamElement; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.Loader; + +import android.net.Uri; +import android.text.TextUtils; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +/** + * A default {@link SmoothStreamingChunkSource} implementation. + */ +public class DefaultSmoothStreamingChunkSource implements SmoothStreamingChunkSource { + + public static final class Factory implements SmoothStreamingChunkSource.Factory { + + private final FormatEvaluator.Factory formatEvaluatorFactory; + private final DataSource.Factory dataSourceFactory; + + public Factory(DataSource.Factory dataSourceFactory, + FormatEvaluator.Factory formatEvaluatorFactory) { + this.dataSourceFactory = dataSourceFactory; + this.formatEvaluatorFactory = formatEvaluatorFactory; + } + + @Override + public SmoothStreamingChunkSource createChunkSource(Loader manifestLoader, + SmoothStreamingManifest manifest, int elementIndex, TrackGroup trackGroup, int[] tracks, + TrackEncryptionBox[] trackEncryptionBoxes) { + FormatEvaluator adaptiveEvaluator = tracks.length > 1 + ? formatEvaluatorFactory.createFormatEvaluator() : null; + DataSource dataSource = dataSourceFactory.createDataSource(); + return new DefaultSmoothStreamingChunkSource(manifestLoader, manifest, elementIndex, + trackGroup, tracks, dataSource, adaptiveEvaluator, + trackEncryptionBoxes); + } + + } + + private final Loader manifestLoader; + private final int elementIndex; + private final TrackGroup trackGroup; + private final ChunkExtractorWrapper[] extractorWrappers; + private final Format[] enabledFormats; + private final boolean[] adaptiveFormatBlacklistFlags; + private final DataSource dataSource; + private final Evaluation evaluation; + private final FormatEvaluator adaptiveFormatEvaluator; + + private SmoothStreamingManifest manifest; + private int currentManifestChunkOffset; + + private IOException fatalError; + + /** + * @param manifestLoader The {@link Loader} being used to load manifests. + * @param manifest The initial manifest. + * @param elementIndex The index of the stream element in the manifest. + * @param trackGroup The track group corresponding to the stream element. + * @param tracks The indices of the selected tracks within the stream element. + * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats. + * @param trackEncryptionBoxes Track encryption boxes for the stream. + */ + public DefaultSmoothStreamingChunkSource(Loader manifestLoader, SmoothStreamingManifest manifest, + int elementIndex, TrackGroup trackGroup, int[] tracks, DataSource dataSource, + FormatEvaluator adaptiveFormatEvaluator, TrackEncryptionBox[] trackEncryptionBoxes) { + this.manifestLoader = manifestLoader; + this.manifest = manifest; + this.elementIndex = elementIndex; + this.trackGroup = trackGroup; + this.dataSource = dataSource; + this.adaptiveFormatEvaluator = adaptiveFormatEvaluator; + this.evaluation = new Evaluation(); + + StreamElement streamElement = manifest.streamElements[elementIndex]; + Format[] formats = streamElement.formats; + extractorWrappers = new ChunkExtractorWrapper[formats.length]; + for (int j = 0; j < formats.length; j++) { + int nalUnitLengthFieldLength = streamElement.type == C.TRACK_TYPE_VIDEO ? 4 : -1; + Track track = new Track(j, streamElement.type, streamElement.timescale, C.UNSET_TIME_US, + manifest.durationUs, formats[j], Track.TRANSFORMATION_NONE, trackEncryptionBoxes, + nalUnitLengthFieldLength, null, null); + FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( + FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME + | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, track); + extractorWrappers[j] = new ChunkExtractorWrapper(extractor, formats[j], false); + } + + enabledFormats = new Format[tracks.length]; + for (int i = 0; i < tracks.length; i++) { + enabledFormats[i] = trackGroup.getFormat(tracks[i]); + } + Arrays.sort(enabledFormats, new DecreasingBandwidthComparator()); + if (adaptiveFormatEvaluator != null) { + adaptiveFormatEvaluator.enable(enabledFormats); + adaptiveFormatBlacklistFlags = new boolean[tracks.length]; + } else { + adaptiveFormatBlacklistFlags = null; + } + } + + @Override + public void updateManifest(SmoothStreamingManifest newManifest) { + StreamElement currentElement = manifest.streamElements[elementIndex]; + int currentElementChunkCount = currentElement.chunkCount; + StreamElement newElement = newManifest.streamElements[elementIndex]; + if (currentElementChunkCount == 0 || newElement.chunkCount == 0) { + // There's no overlap between the old and new elements because at least one is empty. + currentManifestChunkOffset += currentElementChunkCount; + } else { + long currentElementEndTimeUs = currentElement.getStartTimeUs(currentElementChunkCount - 1) + + currentElement.getChunkDurationUs(currentElementChunkCount - 1); + long newElementStartTimeUs = newElement.getStartTimeUs(0); + if (currentElementEndTimeUs <= newElementStartTimeUs) { + // There's no overlap between the old and new elements. + currentManifestChunkOffset += currentElementChunkCount; + } else { + // The new element overlaps with the old one. + currentManifestChunkOffset += currentElement.getChunkIndex(newElementStartTimeUs); + } + } + manifest = newManifest; + } + + // ChunkSource implementation. + + @Override + public void maybeThrowError() throws IOException { + if (fatalError != null) { + throw fatalError; + } else { + manifestLoader.maybeThrowError(); + } + } + + @Override + public int getPreferredQueueSize(long playbackPositionUs, List queue) { + if (fatalError != null || enabledFormats.length < 2) { + return queue.size(); + } + return adaptiveFormatEvaluator.evaluateQueueSize(playbackPositionUs, queue, + adaptiveFormatBlacklistFlags); + } + + @Override + public final void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) { + if (fatalError != null) { + return; + } + + if (enabledFormats.length > 1) { + long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; + adaptiveFormatEvaluator.evaluateFormat(bufferedDurationUs, adaptiveFormatBlacklistFlags, + evaluation); + } else { + evaluation.format = enabledFormats[0]; + evaluation.trigger = FormatEvaluator.TRIGGER_UNKNOWN; + evaluation.data = null; + } + + Format selectedFormat = evaluation.format; + if (selectedFormat == null) { + return; + } + + StreamElement streamElement = manifest.streamElements[elementIndex]; + if (streamElement.chunkCount == 0) { + // There aren't any chunks for us to load. + out.endOfStream = !manifest.isLive; + return; + } + + int chunkIndex; + if (previous == null) { + chunkIndex = streamElement.getChunkIndex(playbackPositionUs); + } else { + chunkIndex = previous.getNextChunkIndex() - currentManifestChunkOffset; + if (chunkIndex < 0) { + // This is before the first chunk in the current manifest. + fatalError = new BehindLiveWindowException(); + return; + } + } + + if (chunkIndex >= streamElement.chunkCount) { + // This is beyond the last chunk in the current manifest. + out.endOfStream = !manifest.isLive; + return; + } + + long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex); + long chunkEndTimeUs = chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex); + int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset; + + int trackGroupTrackIndex = getTrackGroupTrackIndex(trackGroup, selectedFormat); + ChunkExtractorWrapper extractorWrapper = extractorWrappers[trackGroupTrackIndex]; + + int manifestTrackIndex = getManifestTrackIndex(streamElement, selectedFormat); + Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex); + + out.chunk = newMediaChunk(selectedFormat, dataSource, uri, null, currentAbsoluteChunkIndex, + chunkStartTimeUs, chunkEndTimeUs, evaluation.trigger, evaluation.data, extractorWrapper); + } + + @Override + public void onChunkLoadCompleted(Chunk chunk) { + // Do nothing. + } + + @Override + public boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e) { + // TODO: Consider implementing stream element blacklisting. + return false; + } + + @Override + public void release() { + if (adaptiveFormatEvaluator != null) { + adaptiveFormatEvaluator.disable(); + } + } + + // Private methods. + + /** + * Gets the index of a format in a track group, using referential equality. + */ + private static int getTrackGroupTrackIndex(TrackGroup trackGroup, Format format) { + for (int i = 0; i < trackGroup.length; i++) { + if (trackGroup.getFormat(i) == format) { + return i; + } + } + // Should never happen. + throw new IllegalStateException("Invalid format: " + format); + } + + /** + * Gets the index of a format in an element, using format.id equality. + *

+ * This method will return the same index as {@link #getTrackGroupTrackIndex(TrackGroup, Format)} + * except in the case where a live manifest is refreshed and the ordering of the tracks in the + * manifest has changed. + */ + private static int getManifestTrackIndex(StreamElement element, Format format) { + Format[] formats = element.formats; + for (int i = 0; i < formats.length; i++) { + if (TextUtils.equals(formats[i].id, format.id)) { + return i; + } + } + // Should never happen. + throw new IllegalStateException("Invalid format: " + format); + } + + private static MediaChunk newMediaChunk(Format format, DataSource dataSource, Uri uri, + String cacheKey, int chunkIndex, long chunkStartTimeUs, long chunkEndTimeUs, + int formatEvaluatorTrigger, Object formatEvaluatorData, + ChunkExtractorWrapper extractorWrapper) { + DataSpec dataSpec = new DataSpec(uri, 0, -1, cacheKey); + // In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk. + // To convert them the absolute timestamps, we need to set sampleOffsetUs to chunkStartTimeUs. + long sampleOffsetUs = chunkStartTimeUs; + return new ContainerMediaChunk(dataSource, dataSpec, format, formatEvaluatorTrigger, + formatEvaluatorData, chunkStartTimeUs, chunkEndTimeUs, chunkIndex, sampleOffsetUs, + extractorWrapper, format); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SmoothStreamingChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SmoothStreamingChunkSource.java index 2f8a1c07f9..7772a28e5f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SmoothStreamingChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SmoothStreamingChunkSource.java @@ -15,266 +15,24 @@ */ package com.google.android.exoplayer2.source.smoothstreaming; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.Format.DecreasingBandwidthComparator; -import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; -import com.google.android.exoplayer2.extractor.mp4.Track; import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; -import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.chunk.Chunk; -import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper; -import com.google.android.exoplayer2.source.chunk.ChunkHolder; import com.google.android.exoplayer2.source.chunk.ChunkSource; -import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk; -import com.google.android.exoplayer2.source.chunk.FormatEvaluator; -import com.google.android.exoplayer2.source.chunk.FormatEvaluator.Evaluation; -import com.google.android.exoplayer2.source.chunk.MediaChunk; -import com.google.android.exoplayer2.source.smoothstreaming.SmoothStreamingManifest.StreamElement; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.Loader; -import android.net.Uri; -import android.text.TextUtils; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; - /** - * An {@link ChunkSource} for SmoothStreaming. + * A {@link ChunkSource} for SmoothStreaming. */ -public class SmoothStreamingChunkSource implements ChunkSource { +public interface SmoothStreamingChunkSource extends ChunkSource { - private final Loader manifestLoader; - private final int elementIndex; - private final TrackGroup trackGroup; - private final ChunkExtractorWrapper[] extractorWrappers; - private final Format[] enabledFormats; - private final boolean[] adaptiveFormatBlacklistFlags; - private final DataSource dataSource; - private final Evaluation evaluation; - private final FormatEvaluator adaptiveFormatEvaluator; + interface Factory { - private SmoothStreamingManifest manifest; - private int currentManifestChunkOffset; + SmoothStreamingChunkSource createChunkSource(Loader manifestLoader, + SmoothStreamingManifest manifest, int elementIndex, TrackGroup trackGroup, int[] tracks, + TrackEncryptionBox[] trackEncryptionBoxes); - private IOException fatalError; - - /** - * @param manifestLoader The {@link Loader} being used to load manifests. - * @param manifest The initial manifest. - * @param elementIndex The index of the stream element in the manifest. - * @param trackGroup The track group corresponding to the stream element. - * @param tracks The indices of the selected tracks within the stream element. - * @param dataSource A {@link DataSource} suitable for loading the media data. - * @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats. - * @param trackEncryptionBoxes Track encryption boxes for the stream. - */ - public SmoothStreamingChunkSource(Loader manifestLoader, SmoothStreamingManifest manifest, - int elementIndex, TrackGroup trackGroup, int[] tracks, DataSource dataSource, - FormatEvaluator adaptiveFormatEvaluator, TrackEncryptionBox[] trackEncryptionBoxes) { - this.manifestLoader = manifestLoader; - this.manifest = manifest; - this.elementIndex = elementIndex; - this.trackGroup = trackGroup; - this.dataSource = dataSource; - this.adaptiveFormatEvaluator = adaptiveFormatEvaluator; - this.evaluation = new Evaluation(); - - StreamElement streamElement = manifest.streamElements[elementIndex]; - Format[] formats = streamElement.formats; - extractorWrappers = new ChunkExtractorWrapper[formats.length]; - for (int j = 0; j < formats.length; j++) { - int nalUnitLengthFieldLength = streamElement.type == C.TRACK_TYPE_VIDEO ? 4 : -1; - Track track = new Track(j, streamElement.type, streamElement.timescale, C.UNSET_TIME_US, - manifest.durationUs, formats[j], Track.TRANSFORMATION_NONE, trackEncryptionBoxes, - nalUnitLengthFieldLength, null, null); - FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( - FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME - | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, track); - extractorWrappers[j] = new ChunkExtractorWrapper(extractor, formats[j], false); - } - - enabledFormats = new Format[tracks.length]; - for (int i = 0; i < tracks.length; i++) { - enabledFormats[i] = trackGroup.getFormat(tracks[i]); - } - Arrays.sort(enabledFormats, new DecreasingBandwidthComparator()); - if (adaptiveFormatEvaluator != null) { - adaptiveFormatEvaluator.enable(enabledFormats); - adaptiveFormatBlacklistFlags = new boolean[tracks.length]; - } else { - adaptiveFormatBlacklistFlags = null; - } } - public void updateManifest(SmoothStreamingManifest newManifest) { - StreamElement currentElement = manifest.streamElements[elementIndex]; - int currentElementChunkCount = currentElement.chunkCount; - StreamElement newElement = newManifest.streamElements[elementIndex]; - if (currentElementChunkCount == 0 || newElement.chunkCount == 0) { - // There's no overlap between the old and new elements because at least one is empty. - currentManifestChunkOffset += currentElementChunkCount; - } else { - long currentElementEndTimeUs = currentElement.getStartTimeUs(currentElementChunkCount - 1) - + currentElement.getChunkDurationUs(currentElementChunkCount - 1); - long newElementStartTimeUs = newElement.getStartTimeUs(0); - if (currentElementEndTimeUs <= newElementStartTimeUs) { - // There's no overlap between the old and new elements. - currentManifestChunkOffset += currentElementChunkCount; - } else { - // The new element overlaps with the old one. - currentManifestChunkOffset += currentElement.getChunkIndex(newElementStartTimeUs); - } - } - manifest = newManifest; - } - - // ChunkSource implementation. - - @Override - public void maybeThrowError() throws IOException { - if (fatalError != null) { - throw fatalError; - } else { - manifestLoader.maybeThrowError(); - } - } - - @Override - public int getPreferredQueueSize(long playbackPositionUs, List queue) { - if (fatalError != null || enabledFormats.length < 2) { - return queue.size(); - } - return adaptiveFormatEvaluator.evaluateQueueSize(playbackPositionUs, queue, - adaptiveFormatBlacklistFlags); - } - - @Override - public final void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) { - if (fatalError != null) { - return; - } - - if (enabledFormats.length > 1) { - long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; - adaptiveFormatEvaluator.evaluateFormat(bufferedDurationUs, adaptiveFormatBlacklistFlags, - evaluation); - } else { - evaluation.format = enabledFormats[0]; - evaluation.trigger = FormatEvaluator.TRIGGER_UNKNOWN; - evaluation.data = null; - } - - Format selectedFormat = evaluation.format; - if (selectedFormat == null) { - return; - } - - StreamElement streamElement = manifest.streamElements[elementIndex]; - if (streamElement.chunkCount == 0) { - // There aren't any chunks for us to load. - out.endOfStream = !manifest.isLive; - return; - } - - int chunkIndex; - if (previous == null) { - chunkIndex = streamElement.getChunkIndex(playbackPositionUs); - } else { - chunkIndex = previous.getNextChunkIndex() - currentManifestChunkOffset; - if (chunkIndex < 0) { - // This is before the first chunk in the current manifest. - fatalError = new BehindLiveWindowException(); - return; - } - } - - if (chunkIndex >= streamElement.chunkCount) { - // This is beyond the last chunk in the current manifest. - out.endOfStream = !manifest.isLive; - return; - } - - long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex); - long chunkEndTimeUs = chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex); - int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset; - - int trackGroupTrackIndex = getTrackGroupTrackIndex(trackGroup, selectedFormat); - ChunkExtractorWrapper extractorWrapper = extractorWrappers[trackGroupTrackIndex]; - - int manifestTrackIndex = getManifestTrackIndex(streamElement, selectedFormat); - Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex); - - out.chunk = newMediaChunk(selectedFormat, dataSource, uri, null, currentAbsoluteChunkIndex, - chunkStartTimeUs, chunkEndTimeUs, evaluation.trigger, evaluation.data, extractorWrapper); - } - - @Override - public void onChunkLoadCompleted(Chunk chunk) { - // Do nothing. - } - - @Override - public boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e) { - // TODO: Consider implementing stream element blacklisting. - return false; - } - - @Override - public void release() { - if (adaptiveFormatEvaluator != null) { - adaptiveFormatEvaluator.disable(); - } - } - - // Private methods. - - /** - * Gets the index of a format in a track group, using referential equality. - */ - private static int getTrackGroupTrackIndex(TrackGroup trackGroup, Format format) { - for (int i = 0; i < trackGroup.length; i++) { - if (trackGroup.getFormat(i) == format) { - return i; - } - } - // Should never happen. - throw new IllegalStateException("Invalid format: " + format); - } - - /** - * Gets the index of a format in an element, using format.id equality. - *

- * This method will return the same index as {@link #getTrackGroupTrackIndex(TrackGroup, Format)} - * except in the case where a live manifest is refreshed and the ordering of the tracks in the - * manifest has changed. - */ - private static int getManifestTrackIndex(StreamElement element, Format format) { - Format[] formats = element.formats; - for (int i = 0; i < formats.length; i++) { - if (TextUtils.equals(formats[i].id, format.id)) { - return i; - } - } - // Should never happen. - throw new IllegalStateException("Invalid format: " + format); - } - - private static MediaChunk newMediaChunk(Format format, DataSource dataSource, Uri uri, - String cacheKey, int chunkIndex, long chunkStartTimeUs, long chunkEndTimeUs, - int formatEvaluatorTrigger, Object formatEvaluatorData, - ChunkExtractorWrapper extractorWrapper) { - DataSpec dataSpec = new DataSpec(uri, 0, -1, cacheKey); - // In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk. - // To convert them the absolute timestamps, we need to set sampleOffsetUs to chunkStartTimeUs. - long sampleOffsetUs = chunkStartTimeUs; - return new ContainerMediaChunk(dataSource, dataSpec, format, formatEvaluatorTrigger, - formatEvaluatorData, chunkStartTimeUs, chunkEndTimeUs, chunkIndex, sampleOffsetUs, - extractorWrapper, format); - } + void updateManifest(SmoothStreamingManifest newManifest); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SmoothStreamingMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SmoothStreamingMediaSource.java index 4c7a962409..c1c5bee8c6 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SmoothStreamingMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SmoothStreamingMediaSource.java @@ -29,7 +29,6 @@ import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.chunk.ChunkSampleStream; -import com.google.android.exoplayer2.source.chunk.FormatEvaluator; import com.google.android.exoplayer2.source.smoothstreaming.SmoothStreamingManifest.ProtectionElement; import com.google.android.exoplayer2.source.smoothstreaming.SmoothStreamingManifest.StreamElement; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -66,7 +65,7 @@ public final class SmoothStreamingMediaSource implements MediaPeriod, MediaSourc private final Uri manifestUri; private final DataSource.Factory dataSourceFactory; - private final FormatEvaluator.Factory formatEvaluatorFactory; + private final SmoothStreamingChunkSource.Factory chunkSourceFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; private final SmoothStreamingManifestParser manifestParser; @@ -88,20 +87,20 @@ public final class SmoothStreamingMediaSource implements MediaPeriod, MediaSourc private TrackGroupArray trackGroups; private int[] trackGroupElementIndices; - public SmoothStreamingMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, - FormatEvaluator.Factory formatEvaluatorFactory, Handler eventHandler, + public SmoothStreamingMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, + SmoothStreamingChunkSource.Factory chunkSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { - this(manifestUri, dataSourceFactory, formatEvaluatorFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, - eventHandler, eventListener); + this(manifestUri, manifestDataSourceFactory, chunkSourceFactory, + DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, eventListener); } public SmoothStreamingMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, - FormatEvaluator.Factory formatEvaluatorFactory, int minLoadableRetryCount, + SmoothStreamingChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this.manifestUri = Util.toLowerInvariant(manifestUri.getLastPathSegment()).equals("manifest") ? manifestUri : Uri.withAppendedPath(manifestUri, "Manifest"); this.dataSourceFactory = dataSourceFactory; - this.formatEvaluatorFactory = formatEvaluatorFactory; + this.chunkSourceFactory = chunkSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); manifestParser = new SmoothStreamingManifestParser(); @@ -354,17 +353,12 @@ public final class SmoothStreamingMediaSource implements MediaPeriod, MediaSourc private ChunkSampleStream buildSampleStream(TrackSelection selection, long positionUs) { int[] selectedTracks = selection.getTracks(); - FormatEvaluator adaptiveEvaluator = selectedTracks.length > 1 - ? formatEvaluatorFactory.createFormatEvaluator() : null; int streamElementIndex = trackGroupElementIndices[selection.group]; - StreamElement streamElement = manifest.streamElements[streamElementIndex]; - int streamElementType = streamElement.type; - DataSource dataSource = dataSourceFactory.createDataSource(); - SmoothStreamingChunkSource chunkSource = new SmoothStreamingChunkSource(manifestLoader, - manifest, streamElementIndex, trackGroups.get(selection.group), selectedTracks, dataSource, - adaptiveEvaluator, trackEncryptionBoxes); - return new ChunkSampleStream<>(streamElementType, chunkSource, this, allocator, positionUs, - minLoadableRetryCount, eventDispatcher); + SmoothStreamingChunkSource chunkSource = chunkSourceFactory.createChunkSource(manifestLoader, + manifest, streamElementIndex, trackGroups.get(selection.group), selectedTracks, + trackEncryptionBoxes); + return new ChunkSampleStream<>(manifest.streamElements[streamElementIndex].type, chunkSource, + this, allocator, positionUs, minLoadableRetryCount, eventDispatcher); } @SuppressWarnings("unchecked") diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java index 9253a6249c..42833f9fee 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.chunk.FormatEvaluator; import com.google.android.exoplayer2.source.chunk.FormatEvaluator.AdaptiveEvaluator; import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.BandwidthMeter; @@ -423,12 +424,15 @@ public final class DashTest extends ActivityInstrumentationTestCase2