From b120bea029a2d1fc8fbcf6058726f5d7e41316be Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 15 Aug 2016 03:32:19 -0700 Subject: [PATCH] Don't reuse MediaPeriods. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=130266483 --- .../exoplayer2/ExoPlayerImplInternal.java | 25 +- .../source/ConcatenatingMediaSource.java | 28 +- .../source/ExtractorMediaPeriod.java | 659 +++++++++++++++++ .../source/ExtractorMediaSource.java | 689 +----------------- .../exoplayer2/source/MediaPeriod.java | 24 +- .../exoplayer2/source/MediaSource.java | 31 +- .../exoplayer2/source/MergingMediaPeriod.java | 31 +- .../exoplayer2/source/MergingMediaSource.java | 26 +- .../source/SingleSampleMediaPeriod.java | 279 +++++++ .../source/SingleSampleMediaSource.java | 271 +------ .../source/dash/DashMediaPeriod.java | 39 +- .../source/dash/DashMediaSource.java | 30 +- .../exoplayer2/source/hls/HlsMediaPeriod.java | 392 ++++++++++ .../exoplayer2/source/hls/HlsMediaSource.java | 397 +--------- .../source/smoothstreaming/SsMediaPeriod.java | 45 +- .../source/smoothstreaming/SsMediaSource.java | 35 +- 16 files changed, 1576 insertions(+), 1425 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java create mode 100644 library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index ffad64152e..2ccb669762 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -229,7 +229,7 @@ import java.io.IOException; // MediaPeriod.Callback implementation. @Override - public void onPeriodPrepared(MediaPeriod source) { + public void onPrepared(MediaPeriod source) { handler.obtainMessage(MSG_PERIOD_PREPARED, source).sendToTarget(); } @@ -906,6 +906,7 @@ import java.io.IOException; public void updatePeriods() throws ExoPlaybackException, IOException { if (timeline == null) { // We're waiting to get information about periods. + mediaSource.maybeThrowSourceInfoRefreshError(); return; } @@ -927,11 +928,14 @@ import java.io.IOException; } } - MediaPeriod mediaPeriod; - if (startPositionUs != C.UNSET_TIME_US - && (mediaPeriod = mediaSource.createPeriod(periodIndex)) != null) { - Period newPeriod = new Period(renderers, rendererCapabilities, trackSelector, mediaPeriod, - timeline.getPeriodId(periodIndex), periodIndex, startPositionUs); + if (periodIndex >= timeline.getPeriodCount()) { + // This period is not available yet. + mediaSource.maybeThrowSourceInfoRefreshError(); + } else if (startPositionUs != C.UNSET_TIME_US) { + MediaPeriod mediaPeriod = mediaSource.createPeriod(periodIndex, this, + loadControl.getAllocator(), startPositionUs); + Period newPeriod = new Period(renderers, rendererCapabilities, trackSelector, mediaSource, + mediaPeriod, timeline.getPeriodId(periodIndex), periodIndex, startPositionUs); newPeriod.isLast = timeline.isFinal() && periodIndex == timeline.getPeriodCount() - 1; if (loadingPeriod != null) { loadingPeriod.setNextPeriod(newPeriod); @@ -941,7 +945,6 @@ import java.io.IOException; bufferAheadPeriodCount++; loadingPeriod = newPeriod; setIsLoading(true); - loadingPeriod.mediaPeriod.preparePeriod(this, loadControl.getAllocator(), startPositionUs); } } @@ -1169,17 +1172,19 @@ import java.io.IOException; private final Renderer[] renderers; private final RendererCapabilities[] rendererCapabilities; private final TrackSelector trackSelector; + private final MediaSource mediaSource; private Object trackSelectionData; private TrackSelectionArray trackSelections; private TrackSelectionArray periodTrackSelections; public Period(Renderer[] renderers, RendererCapabilities[] rendererCapabilities, - TrackSelector trackSelector, MediaPeriod mediaPeriod, Object id, int index, - long positionUs) { + TrackSelector trackSelector, MediaSource mediaSource, MediaPeriod mediaPeriod, Object id, + int index, long positionUs) { this.renderers = renderers; this.rendererCapabilities = rendererCapabilities; this.trackSelector = trackSelector; + this.mediaSource = mediaSource; this.mediaPeriod = mediaPeriod; this.id = Assertions.checkNotNull(id); sampleStreams = new SampleStream[renderers.length]; @@ -1250,7 +1255,7 @@ import java.io.IOException; public void release() { try { - mediaPeriod.releasePeriod(); + mediaSource.releasePeriod(mediaPeriod); } catch (RuntimeException e) { // There's nothing we can do. Log.e(TAG, "Period release failed.", e); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index e01fa17917..6b748bcdff 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -16,9 +16,13 @@ package com.google.android.exoplayer2.source; import android.util.Pair; +import com.google.android.exoplayer2.source.MediaPeriod.Callback; +import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; /** * Concatenates multiple {@link MediaSource}s. @@ -28,6 +32,7 @@ public final class ConcatenatingMediaSource implements MediaSource { private final MediaSource[] mediaSources; private final Timeline[] timelines; private final Object[] manifests; + private final Map sourceIndexByMediaPeriod; private ConcatenatedTimeline timeline; @@ -38,6 +43,7 @@ public final class ConcatenatingMediaSource implements MediaSource { this.mediaSources = mediaSources; timelines = new Timeline[mediaSources.length]; manifests = new Object[mediaSources.length]; + sourceIndexByMediaPeriod = new HashMap<>(); } @Override @@ -85,10 +91,28 @@ public final class ConcatenatingMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int index) throws IOException { + public void maybeThrowSourceInfoRefreshError() throws IOException { + for (MediaSource mediaSource : mediaSources) { + mediaSource.maybeThrowSourceInfoRefreshError(); + } + } + + @Override + public MediaPeriod createPeriod(int index, Callback callback, Allocator allocator, + long positionUs) { int sourceIndex = timeline.getSourceIndexForPeriod(index); int periodIndexInSource = index - timeline.getFirstPeriodIndexInSource(sourceIndex); - return mediaSources[sourceIndex].createPeriod(periodIndexInSource); + MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIndexInSource, callback, + allocator, positionUs); + sourceIndexByMediaPeriod.put(mediaPeriod, sourceIndex); + return mediaPeriod; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + int sourceIndex = sourceIndexByMediaPeriod.get(mediaPeriod); + sourceIndexByMediaPeriod.remove(mediaPeriod); + mediaSources[sourceIndex].releasePeriod(mediaPeriod); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java new file mode 100644 index 0000000000..080386da44 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -0,0 +1,659 @@ +/* + * 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; + +import android.net.Uri; +import android.os.Handler; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.extractor.DefaultExtractorInput; +import com.google.android.exoplayer2.extractor.DefaultTrackOutput; +import com.google.android.exoplayer2.extractor.DefaultTrackOutput.UpstreamFormatChangedListener; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.PositionHolder; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.TrackOutput; +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.DataSpec; +import com.google.android.exoplayer2.upstream.Loader; +import com.google.android.exoplayer2.upstream.Loader.Loadable; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.ConditionVariable; +import java.io.EOFException; +import java.io.IOException; +import java.util.Arrays; + +/** + * A {@link MediaPeriod} that extracts data using an {@link Extractor}. + */ +/* package */ final class ExtractorMediaPeriod implements MediaPeriod, ExtractorOutput, + Loader.Callback, UpstreamFormatChangedListener { + + /** + * When the source's duration is unknown, it is calculated by adding this value to the largest + * sample timestamp seen when buffering completes. + */ + private static final long DEFAULT_LAST_SAMPLE_DURATION_US = 10000; + + private final Uri uri; + private final DataSource dataSource; + private final int minLoadableRetryCount; + private final Handler eventHandler; + private final ExtractorMediaSource.EventListener eventListener; + private final MediaSource.Listener sourceListener; + private final Callback callback; + private final Allocator allocator; + private final Loader loader; + private final ExtractorHolder extractorHolder; + private final ConditionVariable loadCondition; + + private SeekMap seekMap; + private boolean tracksBuilt; + private boolean prepared; + + private boolean seenFirstTrackSelection; + private boolean notifyReset; + private int enabledTrackCount; + private DefaultTrackOutput[] sampleQueues; + private TrackGroupArray tracks; + private long durationUs; + private boolean[] trackEnabledStates; + private long length; + + private long lastSeekPositionUs; + private long pendingResetPositionUs; + + private int extractedSamplesCountAtStartOfLoad; + private boolean loadingFinished; + + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSource The data source to read the media. + * @param extractors The extractors to use to read the data source. + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param sourceListener A listener to notify when the timeline has been loaded. + * @param callback A callback to receive updates from the period. + * @param allocator An {@link Allocator} from which to obtain media buffer allocations. + */ + public ExtractorMediaPeriod(Uri uri, DataSource dataSource, Extractor[] extractors, + int minLoadableRetryCount, Handler eventHandler, + ExtractorMediaSource.EventListener eventListener, MediaSource.Listener sourceListener, + Callback callback, Allocator allocator) { + this.uri = uri; + this.dataSource = dataSource; + this.minLoadableRetryCount = minLoadableRetryCount; + this.eventHandler = eventHandler; + this.eventListener = eventListener; + this.sourceListener = sourceListener; + this.callback = callback; + this.allocator = allocator; + loader = new Loader("Loader:ExtractorMediaPeriod"); + extractorHolder = new ExtractorHolder(extractors, this); + loadCondition = new ConditionVariable(); + + pendingResetPositionUs = C.UNSET_TIME_US; + sampleQueues = new DefaultTrackOutput[0]; + length = C.LENGTH_UNBOUNDED; + loadCondition.open(); + startLoading(); + } + + public void release() { + final ExtractorHolder extractorHolder = this.extractorHolder; + loader.release(new Runnable() { + @Override + public void run() { + extractorHolder.release(); + } + }); + for (DefaultTrackOutput sampleQueue : sampleQueues) { + sampleQueue.disable(); + } + } + + @Override + public void maybeThrowPrepareError() throws IOException { + maybeThrowError(); + } + + @Override + public TrackGroupArray getTrackGroups() { + return tracks; + } + + @Override + public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + Assertions.checkState(prepared); + // Disable old tracks. + for (int i = 0; i < selections.length; i++) { + if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { + int track = ((SampleStreamImpl) streams[i]).track; + Assertions.checkState(trackEnabledStates[track]); + enabledTrackCount--; + trackEnabledStates[track] = false; + sampleQueues[track].disable(); + streams[i] = null; + } + } + // Enable new tracks. + boolean selectedNewTracks = false; + for (int i = 0; i < selections.length; i++) { + if (streams[i] == null && selections[i] != null) { + TrackSelection selection = selections[i]; + Assertions.checkState(selection.length() == 1); + Assertions.checkState(selection.getIndexInTrackGroup(0) == 0); + int track = tracks.indexOf(selection.getTrackGroup()); + Assertions.checkState(!trackEnabledStates[track]); + enabledTrackCount++; + trackEnabledStates[track] = true; + streams[i] = new SampleStreamImpl(track); + streamResetFlags[i] = true; + selectedNewTracks = true; + } + } + if (!seenFirstTrackSelection) { + // At the time of the first track selection all queues will be enabled, so we need to disable + // any that are no longer required. + for (int i = 0; i < sampleQueues.length; i++) { + if (!trackEnabledStates[i]) { + sampleQueues[i].disable(); + } + } + } + if (enabledTrackCount == 0) { + notifyReset = false; + if (loader.isLoading()) { + loader.cancelLoading(); + } + } else if (seenFirstTrackSelection ? selectedNewTracks : positionUs != 0) { + positionUs = seekToUs(positionUs); + // We'll need to reset renderers consuming from all streams due to the seek. + for (int i = 0; i < streams.length; i++) { + if (streams[i] != null) { + streamResetFlags[i] = true; + } + } + } + seenFirstTrackSelection = true; + return positionUs; + } + + @Override + public boolean continueLoading(long playbackPositionUs) { + if (loadingFinished) { + return false; + } + boolean continuedLoading = loadCondition.open(); + if (!loader.isLoading()) { + startLoading(); + continuedLoading = true; + } + return continuedLoading; + } + + @Override + public long getNextLoadPositionUs() { + return getBufferedPositionUs(); + } + + @Override + public long readDiscontinuity() { + if (notifyReset) { + notifyReset = false; + return lastSeekPositionUs; + } + return C.UNSET_TIME_US; + } + + @Override + public long getBufferedPositionUs() { + if (loadingFinished) { + return C.END_OF_SOURCE_US; + } else if (isPendingReset()) { + return pendingResetPositionUs; + } else { + long largestQueuedTimestampUs = getLargestQueuedTimestampUs(); + return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs + : largestQueuedTimestampUs; + } + } + + @Override + public long seekToUs(long positionUs) { + // Treat all seeks into non-seekable media as being to t=0. + positionUs = seekMap.isSeekable() ? positionUs : 0; + lastSeekPositionUs = positionUs; + // If we're not pending a reset, see if we can seek within the sample queues. + boolean seekInsideBuffer = !isPendingReset(); + for (int i = 0; seekInsideBuffer && i < sampleQueues.length; i++) { + if (trackEnabledStates[i]) { + seekInsideBuffer = sampleQueues[i].skipToKeyframeBefore(positionUs); + } + } + // If we failed to seek within the sample queues, we need to restart. + if (!seekInsideBuffer) { + pendingResetPositionUs = positionUs; + loadingFinished = false; + if (loader.isLoading()) { + loader.cancelLoading(); + } else { + for (int i = 0; i < sampleQueues.length; i++) { + sampleQueues[i].reset(trackEnabledStates[i]); + } + } + } + notifyReset = false; + return positionUs; + } + + // SampleStream methods. + + /* package */ boolean isReady(int track) { + return loadingFinished || (!isPendingReset() && !sampleQueues[track].isEmpty()); + } + + /* package */ void maybeThrowError() throws IOException { + loader.maybeThrowError(); + } + + /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer) { + if (notifyReset || isPendingReset()) { + return C.RESULT_NOTHING_READ; + } + + return sampleQueues[track].readData(formatHolder, buffer, loadingFinished, lastSeekPositionUs); + } + + // Loader.Callback implementation. + + @Override + public void onLoadCompleted(ExtractingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs) { + copyLengthFromLoader(loadable); + loadingFinished = true; + if (durationUs == C.UNSET_TIME_US) { + long largestQueuedTimestampUs = getLargestQueuedTimestampUs(); + durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0 + : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US; + sourceListener.onSourceInfoRefreshed(seekMap.isSeekable() + ? SinglePeriodTimeline.createSeekableFinalTimeline(0, durationUs) + : SinglePeriodTimeline.createUnseekableFinalTimeline(0, durationUs), null); + } + } + + @Override + public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs, boolean released) { + copyLengthFromLoader(loadable); + if (!released && enabledTrackCount > 0) { + for (int i = 0; i < sampleQueues.length; i++) { + sampleQueues[i].reset(trackEnabledStates[i]); + } + callback.onContinueLoadingRequested(this); + } + } + + @Override + public int onLoadError(ExtractingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs, IOException error) { + copyLengthFromLoader(loadable); + notifyLoadError(error); + if (isLoadableExceptionFatal(error)) { + return Loader.DONT_RETRY_FATAL; + } + int extractedSamplesCount = getExtractedSamplesCount(); + boolean madeProgress = extractedSamplesCount > extractedSamplesCountAtStartOfLoad; + configureRetry(loadable); // May reset the sample queues. + extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount(); + return madeProgress ? Loader.RETRY_RESET_ERROR_COUNT : Loader.RETRY; + } + + // ExtractorOutput implementation. + + @Override + public TrackOutput track(int id) { + sampleQueues = Arrays.copyOf(sampleQueues, sampleQueues.length + 1); + DefaultTrackOutput sampleQueue = new DefaultTrackOutput(allocator); + sampleQueue.setUpstreamFormatChangeListener(this); + sampleQueues[sampleQueues.length - 1] = sampleQueue; + return sampleQueue; + } + + @Override + public void endTracks() { + tracksBuilt = true; + maybeFinishPrepare(); + } + + @Override + public void seekMap(SeekMap seekMap) { + this.seekMap = seekMap; + maybeFinishPrepare(); + } + + // UpstreamFormatChangedListener implementation + + @Override + public void onUpstreamFormatChanged(Format format) { + maybeFinishPrepare(); + } + + // Internal methods. + + private void maybeFinishPrepare() { + if (prepared || seekMap == null || !tracksBuilt) { + return; + } + for (DefaultTrackOutput sampleQueue : sampleQueues) { + if (sampleQueue.getUpstreamFormat() == null) { + return; + } + } + loadCondition.close(); + int trackCount = sampleQueues.length; + TrackGroup[] trackArray = new TrackGroup[trackCount]; + trackEnabledStates = new boolean[trackCount]; + durationUs = seekMap.getDurationUs(); + for (int i = 0; i < trackCount; i++) { + trackArray[i] = new TrackGroup(sampleQueues[i].getUpstreamFormat()); + } + tracks = new TrackGroupArray(trackArray); + prepared = true; + callback.onPrepared(this); + sourceListener.onSourceInfoRefreshed(seekMap.isSeekable() + ? SinglePeriodTimeline.createSeekableFinalTimeline(0, durationUs) + : SinglePeriodTimeline.createUnseekableFinalTimeline(0, durationUs), null); + } + + private void copyLengthFromLoader(ExtractingLoadable loadable) { + if (length == C.LENGTH_UNBOUNDED) { + length = loadable.length; + } + } + + private void startLoading() { + ExtractingLoadable loadable = new ExtractingLoadable(uri, dataSource, extractorHolder, + loadCondition); + if (prepared) { + Assertions.checkState(isPendingReset()); + if (durationUs != C.UNSET_TIME_US && pendingResetPositionUs >= durationUs) { + loadingFinished = true; + pendingResetPositionUs = C.UNSET_TIME_US; + return; + } + loadable.setLoadPosition(seekMap.getPosition(pendingResetPositionUs)); + pendingResetPositionUs = C.UNSET_TIME_US; + } + extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount(); + + int minRetryCount = minLoadableRetryCount; + if (minRetryCount == ExtractorMediaSource.MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA) { + // We assume on-demand before we're prepared. + minRetryCount = !prepared || length != C.LENGTH_UNBOUNDED + || (seekMap != null && seekMap.getDurationUs() != C.UNSET_TIME_US) + ? ExtractorMediaSource.DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND + : ExtractorMediaSource.DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE; + } + loader.startLoading(loadable, this, minRetryCount); + } + + private void configureRetry(ExtractingLoadable loadable) { + if (length != C.LENGTH_UNBOUNDED + || (seekMap != null && seekMap.getDurationUs() != C.UNSET_TIME_US)) { + // We're playing an on-demand stream. Resume the current loadable, which will + // request data starting from the point it left off. + } else { + // We're playing a stream of unknown length and duration. Assume it's live, and + // therefore that the data at the uri is a continuously shifting window of the latest + // available media. For this case there's no way to continue loading from where a + // previous load finished, so it's necessary to load from the start whenever commencing + // a new load. + lastSeekPositionUs = 0; + notifyReset = prepared; + for (int i = 0; i < sampleQueues.length; i++) { + sampleQueues[i].reset(trackEnabledStates[i]); + } + loadable.setLoadPosition(0); + } + } + + private int getExtractedSamplesCount() { + int extractedSamplesCount = 0; + for (DefaultTrackOutput sampleQueue : sampleQueues) { + extractedSamplesCount += sampleQueue.getWriteIndex(); + } + return extractedSamplesCount; + } + + private long getLargestQueuedTimestampUs() { + long largestQueuedTimestampUs = Long.MIN_VALUE; + for (DefaultTrackOutput sampleQueue : sampleQueues) { + largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, + sampleQueue.getLargestQueuedTimestampUs()); + } + return largestQueuedTimestampUs; + } + + private boolean isPendingReset() { + return pendingResetPositionUs != C.UNSET_TIME_US; + } + + private boolean isLoadableExceptionFatal(IOException e) { + return e instanceof ExtractorMediaSource.UnrecognizedInputFormatException; + } + + private void notifyLoadError(final IOException error) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onLoadError(error); + } + }); + } + } + + private final class SampleStreamImpl implements SampleStream { + + private final int track; + + public SampleStreamImpl(int track) { + this.track = track; + } + + @Override + public boolean isReady() { + return ExtractorMediaPeriod.this.isReady(track); + } + + @Override + public void maybeThrowError() throws IOException { + ExtractorMediaPeriod.this.maybeThrowError(); + } + + @Override + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { + return ExtractorMediaPeriod.this.readData(track, formatHolder, buffer); + } + + @Override + public void skipToKeyframeBefore(long timeUs) { + sampleQueues[track].skipToKeyframeBefore(timeUs); + } + + } + + /** + * Loads the media stream and extracts sample data from it. + */ + /* package */ final class ExtractingLoadable implements Loadable { + + /** + * The number of bytes that should be loaded between each each invocation of + * {@link Callback#onContinueLoadingRequested(SequenceableLoader)}. + */ + private static final int CONTINUE_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024; + + private final Uri uri; + private final DataSource dataSource; + private final ExtractorHolder extractorHolder; + private final ConditionVariable loadCondition; + private final PositionHolder positionHolder; + + private volatile boolean loadCanceled; + + private boolean pendingExtractorSeek; + private long length; + + public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder, + ConditionVariable loadCondition) { + this.uri = Assertions.checkNotNull(uri); + this.dataSource = Assertions.checkNotNull(dataSource); + this.extractorHolder = Assertions.checkNotNull(extractorHolder); + this.loadCondition = loadCondition; + this.positionHolder = new PositionHolder(); + this.pendingExtractorSeek = true; + this.length = C.LENGTH_UNBOUNDED; + } + + public void setLoadPosition(long position) { + positionHolder.position = position; + pendingExtractorSeek = true; + } + + @Override + public void cancelLoad() { + loadCanceled = true; + } + + @Override + public boolean isLoadCanceled() { + return loadCanceled; + } + + @Override + public void load() throws IOException, InterruptedException { + int result = Extractor.RESULT_CONTINUE; + while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { + ExtractorInput input = null; + try { + long position = positionHolder.position; + length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNBOUNDED, null)); + if (length != C.LENGTH_UNBOUNDED) { + length += position; + } + input = new DefaultExtractorInput(dataSource, position, length); + Extractor extractor = extractorHolder.selectExtractor(input); + if (pendingExtractorSeek) { + extractor.seek(position); + pendingExtractorSeek = false; + } + while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { + loadCondition.block(); + result = extractor.read(input, positionHolder); + if (input.getPosition() > position + CONTINUE_LOADING_CHECK_INTERVAL_BYTES) { + position = input.getPosition(); + loadCondition.close(); + callback.onContinueLoadingRequested(ExtractorMediaPeriod.this); + } + } + } finally { + if (result == Extractor.RESULT_SEEK) { + result = Extractor.RESULT_CONTINUE; + } else if (input != null) { + positionHolder.position = input.getPosition(); + } + dataSource.close(); + } + } + } + + } + + /** + * Stores a list of extractors and a selected extractor when the format has been detected. + */ + private static final class ExtractorHolder { + + private final Extractor[] extractors; + private final ExtractorOutput extractorOutput; + private Extractor extractor; + + /** + * Creates a holder that will select an extractor and initialize it using the specified output. + * + * @param extractors One or more extractors to choose from. + * @param extractorOutput The output that will be used to initialize the selected extractor. + */ + public ExtractorHolder(Extractor[] extractors, ExtractorOutput extractorOutput) { + this.extractors = extractors; + this.extractorOutput = extractorOutput; + } + + /** + * Returns an initialized extractor for reading {@code input}, and returns the same extractor on + * later calls. + * + * @param input The {@link ExtractorInput} from which data should be read. + * @return An initialized extractor for reading {@code input}. + * @throws ExtractorMediaSource.UnrecognizedInputFormatException Thrown if the input format + * could not be detected. + * @throws IOException Thrown if the input could not be read. + * @throws InterruptedException Thrown if the thread was interrupted. + */ + public Extractor selectExtractor(ExtractorInput input) + throws IOException, InterruptedException { + if (extractor != null) { + return extractor; + } + for (Extractor extractor : extractors) { + try { + if (extractor.sniff(input)) { + this.extractor = extractor; + break; + } + } catch (EOFException e) { + // Do nothing. + } finally { + input.resetPeekPosition(); + } + } + if (extractor == null) { + throw new ExtractorMediaSource.UnrecognizedInputFormatException(extractors); + } + extractor.init(extractorOutput); + return extractor; + } + + public void release() { + if (extractor != null) { + extractor.release(); + extractor = null; + } + } + + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 52688ab40f..f4038116fb 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -18,50 +18,30 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.extractor.DefaultTrackOutput; -import com.google.android.exoplayer2.extractor.DefaultTrackOutput.UpstreamFormatChangedListener; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.extractor.PositionHolder; -import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.source.MediaPeriod.Callback; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.upstream.Loader; -import com.google.android.exoplayer2.upstream.Loader.Loadable; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.ConditionVariable; -import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; -import java.io.EOFException; import java.io.IOException; -import java.util.Arrays; /** - * Provides a single {@link MediaPeriod} whose data is loaded from a {@link Uri} and extracted using - * an {@link Extractor}. + * Provides one period that loads data from a {@link Uri} and extracted using an {@link Extractor}. *

* If the possible input stream container formats are known, pass a factory that instantiates * extractors for them to the constructor. Otherwise, pass a {@link DefaultExtractorsFactory} to * use the default extractors. When reading a new stream, the first {@link Extractor} in the array * of extractors created by the factory that returns {@code true} from * {@link Extractor#sniff(ExtractorInput)} will be used to extract samples from the input stream. - * - *

Note that the built-in extractors for AAC, MPEG TS and FLV streams do not support seeking. + *

+ * Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking. */ -public final class ExtractorMediaSource implements MediaPeriod, MediaSource, - ExtractorOutput, Loader.Callback, - UpstreamFormatChangedListener { +public final class ExtractorMediaSource implements MediaSource, MediaSource.Listener { /** * Listener of {@link ExtractorMediaSource} events. @@ -99,13 +79,12 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource, */ public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE = 6; - private static final int MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA = -1; - /** - * When the source's duration is unknown, it is calculated by adding this value to the largest - * sample timestamp seen when buffering completes. + * Value for {@code minLoadableRetryCount} that causes the loader to retry + * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE} times for live streams and + * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND} for on-demand streams. */ - private static final long DEFAULT_LAST_SAMPLE_DURATION_US = 10000; + public static final int MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA = -1; private final Uri uri; private final DataSource.Factory dataSourceFactory; @@ -115,40 +94,15 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource, private final EventListener eventListener; private MediaSource.Listener sourceListener; - private DataSource dataSource; - private Loader loader; - private ExtractorHolder extractorHolder; - private ConditionVariable loadCondition; - - private Callback callback; - private Allocator allocator; - private SeekMap seekMap; - private boolean tracksBuilt; - private boolean prepared; - - private boolean seenFirstTrackSelection; - private boolean notifyReset; - private int enabledTrackCount; - private DefaultTrackOutput[] sampleQueues; - private TrackGroupArray tracks; - private boolean[] tracksAreAudioVideoFlags; - private boolean haveAudioVideoTracks; - private long durationUs; - private boolean[] trackEnabledStates; - private long length; - - private long lastSeekPositionUs; - private long pendingResetPositionUs; - - private int extractedSamplesCountAtStartOfLoad; - private boolean loadingFinished; + private Timeline timeline; /** * @param uri The {@link Uri} of the media stream. * @param dataSourceFactory A factory for {@link DataSource}s to read the media. - * @param extractorsFactory Factory for {@link Extractor}s to process the media stream. If the + * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the * possible formats are known, pass a factory that instantiates extractors for those formats. * Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors. + * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, @@ -160,11 +114,11 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource, /** * @param uri The {@link Uri} of the media stream. * @param dataSourceFactory A factory for {@link DataSource}s to read the media. - * @param extractorsFactory Factory for {@link Extractor}s to process the media stream. If the + * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the * possible formats are known, pass a factory that instantiates extractors for those formats. * Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors. - * @param minLoadableRetryCount The minimum number of times that the sample source will retry - * if a loading error occurs. + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, @@ -178,17 +132,16 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource, this.eventListener = eventListener; } - // MediaSource implementation. - @Override public void prepareSource(MediaSource.Listener listener) { sourceListener = listener; - listener.onSourceInfoRefreshed(SinglePeriodTimeline.createNonFinalTimeline(this), null); + timeline = SinglePeriodTimeline.createNonFinalTimeline(0); + listener.onSourceInfoRefreshed(timeline, null); } @Override public int getNewPlayingPeriodIndex(int oldPlayingPeriodIndex, Timeline oldTimeline) { - return oldPlayingPeriodIndex; + return 0; } @Override @@ -197,9 +150,22 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource, } @Override - public MediaPeriod createPeriod(int index) { + public void maybeThrowSourceInfoRefreshError() throws IOException { + // Do nothing. + } + + @Override + public MediaPeriod createPeriod(int index, Callback callback, Allocator allocator, + long positionUs) { Assertions.checkArgument(index == 0); - return this; + return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(), + extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener, + this, callback, allocator); + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + ((ExtractorMediaPeriod) mediaPeriod).release(); } @Override @@ -207,593 +173,14 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource, sourceListener = null; } - // MediaPeriod implementation. + // MediaSource.Listener implementation. @Override - public void preparePeriod(Callback callback, Allocator allocator, long positionUs) { - this.callback = callback; - this.allocator = allocator; - - dataSource = dataSourceFactory.createDataSource(); - loader = new Loader("Loader:ExtractorMediaSource"); - extractorHolder = new ExtractorHolder(extractorsFactory.createExtractors(), this); - loadCondition = new ConditionVariable(); - pendingResetPositionUs = C.UNSET_TIME_US; - sampleQueues = new DefaultTrackOutput[0]; - length = C.LENGTH_UNBOUNDED; - - loadCondition.open(); - startLoading(); - } - - @Override - public void maybeThrowPrepareError() throws IOException { - maybeThrowError(); - } - - @Override - public TrackGroupArray getTrackGroups() { - return tracks; - } - - @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - Assertions.checkState(prepared); - // Disable old tracks. - for (int i = 0; i < selections.length; i++) { - if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { - int track = ((SampleStreamImpl) streams[i]).track; - Assertions.checkState(trackEnabledStates[track]); - enabledTrackCount--; - trackEnabledStates[track] = false; - sampleQueues[track].disable(); - streams[i] = null; - } + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + if (!this.timeline.isFinal() || timeline.getPeriodDurationUs(0) != C.UNSET_TIME_US) { + this.timeline = timeline; + sourceListener.onSourceInfoRefreshed(timeline, null); } - // Enable new tracks. - boolean selectedNewTracks = false; - for (int i = 0; i < selections.length; i++) { - if (streams[i] == null && selections[i] != null) { - TrackSelection selection = selections[i]; - Assertions.checkState(selection.length() == 1); - Assertions.checkState(selection.getIndexInTrackGroup(0) == 0); - int track = tracks.indexOf(selection.getTrackGroup()); - Assertions.checkState(!trackEnabledStates[track]); - enabledTrackCount++; - trackEnabledStates[track] = true; - streams[i] = new SampleStreamImpl(track); - streamResetFlags[i] = true; - selectedNewTracks = true; - } - } - if (!seenFirstTrackSelection) { - // At the time of the first track selection all queues will be enabled, so we need to disable - // any that are no longer required. - for (int i = 0; i < sampleQueues.length; i++) { - if (!trackEnabledStates[i]) { - sampleQueues[i].disable(); - } - } - } - if (enabledTrackCount == 0) { - notifyReset = false; - if (loader.isLoading()) { - loader.cancelLoading(); - } - } else if (seenFirstTrackSelection ? selectedNewTracks : positionUs != 0) { - positionUs = seekToUs(positionUs); - // We'll need to reset renderers consuming from all streams due to the seek. - for (int i = 0; i < streams.length; i++) { - if (streams[i] != null) { - streamResetFlags[i] = true; - } - } - } - seenFirstTrackSelection = true; - return positionUs; - } - - @Override - public boolean continueLoading(long playbackPositionUs) { - if (loadingFinished) { - return false; - } - boolean continuedLoading = loadCondition.open(); - if (!loader.isLoading()) { - startLoading(); - continuedLoading = true; - } - return continuedLoading; - } - - @Override - public long getNextLoadPositionUs() { - return getBufferedPositionUs(); - } - - @Override - public long readDiscontinuity() { - if (notifyReset) { - notifyReset = false; - return lastSeekPositionUs; - } - return C.UNSET_TIME_US; - } - - @Override - public long getBufferedPositionUs() { - if (loadingFinished) { - return C.END_OF_SOURCE_US; - } else if (isPendingReset()) { - return pendingResetPositionUs; - } else { - long largestQueuedTimestampUs = getLargestQueuedTimestampUs(); - return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs - : largestQueuedTimestampUs; - } - } - - @Override - public long seekToUs(long positionUs) { - // Treat all seeks into non-seekable media as being to t=0. - positionUs = seekMap.isSeekable() ? positionUs : 0; - lastSeekPositionUs = positionUs; - // If we're not pending a reset, see if we can seek within the sample queues. - boolean seekInsideBuffer = !isPendingReset(); - for (int i = 0; seekInsideBuffer && i < sampleQueues.length; i++) { - if (trackEnabledStates[i]) { - seekInsideBuffer = sampleQueues[i].skipToKeyframeBefore(positionUs); - } - } - // If we failed to seek within the sample queues, we need to restart. - if (!seekInsideBuffer) { - pendingResetPositionUs = positionUs; - loadingFinished = false; - if (loader.isLoading()) { - loader.cancelLoading(); - } else { - for (int i = 0; i < sampleQueues.length; i++) { - sampleQueues[i].reset(trackEnabledStates[i]); - } - } - } - notifyReset = false; - return positionUs; - } - - @Override - public void releasePeriod() { - dataSource = null; - if (loader != null) { - final ExtractorHolder extractorHolder = this.extractorHolder; - loader.release(new Runnable() { - @Override - public void run() { - extractorHolder.release(); - } - }); - loader = null; - } - extractorHolder = null; - loadCondition = null; - callback = null; - allocator = null; - seekMap = null; - tracksBuilt = false; - prepared = false; - seenFirstTrackSelection = false; - notifyReset = false; - enabledTrackCount = 0; - if (sampleQueues != null) { - for (DefaultTrackOutput sampleQueue : sampleQueues) { - sampleQueue.disable(); - } - sampleQueues = null; - } - tracks = null; - durationUs = 0; - trackEnabledStates = null; - length = 0; - lastSeekPositionUs = 0; - pendingResetPositionUs = 0; - extractedSamplesCountAtStartOfLoad = 0; - loadingFinished = false; - } - - // SampleStream methods. - - /* package */ boolean isReady(int track) { - return loadingFinished || (!isPendingReset() && !sampleQueues[track].isEmpty()); - } - - /* package */ void maybeThrowError() throws IOException { - loader.maybeThrowError(); - } - - /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer) { - if (notifyReset || isPendingReset()) { - return C.RESULT_NOTHING_READ; - } - - return sampleQueues[track].readData(formatHolder, buffer, loadingFinished, lastSeekPositionUs); - } - - // Loader.Callback implementation. - - @Override - public void onLoadCompleted(ExtractingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs) { - copyLengthFromLoader(loadable); - loadingFinished = true; - if (durationUs == C.UNSET_TIME_US) { - long largestQueuedTimestampUs = getLargestQueuedTimestampUs(); - durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0 - : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US; - sourceListener.onSourceInfoRefreshed(seekMap.isSeekable() - ? SinglePeriodTimeline.createSeekableFinalTimeline(this, durationUs) - : SinglePeriodTimeline.createUnseekableFinalTimeline(this, durationUs), null); - } - } - - @Override - public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, boolean released) { - copyLengthFromLoader(loadable); - if (!released && enabledTrackCount > 0) { - for (int i = 0; i < sampleQueues.length; i++) { - sampleQueues[i].reset(trackEnabledStates[i]); - } - callback.onContinueLoadingRequested(this); - } - } - - @Override - public int onLoadError(ExtractingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, IOException error) { - copyLengthFromLoader(loadable); - notifyLoadError(error); - if (isLoadableExceptionFatal(error)) { - return Loader.DONT_RETRY_FATAL; - } - int extractedSamplesCount = getExtractedSamplesCount(); - boolean madeProgress = extractedSamplesCount > extractedSamplesCountAtStartOfLoad; - configureRetry(loadable); // May reset the sample queues. - extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount(); - return madeProgress ? Loader.RETRY_RESET_ERROR_COUNT : Loader.RETRY; - } - - // ExtractorOutput implementation. - - @Override - public TrackOutput track(int id) { - sampleQueues = Arrays.copyOf(sampleQueues, sampleQueues.length + 1); - DefaultTrackOutput sampleQueue = new DefaultTrackOutput(allocator); - sampleQueue.setUpstreamFormatChangeListener(this); - sampleQueues[sampleQueues.length - 1] = sampleQueue; - return sampleQueue; - } - - @Override - public void endTracks() { - tracksBuilt = true; - maybeFinishPrepare(); - } - - @Override - public void seekMap(SeekMap seekMap) { - this.seekMap = seekMap; - maybeFinishPrepare(); - } - - // UpstreamFormatChangedListener implementation - - @Override - public void onUpstreamFormatChanged(Format format) { - maybeFinishPrepare(); - } - - // Internal methods. - - private void maybeFinishPrepare() { - if (prepared || seekMap == null || !tracksBuilt) { - return; - } - for (DefaultTrackOutput sampleQueue : sampleQueues) { - if (sampleQueue.getUpstreamFormat() == null) { - return; - } - } - loadCondition.close(); - int trackCount = sampleQueues.length; - TrackGroup[] trackArray = new TrackGroup[trackCount]; - tracksAreAudioVideoFlags = new boolean[trackCount]; - trackEnabledStates = new boolean[trackCount]; - durationUs = seekMap.getDurationUs(); - for (int i = 0; i < trackCount; i++) { - Format format = sampleQueues[i].getUpstreamFormat(); - trackArray[i] = new TrackGroup(format); - String sampleMimeType = format.sampleMimeType; - tracksAreAudioVideoFlags[i] = MimeTypes.isVideo(sampleMimeType) - || MimeTypes.isAudio(sampleMimeType); - haveAudioVideoTracks |= tracksAreAudioVideoFlags[i]; - } - tracks = new TrackGroupArray(trackArray); - prepared = true; - callback.onPeriodPrepared(this); - sourceListener.onSourceInfoRefreshed(seekMap.isSeekable() - ? SinglePeriodTimeline.createSeekableFinalTimeline(this, durationUs) - : SinglePeriodTimeline.createUnseekableFinalTimeline(this, durationUs), null); - } - - private void copyLengthFromLoader(ExtractingLoadable loadable) { - if (length == C.LENGTH_UNBOUNDED) { - length = loadable.length; - } - } - - private void startLoading() { - ExtractingLoadable loadable = new ExtractingLoadable(uri, dataSource, extractorHolder, - loadCondition); - if (prepared) { - Assertions.checkState(isPendingReset()); - if (durationUs != C.UNSET_TIME_US && pendingResetPositionUs >= durationUs) { - loadingFinished = true; - pendingResetPositionUs = C.UNSET_TIME_US; - return; - } - loadable.setLoadPosition(seekMap.getPosition(pendingResetPositionUs)); - pendingResetPositionUs = C.UNSET_TIME_US; - } - extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount(); - - int minRetryCount = minLoadableRetryCount; - if (minRetryCount == MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA) { - // We assume on-demand before we're prepared. - minRetryCount = !prepared || length != C.LENGTH_UNBOUNDED - || (seekMap != null && seekMap.getDurationUs() != C.UNSET_TIME_US) - ? DEFAULT_MIN_LOADABLE_RETRY_COUNT_ON_DEMAND - : DEFAULT_MIN_LOADABLE_RETRY_COUNT_LIVE; - } - loader.startLoading(loadable, this, minRetryCount); - } - - private void configureRetry(ExtractingLoadable loadable) { - if (length != C.LENGTH_UNBOUNDED - || (seekMap != null && seekMap.getDurationUs() != C.UNSET_TIME_US)) { - // We're playing an on-demand stream. Resume the current loadable, which will - // request data starting from the point it left off. - } else { - // We're playing a stream of unknown length and duration. Assume it's live, and - // therefore that the data at the uri is a continuously shifting window of the latest - // available media. For this case there's no way to continue loading from where a - // previous load finished, so it's necessary to load from the start whenever commencing - // a new load. - lastSeekPositionUs = 0; - notifyReset = prepared; - for (int i = 0; i < sampleQueues.length; i++) { - sampleQueues[i].reset(trackEnabledStates[i]); - } - loadable.setLoadPosition(0); - } - } - - private int getExtractedSamplesCount() { - int extractedSamplesCount = 0; - for (DefaultTrackOutput sampleQueue : sampleQueues) { - extractedSamplesCount += sampleQueue.getWriteIndex(); - } - return extractedSamplesCount; - } - - private long getLargestQueuedTimestampUs() { - long largestQueuedTimestampUs = Long.MAX_VALUE; - for (int i = 0; i < sampleQueues.length; i++) { - if (tracksAreAudioVideoFlags[i] || !haveAudioVideoTracks) { - largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs, - sampleQueues[i].getLargestQueuedTimestampUs()); - } - } - return largestQueuedTimestampUs == Long.MAX_VALUE ? Long.MIN_VALUE : largestQueuedTimestampUs; - } - - private boolean isPendingReset() { - return pendingResetPositionUs != C.UNSET_TIME_US; - } - - private boolean isLoadableExceptionFatal(IOException e) { - return e instanceof UnrecognizedInputFormatException; - } - - private void notifyLoadError(final IOException error) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onLoadError(error); - } - }); - } - } - - private final class SampleStreamImpl implements SampleStream { - - private final int track; - - public SampleStreamImpl(int track) { - this.track = track; - } - - @Override - public boolean isReady() { - return ExtractorMediaSource.this.isReady(track); - } - - @Override - public void maybeThrowError() throws IOException { - ExtractorMediaSource.this.maybeThrowError(); - } - - @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { - return ExtractorMediaSource.this.readData(track, formatHolder, buffer); - } - - @Override - public void skipToKeyframeBefore(long timeUs) { - sampleQueues[track].skipToKeyframeBefore(timeUs); - } - - } - - /** - * Loads the media stream and extracts sample data from it. - */ - /* package */ final class ExtractingLoadable implements Loadable { - - /** - * The number of bytes that should be loaded between each each invocation of - * {@link Callback#onContinueLoadingRequested(SequenceableLoader)}. - */ - private static final int CONTINUE_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024; - - private final Uri uri; - private final DataSource dataSource; - private final ExtractorHolder extractorHolder; - private final ConditionVariable loadCondition; - private final PositionHolder positionHolder; - - private volatile boolean loadCanceled; - - private boolean pendingExtractorSeek; - private long length; - - public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder, - ConditionVariable loadCondition) { - this.uri = Assertions.checkNotNull(uri); - this.dataSource = Assertions.checkNotNull(dataSource); - this.extractorHolder = Assertions.checkNotNull(extractorHolder); - this.loadCondition = loadCondition; - this.positionHolder = new PositionHolder(); - this.pendingExtractorSeek = true; - this.length = C.LENGTH_UNBOUNDED; - } - - public void setLoadPosition(long position) { - positionHolder.position = position; - pendingExtractorSeek = true; - } - - @Override - public void cancelLoad() { - loadCanceled = true; - } - - @Override - public boolean isLoadCanceled() { - return loadCanceled; - } - - @Override - public void load() throws IOException, InterruptedException { - int result = Extractor.RESULT_CONTINUE; - while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - ExtractorInput input = null; - try { - long position = positionHolder.position; - length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNBOUNDED, null)); - if (length != C.LENGTH_UNBOUNDED) { - length += position; - } - input = new DefaultExtractorInput(dataSource, position, length); - Extractor extractor = extractorHolder.selectExtractor(input); - if (pendingExtractorSeek) { - extractor.seek(position); - pendingExtractorSeek = false; - } - while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { - loadCondition.block(); - result = extractor.read(input, positionHolder); - if (input.getPosition() > position + CONTINUE_LOADING_CHECK_INTERVAL_BYTES) { - position = input.getPosition(); - loadCondition.close(); - callback.onContinueLoadingRequested(ExtractorMediaSource.this); - } - } - } finally { - if (result == Extractor.RESULT_SEEK) { - result = Extractor.RESULT_CONTINUE; - } else if (input != null) { - positionHolder.position = input.getPosition(); - } - dataSource.close(); - } - } - } - - } - - /** - * Stores a list of extractors and a selected extractor when the format has been detected. - */ - private static final class ExtractorHolder { - - private final Extractor[] extractors; - private final ExtractorOutput extractorOutput; - private Extractor extractor; - - /** - * Creates a holder that will select an extractor and initialize it using the specified output. - * - * @param extractors One or more extractors to choose from. - * @param extractorOutput The output that will be used to initialize the selected extractor. - */ - public ExtractorHolder(Extractor[] extractors, ExtractorOutput extractorOutput) { - this.extractors = extractors; - this.extractorOutput = extractorOutput; - } - - /** - * Returns an initialized extractor for reading {@code input}, and returns the same extractor on - * later calls. - * - * @param input The {@link ExtractorInput} from which data should be read. - * @return An initialized extractor for reading {@code input}. - * @throws UnrecognizedInputFormatException Thrown if the input format could not be detected. - * @throws IOException Thrown if the input could not be read. - * @throws InterruptedException Thrown if the thread was interrupted. - */ - public Extractor selectExtractor(ExtractorInput input) - throws IOException, InterruptedException { - if (extractor != null) { - return extractor; - } - for (Extractor extractor : extractors) { - try { - if (extractor.sniff(input)) { - this.extractor = extractor; - break; - } - } catch (EOFException e) { - // Do nothing. - } finally { - input.resetPeekPosition(); - } - } - if (extractor == null) { - throw new UnrecognizedInputFormatException(extractors); - } - extractor.init(extractorOutput); - return extractor; - } - - public void release() { - if (extractor != null) { - extractor.release(); - extractor = null; - } - } - } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index 323d5d8864..e97819910e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.upstream.Allocator; import java.io.IOException; /** @@ -39,23 +38,10 @@ public interface MediaPeriod extends SequenceableLoader { * * @param mediaPeriod The prepared {@link MediaPeriod}. */ - void onPeriodPrepared(MediaPeriod mediaPeriod); + void onPrepared(MediaPeriod mediaPeriod); } - /** - * Starts preparation of the period. - *

- * {@link Callback#onPeriodPrepared(MediaPeriod)} is called when preparation completes. If - * preparation fails, {@link #maybeThrowPrepareError()} will throw an {@link IOException} if - * called. - * - * @param callback A callback to receive updates from the period. - * @param allocator An {@link Allocator} from which to obtain media buffer allocations. - * @param positionUs The player's current playback position. - */ - void preparePeriod(Callback callback, Allocator allocator, long positionUs); - /** * Throws an error that's preventing the period from becoming prepared. Does nothing if no such * error exists. @@ -136,12 +122,4 @@ public interface MediaPeriod extends SequenceableLoader { */ long seekToUs(long positionUs); - /** - * Releases the period. - *

- * This method should be called when the period is no longer required. It may be called in any - * state. - */ - void releasePeriod(); - } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 2ab5dd46a9..fa7bb582ff 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.source; +import com.google.android.exoplayer2.source.MediaPeriod.Callback; +import com.google.android.exoplayer2.upstream.Allocator; import java.io.IOException; /** @@ -105,16 +107,31 @@ public interface MediaSource { Position getDefaultStartPosition(int index); /** - * Returns a {@link MediaPeriod} corresponding to the period at the specified index, or - * {@code null} if the period at the specified index is not yet available. + * Throws any pending error encountered while loading or refreshing source information. + */ + void maybeThrowSourceInfoRefreshError() throws IOException; + + /** + * Returns a {@link MediaPeriod} corresponding to the period at the specified index. + *

+ * {@link Callback#onPrepared(MediaPeriod)} is called when the new period is prepared. If + * preparation fails, {@link MediaPeriod#maybeThrowPrepareError()} will throw an + * {@link IOException} if called on the returned instance. * * @param index The index of the period. - * @return A {@link MediaPeriod}, or {@code null} if the source at the specified index is not - * available. - * @throws IOException If there is an error that's preventing the source from becoming prepared or - * creating periods. + * @param callback A callback to receive updates from the period. + * @param allocator An {@link Allocator} from which to obtain media buffer allocations. + * @param positionUs The player's current playback position. + * @return A new {@link MediaPeriod}. */ - MediaPeriod createPeriod(int index) throws IOException; + MediaPeriod createPeriod(int index, Callback callback, Allocator allocator, long positionUs); + + /** + * Releases the period. + * + * @param mediaPeriod The period to release. + */ + void releasePeriod(MediaPeriod mediaPeriod); /** * Releases the source. diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index 82029dd940..cbe6bd8e33 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.upstream.Allocator; import java.io.IOException; import java.util.ArrayList; import java.util.IdentityHashMap; @@ -25,30 +24,23 @@ import java.util.IdentityHashMap; /** * Merges multiple {@link MediaPeriod} instances. */ -public final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callback { +/* package */ final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callback { - private final MediaPeriod[] periods; + public final MediaPeriod[] periods; + private final Callback callback; private final IdentityHashMap streamPeriodIndices; - private Callback callback; private int pendingChildPrepareCount; private TrackGroupArray trackGroups; private MediaPeriod[] enabledPeriods; private SequenceableLoader sequenceableLoader; - public MergingMediaPeriod(MediaPeriod... periods) { + public MergingMediaPeriod(Callback callback, MediaPeriod... periods) { this.periods = periods; - pendingChildPrepareCount = periods.length; - streamPeriodIndices = new IdentityHashMap<>(); - } - - @Override - public void preparePeriod(Callback callback, Allocator allocator, long positionUs) { this.callback = callback; - for (MediaPeriod period : periods) { - period.preparePeriod(this, allocator, positionUs); - } + streamPeriodIndices = new IdentityHashMap<>(); + pendingChildPrepareCount = periods.length; } @Override @@ -174,17 +166,10 @@ public final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callba return positionUs; } - @Override - public void releasePeriod() { - for (MediaPeriod period : periods) { - period.releasePeriod(); - } - } - // MediaPeriod.Callback implementation @Override - public void onPeriodPrepared(MediaPeriod ignored) { + public void onPrepared(MediaPeriod ignored) { if (--pendingChildPrepareCount > 0) { return; } @@ -202,7 +187,7 @@ public final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callba } } trackGroups = new TrackGroupArray(trackGroupArray); - callback.onPeriodPrepared(this); + callback.onPrepared(this); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index 91250cc10d..63cfe79045 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.source; +import com.google.android.exoplayer2.source.MediaPeriod.Callback; +import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -73,13 +75,31 @@ public final class MergingMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int index) throws IOException { + public void maybeThrowSourceInfoRefreshError() throws IOException { + for (MediaSource mediaSource : mediaSources) { + mediaSource.maybeThrowSourceInfoRefreshError(); + } + } + + @Override + public MediaPeriod createPeriod(int index, Callback callback, Allocator allocator, + long positionUs) { MediaPeriod[] periods = new MediaPeriod[mediaSources.length]; + // The periods are only referenced after they have all been prepared. + MergingMediaPeriod mergingPeriod = new MergingMediaPeriod(callback, periods); for (int i = 0; i < periods.length; i++) { - periods[i] = mediaSources[i].createPeriod(index); + periods[i] = mediaSources[i].createPeriod(index, mergingPeriod, allocator, positionUs); Assertions.checkState(periods[i] != null, "Child source must not return null period"); } - return new MergingMediaPeriod(periods); + return mergingPeriod; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + MergingMediaPeriod mergingPeriod = (MergingMediaPeriod) mediaPeriod; + for (int i = 0; i < mediaSources.length; i++) { + mediaSources[i].releasePeriod(mergingPeriod.periods[i]); + } } @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java new file mode 100644 index 0000000000..be916011ec --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -0,0 +1,279 @@ +/* + * 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; + +import android.net.Uri; +import android.os.Handler; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.source.SingleSampleMediaSource.EventListener; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.Loader; +import com.google.android.exoplayer2.upstream.Loader.Loadable; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * A {@link MediaPeriod} with a single sample. + */ +/* package */ final class SingleSampleMediaPeriod implements MediaPeriod, + Loader.Callback { + + /** + * The initial size of the allocation used to hold the sample data. + */ + private static final int INITIAL_SAMPLE_SIZE = 1; + + private final Uri uri; + private final DataSource.Factory dataSourceFactory; + private final int minLoadableRetryCount; + private final Handler eventHandler; + private final EventListener eventListener; + private final int eventSourceId; + private final TrackGroupArray tracks; + private final ArrayList sampleStreams; + /* package */ final Loader loader; + /* package */ final Format format; + + /* package */ boolean loadingFinished; + /* package */ byte[] sampleData; + /* package */ int sampleSize; + + public SingleSampleMediaPeriod(Uri uri, DataSource.Factory dataSourceFactory, Format format, + int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, + int eventSourceId) { + this.uri = uri; + this.dataSourceFactory = dataSourceFactory; + this.format = format; + this.minLoadableRetryCount = minLoadableRetryCount; + this.eventHandler = eventHandler; + this.eventListener = eventListener; + this.eventSourceId = eventSourceId; + tracks = new TrackGroupArray(new TrackGroup(format)); + sampleStreams = new ArrayList<>(); + loader = new Loader("Loader:SingleSampleMediaPeriod"); + sampleData = new byte[INITIAL_SAMPLE_SIZE]; + } + + public void release() { + loader.release(); + } + + @Override + public void maybeThrowPrepareError() throws IOException { + loader.maybeThrowError(); + } + + @Override + public TrackGroupArray getTrackGroups() { + return tracks; + } + + @Override + public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + for (int i = 0; i < selections.length; i++) { + if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { + sampleStreams.remove(streams[i]); + streams[i] = null; + } + if (streams[i] == null && selections[i] != null) { + SampleStreamImpl stream = new SampleStreamImpl(); + sampleStreams.add(stream); + streams[i] = stream; + streamResetFlags[i] = true; + } + } + return positionUs; + } + + @Override + public boolean continueLoading(long positionUs) { + if (loadingFinished || loader.isLoading()) { + return false; + } + loader.startLoading(new SourceLoadable(uri, dataSourceFactory.createDataSource()), this, + minLoadableRetryCount); + return true; + } + + @Override + public long readDiscontinuity() { + return C.UNSET_TIME_US; + } + + @Override + public long getNextLoadPositionUs() { + return loadingFinished || loader.isLoading() ? C.END_OF_SOURCE_US : 0; + } + + @Override + public long getBufferedPositionUs() { + return loadingFinished ? C.END_OF_SOURCE_US : 0; + } + + @Override + public long seekToUs(long positionUs) { + for (int i = 0; i < sampleStreams.size(); i++) { + sampleStreams.get(i).seekToUs(positionUs); + } + return positionUs; + } + + // Loader.Callback implementation. + + @Override + public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs) { + sampleSize = loadable.sampleSize; + sampleData = loadable.sampleData; + loadingFinished = true; + } + + @Override + public void onLoadCanceled(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, + boolean released) { + // Do nothing. + } + + @Override + public int onLoadError(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, + IOException error) { + notifyLoadError(error); + return Loader.RETRY; + } + + // Internal methods. + + private void notifyLoadError(final IOException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onLoadError(eventSourceId, e); + } + }); + } + } + + private final class SampleStreamImpl implements SampleStream { + + private static final int STREAM_STATE_SEND_FORMAT = 0; + private static final int STREAM_STATE_SEND_SAMPLE = 1; + private static final int STREAM_STATE_END_OF_STREAM = 2; + + private int streamState; + + public void seekToUs(long positionUs) { + if (streamState == STREAM_STATE_END_OF_STREAM) { + streamState = STREAM_STATE_SEND_SAMPLE; + } + } + + @Override + public boolean isReady() { + return loadingFinished; + } + + @Override + public void maybeThrowError() throws IOException { + loader.maybeThrowError(); + } + + @Override + public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { + if (streamState == STREAM_STATE_END_OF_STREAM) { + buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); + return C.RESULT_BUFFER_READ; + } else if (streamState == STREAM_STATE_SEND_FORMAT) { + formatHolder.format = format; + streamState = STREAM_STATE_SEND_SAMPLE; + return C.RESULT_FORMAT_READ; + } + + Assertions.checkState(streamState == STREAM_STATE_SEND_SAMPLE); + if (!loadingFinished) { + return C.RESULT_NOTHING_READ; + } else { + buffer.timeUs = 0; + buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); + buffer.ensureSpaceForWrite(sampleSize); + buffer.data.put(sampleData, 0, sampleSize); + streamState = STREAM_STATE_END_OF_STREAM; + return C.RESULT_BUFFER_READ; + } + } + + @Override + public void skipToKeyframeBefore(long timeUs) { + // Do nothing. + } + + } + + /* package */ static final class SourceLoadable implements Loadable { + + private final Uri uri; + private final DataSource dataSource; + + private int sampleSize; + private byte[] sampleData; + + public SourceLoadable(Uri uri, DataSource dataSource) { + this.uri = uri; + this.dataSource = dataSource; + } + + @Override + public void cancelLoad() { + // Never happens. + } + + @Override + public boolean isLoadCanceled() { + return false; + } + + @Override + public void load() throws IOException, InterruptedException { + // We always load from the beginning, so reset the sampleSize to 0. + sampleSize = 0; + try { + // Create and open the input. + dataSource.open(new DataSpec(uri)); + // Load the sample data. + int result = 0; + while (result != C.RESULT_END_OF_INPUT) { + sampleSize += result; + if (sampleSize == sampleData.length) { + sampleData = Arrays.copyOf(sampleData, sampleData.length * 2); + } + result = dataSource.read(sampleData, sampleSize, sampleData.length - sampleSize); + } + } finally { + dataSource.close(); + } + } + + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 099ceb9481..ee3c6ed306 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -17,26 +17,17 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.FormatHolder; -import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.source.MediaPeriod.Callback; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.upstream.Loader; -import com.google.android.exoplayer2.upstream.Loader.Loadable; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; /** * Loads data at a given {@link Uri} as a single sample belonging to a single {@link MediaPeriod}. */ -public final class SingleSampleMediaSource implements MediaPeriod, MediaSource, - Loader.Callback { +public final class SingleSampleMediaSource implements MediaSource { /** * Listener of {@link SingleSampleMediaSource} events. @@ -58,26 +49,14 @@ public final class SingleSampleMediaSource implements MediaPeriod, MediaSource, */ public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; - /** - * The initial size of the allocation used to hold the sample data. - */ - private static final int INITIAL_SAMPLE_SIZE = 1; - private final Uri uri; private final DataSource.Factory dataSourceFactory; - private final long durationUs; + private final Format format; private final int minLoadableRetryCount; - private final TrackGroupArray tracks; private final Handler eventHandler; private final EventListener eventListener; private final int eventSourceId; - private final ArrayList sampleStreams; - /* package */ final Format format; - - /* package */ Loader loader; - /* package */ boolean loadingFinished; - /* package */ byte[] sampleData; - /* package */ int sampleSize; + private final Timeline timeline; public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) { @@ -95,21 +74,17 @@ public final class SingleSampleMediaSource implements MediaPeriod, MediaSource, this.uri = uri; this.dataSourceFactory = dataSourceFactory; this.format = format; - this.durationUs = durationUs; this.minLoadableRetryCount = minLoadableRetryCount; this.eventHandler = eventHandler; this.eventListener = eventListener; this.eventSourceId = eventSourceId; - tracks = new TrackGroupArray(new TrackGroup(format)); - sampleData = new byte[INITIAL_SAMPLE_SIZE]; - sampleStreams = new ArrayList<>(); + timeline = SinglePeriodTimeline.createSeekableFinalTimeline(0, durationUs); } // MediaSource implementation. @Override public void prepareSource(MediaSource.Listener listener) { - Timeline timeline = SinglePeriodTimeline.createSeekableFinalTimeline(this, durationUs); listener.onSourceInfoRefreshed(timeline, null); } @@ -124,9 +99,23 @@ public final class SingleSampleMediaSource implements MediaPeriod, MediaSource, } @Override - public MediaPeriod createPeriod(int index) { + public void maybeThrowSourceInfoRefreshError() throws IOException { + // Do nothing. + } + + @Override + public MediaPeriod createPeriod(int index, Callback callback, Allocator allocator, + long positionUs) { Assertions.checkArgument(index == 0); - return this; + MediaPeriod mediaPeriod = new SingleSampleMediaPeriod(uri, dataSourceFactory, format, + minLoadableRetryCount, eventHandler, eventListener, eventSourceId); + callback.onPrepared(mediaPeriod); + return mediaPeriod; + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + ((SingleSampleMediaPeriod) mediaPeriod).release(); } @Override @@ -134,222 +123,4 @@ public final class SingleSampleMediaSource implements MediaPeriod, MediaSource, // Do nothing. } - // MediaPeriod implementation. - - @Override - public void preparePeriod(Callback callback, Allocator allocator, long positionUs) { - loader = new Loader("Loader:SingleSampleMediaSource"); - callback.onPeriodPrepared(this); - } - - @Override - public void maybeThrowPrepareError() throws IOException { - // Do nothing. - } - - @Override - public TrackGroupArray getTrackGroups() { - return tracks; - } - - @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - for (int i = 0; i < selections.length; i++) { - if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { - sampleStreams.remove(streams[i]); - streams[i] = null; - } - if (streams[i] == null && selections[i] != null) { - SampleStreamImpl stream = new SampleStreamImpl(); - sampleStreams.add(stream); - streams[i] = stream; - streamResetFlags[i] = true; - } - } - return positionUs; - } - - @Override - public boolean continueLoading(long positionUs) { - if (loadingFinished || loader.isLoading()) { - return false; - } - loader.startLoading(new SourceLoadable(uri, dataSourceFactory.createDataSource()), this, - minLoadableRetryCount); - return true; - } - - @Override - public long readDiscontinuity() { - return C.UNSET_TIME_US; - } - - @Override - public long getNextLoadPositionUs() { - return loadingFinished || loader.isLoading() ? C.END_OF_SOURCE_US : 0; - } - - @Override - public long getBufferedPositionUs() { - return loadingFinished ? C.END_OF_SOURCE_US : 0; - } - - @Override - public long seekToUs(long positionUs) { - for (int i = 0; i < sampleStreams.size(); i++) { - sampleStreams.get(i).seekToUs(positionUs); - } - return positionUs; - } - - @Override - public void releasePeriod() { - if (loader != null) { - loader.release(); - loader = null; - } - loadingFinished = false; - sampleStreams.clear(); - sampleData = null; - sampleSize = 0; - } - - // Loader.Callback implementation. - - @Override - public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs) { - sampleSize = loadable.sampleSize; - sampleData = loadable.sampleData; - loadingFinished = true; - } - - @Override - public void onLoadCanceled(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, - boolean released) { - // Do nothing. - } - - @Override - public int onLoadError(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, - IOException error) { - notifyLoadError(error); - return Loader.RETRY; - } - - // Internal methods. - - private void notifyLoadError(final IOException e) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onLoadError(eventSourceId, e); - } - }); - } - } - - private final class SampleStreamImpl implements SampleStream { - - private static final int STREAM_STATE_SEND_FORMAT = 0; - private static final int STREAM_STATE_SEND_SAMPLE = 1; - private static final int STREAM_STATE_END_OF_STREAM = 2; - - private int streamState; - - public void seekToUs(long positionUs) { - if (streamState == STREAM_STATE_END_OF_STREAM) { - streamState = STREAM_STATE_SEND_SAMPLE; - } - } - - @Override - public boolean isReady() { - return loadingFinished; - } - - @Override - public void maybeThrowError() throws IOException { - loader.maybeThrowError(); - } - - @Override - public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { - if (streamState == STREAM_STATE_END_OF_STREAM) { - buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); - return C.RESULT_BUFFER_READ; - } else if (streamState == STREAM_STATE_SEND_FORMAT) { - formatHolder.format = format; - streamState = STREAM_STATE_SEND_SAMPLE; - return C.RESULT_FORMAT_READ; - } - - Assertions.checkState(streamState == STREAM_STATE_SEND_SAMPLE); - if (!loadingFinished) { - return C.RESULT_NOTHING_READ; - } else { - buffer.timeUs = 0; - buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); - buffer.ensureSpaceForWrite(sampleSize); - buffer.data.put(sampleData, 0, sampleSize); - streamState = STREAM_STATE_END_OF_STREAM; - return C.RESULT_BUFFER_READ; - } - } - - @Override - public void skipToKeyframeBefore(long timeUs) { - // do nothing - } - - } - - /* package */ static final class SourceLoadable implements Loadable { - - private final Uri uri; - private final DataSource dataSource; - - private int sampleSize; - private byte[] sampleData; - - public SourceLoadable(Uri uri, DataSource dataSource) { - this.uri = uri; - this.dataSource = dataSource; - } - - @Override - public void cancelLoad() { - // Never happens. - } - - @Override - public boolean isLoadCanceled() { - return false; - } - - @Override - public void load() throws IOException, InterruptedException { - // We always load from the beginning, so reset the sampleSize to 0. - sampleSize = 0; - try { - // Create and open the input. - dataSource.open(new DataSpec(uri)); - // Load the sample data. - int result = 0; - while (result != C.RESULT_END_OF_INPUT) { - sampleSize += result; - if (sampleSize == sampleData.length) { - sampleData = Arrays.copyOf(sampleData, sampleData.length * 2); - } - result = dataSource.read(sampleData, sampleSize, sampleData.length - sampleSize); - } - } finally { - dataSource.close(); - } - } - - } - } 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 67dee0f98f..795ae0e43f 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 @@ -42,25 +42,27 @@ import java.util.List; /* package */ final class DashMediaPeriod implements MediaPeriod, SequenceableLoader.Callback> { + /* package */ final int id; private final DashChunkSource.Factory chunkSourceFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; private final long elapsedRealtimeOffset; private final LoaderErrorThrower manifestLoaderErrorThrower; + private final Callback callback; + private final Allocator allocator; private final TrackGroupArray trackGroups; private ChunkSampleStream[] sampleStreams; private CompositeSequenceableLoader sequenceableLoader; - private Callback callback; - private Allocator allocator; private DashManifest manifest; private int index; private Period period; - public DashMediaPeriod(DashManifest manifest, int index, + public DashMediaPeriod(int id, DashManifest manifest, int index, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, EventDispatcher eventDispatcher, long elapsedRealtimeOffset, - LoaderErrorThrower manifestLoaderErrorThrower) { + LoaderErrorThrower manifestLoaderErrorThrower, Callback callback, Allocator allocator) { + this.id = id; this.manifest = manifest; this.index = index; this.chunkSourceFactory = chunkSourceFactory; @@ -68,8 +70,13 @@ import java.util.List; this.eventDispatcher = eventDispatcher; this.elapsedRealtimeOffset = elapsedRealtimeOffset; this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; + this.callback = callback; + this.allocator = allocator; + sampleStreams = newSampleStreamArray(0); + sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); period = manifest.getPeriod(index); trackGroups = buildTrackGroups(period); + callback.onPrepared(this); } public void updateManifest(DashManifest manifest, int index) { @@ -84,17 +91,6 @@ import java.util.List; } } - // MediaPeriod implementation. - - @Override - public void preparePeriod(Callback callback, Allocator allocator, long positionUs) { - this.callback = callback; - this.allocator = allocator; - sampleStreams = newSampleStreamArray(0); - sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); - callback.onPeriodPrepared(this); - } - @Override public void maybeThrowPrepareError() throws IOException { manifestLoaderErrorThrower.maybeThrowError(); @@ -168,19 +164,6 @@ import java.util.List; return positionUs; } - @Override - public void releasePeriod() { - if (sampleStreams != null) { - for (ChunkSampleStream sampleStream : sampleStreams) { - sampleStream.release(); - } - sampleStreams = null; - } - sequenceableLoader = null; - callback = null; - allocator = null; - } - // SequenceableLoader.Callback implementation. @Override 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 e01fc9733b..e8c81abd0b 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 @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.ParserException; 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.MediaPeriod.Callback; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.SeekWindow; import com.google.android.exoplayer2.source.Timeline; @@ -32,6 +33,7 @@ import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.dash.manifest.Period; import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; +import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.ParsingLoadable; @@ -169,17 +171,28 @@ public final class DashMediaSource implements MediaSource { } @Override - public MediaPeriod createPeriod(int index) throws IOException { - if (index >= manifest.getPeriodCount()) { - loader.maybeThrowError(); - return null; - } - DashMediaPeriod mediaPeriod = new DashMediaPeriod(manifest, index, chunkSourceFactory, - minLoadableRetryCount, eventDispatcher, elapsedRealtimeOffsetMs, loader); - periodsById.put(firstPeriodId + index, mediaPeriod); + public void maybeThrowSourceInfoRefreshError() throws IOException { + loader.maybeThrowError(); + } + + @Override + public MediaPeriod createPeriod(int index, Callback callback, Allocator allocator, + long positionUs) { + DashMediaPeriod mediaPeriod = new DashMediaPeriod(firstPeriodId + index, manifest, index, + chunkSourceFactory, minLoadableRetryCount, eventDispatcher, elapsedRealtimeOffsetMs, loader, + callback, allocator); + periodsById.put(mediaPeriod.id, mediaPeriod); return mediaPeriod; } + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + int id = ((DashMediaPeriod) mediaPeriod).id; + if (id >= firstPeriodId) { + periodsById.remove(id); + } + } + @Override public void releaseSource() { dataSource = null; @@ -246,7 +259,6 @@ public final class DashMediaSource implements MediaSource { } else { // Remove old periods. while (periodsToRemoveCount-- > 0) { - periodsById.remove(firstPeriodId); firstPeriodId++; periodCount--; } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java new file mode 100644 index 0000000000..70518e597e --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -0,0 +1,392 @@ +/* + * 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.hls; + +import android.net.Uri; +import android.text.TextUtils; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.CompositeSequenceableLoader; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.source.SinglePeriodTimeline; +import com.google.android.exoplayer2.source.Timeline; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; +import com.google.android.exoplayer2.source.hls.playlist.Variant; +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 com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.MimeTypes; +import java.io.IOException; +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; + +/** + * A {@link MediaPeriod} that loads an HLS stream. + */ +/* package */ final class HlsMediaPeriod implements MediaPeriod, + Loader.Callback>, HlsSampleStreamWrapper.Callback { + + private final DataSource.Factory dataSourceFactory; + private final int minLoadableRetryCount; + private final EventDispatcher eventDispatcher; + private final MediaSource.Listener sourceListener; + private final Callback callback; + private final Allocator allocator; + private final IdentityHashMap streamWrapperIndices; + private final PtsTimestampAdjusterProvider timestampAdjusterProvider; + private final HlsPlaylistParser manifestParser; + private final Loader manifestFetcher; + private final long preparePositionUs; + + private int pendingPrepareCount; + private HlsPlaylist playlist; + private boolean seenFirstTrackSelection; + private long durationUs; + private boolean isLive; + private TrackGroupArray trackGroups; + private HlsSampleStreamWrapper[] sampleStreamWrappers; + private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; + private CompositeSequenceableLoader sequenceableLoader; + + public HlsMediaPeriod(Uri manifestUri, DataSource.Factory dataSourceFactory, + int minLoadableRetryCount, EventDispatcher eventDispatcher, + MediaSource.Listener sourceListener, Callback callback, Allocator allocator, + long positionUs) { + this.dataSourceFactory = dataSourceFactory; + this.minLoadableRetryCount = minLoadableRetryCount; + this.eventDispatcher = eventDispatcher; + this.sourceListener = sourceListener; + this.callback = callback; + this.allocator = allocator; + streamWrapperIndices = new IdentityHashMap<>(); + timestampAdjusterProvider = new PtsTimestampAdjusterProvider(); + manifestParser = new HlsPlaylistParser(); + manifestFetcher = new Loader("Loader:ManifestFetcher"); + preparePositionUs = positionUs; + + ParsingLoadable loadable = new ParsingLoadable<>( + dataSourceFactory.createDataSource(), manifestUri, C.DATA_TYPE_MANIFEST, manifestParser); + long elapsedRealtimeMs = manifestFetcher.startLoading(loadable, this, minLoadableRetryCount); + eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs); + } + + public void release() { + manifestFetcher.release(); + for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { + sampleStreamWrapper.release(); + } + } + + @Override + public void maybeThrowPrepareError() throws IOException { + if (sampleStreamWrappers == null) { + manifestFetcher.maybeThrowError(); + } else { + for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { + sampleStreamWrapper.maybeThrowPrepareError(); + } + } + } + + @Override + public TrackGroupArray getTrackGroups() { + return trackGroups; + } + + @Override + public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + // Map each selection and stream onto a child period index. + int[] streamChildIndices = new int[selections.length]; + int[] selectionChildIndices = new int[selections.length]; + for (int i = 0; i < selections.length; i++) { + streamChildIndices[i] = streams[i] == null ? -1 : streamWrapperIndices.get(streams[i]); + selectionChildIndices[i] = -1; + if (selections[i] != null) { + TrackGroup trackGroup = selections[i].getTrackGroup(); + for (int j = 0; j < sampleStreamWrappers.length; j++) { + if (sampleStreamWrappers[j].getTrackGroups().indexOf(trackGroup) != -1) { + selectionChildIndices[i] = j; + break; + } + } + } + } + boolean selectedNewTracks = false; + streamWrapperIndices.clear(); + // Select tracks for each child, copying the resulting streams back into the streams array. + SampleStream[] childStreams = new SampleStream[selections.length]; + TrackSelection[] childSelections = new TrackSelection[selections.length]; + ArrayList enabledSampleStreamWrapperList = new ArrayList<>( + sampleStreamWrappers.length); + for (int i = 0; i < sampleStreamWrappers.length; i++) { + for (int j = 0; j < selections.length; j++) { + childStreams[j] = streamChildIndices[j] == i ? streams[j] : null; + childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null; + } + selectedNewTracks |= sampleStreamWrappers[i].selectTracks(childSelections, + mayRetainStreamFlags, childStreams, streamResetFlags, !seenFirstTrackSelection); + boolean wrapperEnabled = false; + for (int j = 0; j < selections.length; j++) { + if (selectionChildIndices[j] == i) { + streams[j] = childStreams[j]; + if (childStreams[j] != null) { + wrapperEnabled = true; + streamWrapperIndices.put(childStreams[j], i); + } + } + } + if (wrapperEnabled) { + enabledSampleStreamWrapperList.add(sampleStreamWrappers[i]); + } + } + // Update the local state. + enabledSampleStreamWrappers = new HlsSampleStreamWrapper[enabledSampleStreamWrapperList.size()]; + enabledSampleStreamWrapperList.toArray(enabledSampleStreamWrappers); + sequenceableLoader = new CompositeSequenceableLoader(enabledSampleStreamWrappers); + if (seenFirstTrackSelection && selectedNewTracks) { + seekToUs(positionUs); + // We'll need to reset renderers consuming from all streams due to the seek. + for (int i = 0; i < selections.length; i++) { + if (streams[i] != null) { + streamResetFlags[i] = true; + } + } + } + seenFirstTrackSelection = true; + return positionUs; + } + + @Override + public boolean continueLoading(long positionUs) { + return sequenceableLoader.continueLoading(positionUs); + } + + @Override + public long getNextLoadPositionUs() { + return sequenceableLoader.getNextLoadPositionUs(); + } + + @Override + public long readDiscontinuity() { + return C.UNSET_TIME_US; + } + + @Override + public long getBufferedPositionUs() { + long bufferedPositionUs = Long.MAX_VALUE; + for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { + long rendererBufferedPositionUs = sampleStreamWrapper.getBufferedPositionUs(); + if (rendererBufferedPositionUs != C.END_OF_SOURCE_US) { + bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs); + } + } + return bufferedPositionUs == Long.MAX_VALUE ? C.END_OF_SOURCE_US : bufferedPositionUs; + } + + @Override + public long seekToUs(long positionUs) { + // Treat all seeks into non-seekable media as being to t=0. + positionUs = isLive ? 0 : positionUs; + timestampAdjusterProvider.reset(); + for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { + sampleStreamWrapper.seekTo(positionUs); + } + return positionUs; + } + + // Loader.Callback implementation. + + @Override + public void onLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs) { + eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded()); + playlist = loadable.getResult(); + List sampleStreamWrapperList = buildSampleStreamWrappers(); + sampleStreamWrappers = new HlsSampleStreamWrapper[sampleStreamWrapperList.size()]; + sampleStreamWrapperList.toArray(sampleStreamWrappers); + pendingPrepareCount = sampleStreamWrappers.length; + for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { + sampleStreamWrapper.prepare(); + } + } + + @Override + public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs, boolean released) { + eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs, + loadDurationMs, loadable.bytesLoaded()); + } + + @Override + public int onLoadError(ParsingLoadable loadable, long elapsedRealtimeMs, + long loadDurationMs, IOException error) { + boolean isFatal = error instanceof ParserException; + eventDispatcher.loadError(loadable.dataSpec, loadable.type, elapsedRealtimeMs, loadDurationMs, + loadable.bytesLoaded(), error, isFatal); + return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; + } + + // HlsSampleStreamWrapper.Callback implementation. + + @Override + public void onPrepared() { + if (--pendingPrepareCount > 0) { + return; + } + + // The wrapper at index 0 is the one of type TRACK_TYPE_DEFAULT. + durationUs = sampleStreamWrappers[0].getDurationUs(); + isLive = sampleStreamWrappers[0].isLive(); + + int totalTrackGroupCount = 0; + for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { + totalTrackGroupCount += sampleStreamWrapper.getTrackGroups().length; + } + TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount]; + int trackGroupIndex = 0; + for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { + int wrapperTrackGroupCount = sampleStreamWrapper.getTrackGroups().length; + for (int j = 0; j < wrapperTrackGroupCount; j++) { + trackGroupArray[trackGroupIndex++] = sampleStreamWrapper.getTrackGroups().get(j); + } + } + trackGroups = new TrackGroupArray(trackGroupArray); + callback.onPrepared(this); + + // TODO[playlists]: Calculate the seek window. + Timeline timeline = isLive + ? SinglePeriodTimeline.createUnseekableFinalTimeline(0, durationUs) + : SinglePeriodTimeline.createSeekableFinalTimeline(0, durationUs); + sourceListener.onSourceInfoRefreshed(timeline, playlist); + } + + @Override + public void onContinueLoadingRequested(HlsSampleStreamWrapper sampleStreamWrapper) { + if (trackGroups == null) { + // Still preparing. + return; + } + callback.onContinueLoadingRequested(this); + } + + // Internal methods. + + private List buildSampleStreamWrappers() { + ArrayList sampleStreamWrappers = new ArrayList<>(); + String baseUri = playlist.baseUri; + + if (playlist instanceof HlsMediaPlaylist) { + Format format = Format.createContainerFormat("0", MimeTypes.APPLICATION_M3U8, null, + Format.NO_VALUE); + Variant[] variants = new Variant[] {new Variant(playlist.baseUri, format, null)}; + sampleStreamWrappers.add(buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants, + null, null)); + return sampleStreamWrappers; + } + + HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; + + // Build the default stream wrapper. + List selectedVariants = new ArrayList<>(masterPlaylist.variants); + ArrayList definiteVideoVariants = new ArrayList<>(); + ArrayList definiteAudioOnlyVariants = new ArrayList<>(); + for (int i = 0; i < selectedVariants.size(); i++) { + Variant variant = selectedVariants.get(i); + if (variant.format.height > 0 || variantHasExplicitCodecWithPrefix(variant, "avc")) { + definiteVideoVariants.add(variant); + } else if (variantHasExplicitCodecWithPrefix(variant, "mp4a")) { + definiteAudioOnlyVariants.add(variant); + } + } + if (!definiteVideoVariants.isEmpty()) { + // We've identified some variants as definitely containing video. Assume variants within the + // master playlist are marked consistently, and hence that we have the full set. Filter out + // any other variants, which are likely to be audio only. + selectedVariants = definiteVideoVariants; + } else if (definiteAudioOnlyVariants.size() < selectedVariants.size()) { + // We've identified some variants, but not all, as being audio only. Filter them out to leave + // the remaining variants, which are likely to contain video. + selectedVariants.removeAll(definiteAudioOnlyVariants); + } else { + // Leave the enabled variants unchanged. They're likely either all video or all audio. + } + if (!selectedVariants.isEmpty()) { + Variant[] variants = new Variant[selectedVariants.size()]; + selectedVariants.toArray(variants); + sampleStreamWrappers.add(buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants, + masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat)); + } + + // Build the audio stream wrapper if applicable. + List audioVariants = masterPlaylist.audios; + if (!audioVariants.isEmpty()) { + Variant[] variants = new Variant[audioVariants.size()]; + audioVariants.toArray(variants); + sampleStreamWrappers.add(buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO, baseUri, variants, null, + null)); + } + + // Build the text stream wrapper if applicable. + List subtitleVariants = masterPlaylist.subtitles; + if (!subtitleVariants.isEmpty()) { + Variant[] variants = new Variant[subtitleVariants.size()]; + subtitleVariants.toArray(variants); + sampleStreamWrappers.add(buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, baseUri, variants, null, + null)); + } + + return sampleStreamWrappers; + } + + private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, String baseUri, + Variant[] variants, Format muxedAudioFormat, Format muxedCaptionFormat) { + DataSource dataSource = dataSourceFactory.createDataSource(); + HlsChunkSource defaultChunkSource = new HlsChunkSource(baseUri, variants, dataSource, + timestampAdjusterProvider); + return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, + preparePositionUs, muxedAudioFormat, muxedCaptionFormat, minLoadableRetryCount, + eventDispatcher); + } + + private static boolean variantHasExplicitCodecWithPrefix(Variant variant, String prefix) { + String codecs = variant.codecs; + if (TextUtils.isEmpty(codecs)) { + return false; + } + String[] codecArray = codecs.split("(\\s*,\\s*)|(\\s*$)"); + for (String codec : codecArray) { + if (codec.startsWith(prefix)) { + return true; + } + } + return false; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index fd9aebdc1d..6bb4a6b886 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -17,42 +17,21 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; import android.os.Handler; -import android.text.TextUtils; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; -import com.google.android.exoplayer2.source.CompositeSequenceableLoader; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaPeriod.Callback; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.Timeline; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; -import com.google.android.exoplayer2.source.hls.playlist.Variant; -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 com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.MimeTypes; -import java.io.IOException; -import java.util.ArrayList; -import java.util.IdentityHashMap; -import java.util.List; /** * An HLS {@link MediaSource}. */ -public final class HlsMediaSource implements MediaPeriod, MediaSource, - Loader.Callback>, HlsSampleStreamWrapper.Callback { +public final class HlsMediaSource implements MediaSource { /** * The default minimum number of times to retry loading data prior to failing. @@ -63,27 +42,8 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, private final DataSource.Factory dataSourceFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; - private final IdentityHashMap streamWrapperIndices; - private final PtsTimestampAdjusterProvider timestampAdjusterProvider; - private final HlsPlaylistParser manifestParser; private MediaSource.Listener sourceListener; - private DataSource manifestDataSource; - private Loader manifestFetcher; - - private Callback callback; - private Allocator allocator; - private long preparePositionUs; - private int pendingPrepareCount; - - private HlsPlaylist playlist; - private boolean seenFirstTrackSelection; - private long durationUs; - private boolean isLive; - private TrackGroupArray trackGroups; - private HlsSampleStreamWrapper[] sampleStreamWrappers; - private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; - private CompositeSequenceableLoader sequenceableLoader; public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -98,17 +58,12 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; eventDispatcher = new EventDispatcher(eventHandler, eventListener); - streamWrapperIndices = new IdentityHashMap<>(); - timestampAdjusterProvider = new PtsTimestampAdjusterProvider(); - manifestParser = new HlsPlaylistParser(); } - // MediaSource implementation. - @Override public void prepareSource(MediaSource.Listener listener) { - sourceListener = listener; // TODO: Defer until the playlist has been loaded. + sourceListener = listener; listener.onSourceInfoRefreshed(SinglePeriodTimeline.createNonFinalTimeline(this), null); } @@ -124,9 +79,21 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, } @Override - public MediaPeriod createPeriod(int index) { + public void maybeThrowSourceInfoRefreshError() { + // Do nothing. + } + + @Override + public MediaPeriod createPeriod(int index, Callback callback, Allocator allocator, + long positionUs) { Assertions.checkArgument(index == 0); - return this; + return new HlsMediaPeriod(manifestUri, dataSourceFactory, minLoadableRetryCount, + eventDispatcher, sourceListener, callback, allocator, positionUs); + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + ((HlsMediaPeriod) mediaPeriod).release(); } @Override @@ -134,334 +101,4 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, sourceListener = null; } - // MediaPeriod implementation. - - @Override - public void preparePeriod(Callback callback, Allocator allocator, long positionUs) { - this.callback = callback; - this.allocator = allocator; - preparePositionUs = positionUs; - manifestDataSource = dataSourceFactory.createDataSource(); - manifestFetcher = new Loader("Loader:ManifestFetcher"); - ParsingLoadable loadable = new ParsingLoadable<>(manifestDataSource, manifestUri, - C.DATA_TYPE_MANIFEST, manifestParser); - long elapsedRealtimeMs = manifestFetcher.startLoading(loadable, this, minLoadableRetryCount); - eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs); - } - - @Override - public void maybeThrowPrepareError() throws IOException { - if (sampleStreamWrappers == null) { - manifestFetcher.maybeThrowError(); - } else { - for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { - sampleStreamWrapper.maybeThrowPrepareError(); - } - } - } - - @Override - public TrackGroupArray getTrackGroups() { - return trackGroups; - } - - @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - // Map each selection and stream onto a child period index. - int[] streamChildIndices = new int[selections.length]; - int[] selectionChildIndices = new int[selections.length]; - for (int i = 0; i < selections.length; i++) { - streamChildIndices[i] = streams[i] == null ? -1 : streamWrapperIndices.get(streams[i]); - selectionChildIndices[i] = -1; - if (selections[i] != null) { - TrackGroup trackGroup = selections[i].getTrackGroup(); - for (int j = 0; j < sampleStreamWrappers.length; j++) { - if (sampleStreamWrappers[j].getTrackGroups().indexOf(trackGroup) != -1) { - selectionChildIndices[i] = j; - break; - } - } - } - } - boolean selectedNewTracks = false; - streamWrapperIndices.clear(); - // Select tracks for each child, copying the resulting streams back into the streams array. - SampleStream[] childStreams = new SampleStream[selections.length]; - TrackSelection[] childSelections = new TrackSelection[selections.length]; - ArrayList enabledSampleStreamWrapperList = new ArrayList<>( - sampleStreamWrappers.length); - for (int i = 0; i < sampleStreamWrappers.length; i++) { - for (int j = 0; j < selections.length; j++) { - childStreams[j] = streamChildIndices[j] == i ? streams[j] : null; - childSelections[j] = selectionChildIndices[j] == i ? selections[j] : null; - } - selectedNewTracks |= sampleStreamWrappers[i].selectTracks(childSelections, - mayRetainStreamFlags, childStreams, streamResetFlags, !seenFirstTrackSelection); - boolean wrapperEnabled = false; - for (int j = 0; j < selections.length; j++) { - if (selectionChildIndices[j] == i) { - streams[j] = childStreams[j]; - if (childStreams[j] != null) { - wrapperEnabled = true; - streamWrapperIndices.put(childStreams[j], i); - } - } - } - if (wrapperEnabled) { - enabledSampleStreamWrapperList.add(sampleStreamWrappers[i]); - } - } - // Update the local state. - enabledSampleStreamWrappers = new HlsSampleStreamWrapper[enabledSampleStreamWrapperList.size()]; - enabledSampleStreamWrapperList.toArray(enabledSampleStreamWrappers); - sequenceableLoader = new CompositeSequenceableLoader(enabledSampleStreamWrappers); - if (seenFirstTrackSelection && selectedNewTracks) { - seekToUs(positionUs); - // We'll need to reset renderers consuming from all streams due to the seek. - for (int i = 0; i < selections.length; i++) { - if (streams[i] != null) { - streamResetFlags[i] = true; - } - } - } - seenFirstTrackSelection = true; - return positionUs; - } - - @Override - public boolean continueLoading(long positionUs) { - return sequenceableLoader.continueLoading(positionUs); - } - - @Override - public long getNextLoadPositionUs() { - return sequenceableLoader.getNextLoadPositionUs(); - } - - @Override - public long readDiscontinuity() { - return C.UNSET_TIME_US; - } - - @Override - public long getBufferedPositionUs() { - long bufferedPositionUs = Long.MAX_VALUE; - for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { - long rendererBufferedPositionUs = sampleStreamWrapper.getBufferedPositionUs(); - if (rendererBufferedPositionUs != C.END_OF_SOURCE_US) { - bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs); - } - } - return bufferedPositionUs == Long.MAX_VALUE ? C.END_OF_SOURCE_US : bufferedPositionUs; - } - - @Override - public long seekToUs(long positionUs) { - // Treat all seeks into non-seekable media as being to t=0. - positionUs = isLive ? 0 : positionUs; - timestampAdjusterProvider.reset(); - for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) { - sampleStreamWrapper.seekTo(positionUs); - } - return positionUs; - } - - @Override - public void releasePeriod() { - streamWrapperIndices.clear(); - timestampAdjusterProvider.reset(); - manifestDataSource = null; - if (manifestFetcher != null) { - manifestFetcher.release(); - manifestFetcher = null; - } - callback = null; - allocator = null; - preparePositionUs = 0; - pendingPrepareCount = 0; - playlist = null; - seenFirstTrackSelection = false; - durationUs = 0; - isLive = false; - trackGroups = null; - if (sampleStreamWrappers != null) { - for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { - sampleStreamWrapper.release(); - } - sampleStreamWrappers = null; - } - enabledSampleStreamWrappers = null; - sequenceableLoader = null; - } - - // Loader.Callback implementation. - - @Override - public void onLoadCompleted(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs) { - eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - playlist = loadable.getResult(); - List sampleStreamWrapperList = buildSampleStreamWrappers(); - sampleStreamWrappers = new HlsSampleStreamWrapper[sampleStreamWrapperList.size()]; - sampleStreamWrapperList.toArray(sampleStreamWrappers); - pendingPrepareCount = sampleStreamWrappers.length; - for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { - sampleStreamWrapper.prepare(); - } - } - - @Override - public void onLoadCanceled(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, boolean released) { - eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs, - loadDurationMs, loadable.bytesLoaded()); - } - - @Override - public int onLoadError(ParsingLoadable loadable, long elapsedRealtimeMs, - long loadDurationMs, IOException error) { - boolean isFatal = error instanceof ParserException; - eventDispatcher.loadError(loadable.dataSpec, loadable.type, elapsedRealtimeMs, loadDurationMs, - loadable.bytesLoaded(), error, isFatal); - return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; - } - - // HlsSampleStreamWrapper.Callback implementation. - - @Override - public void onPrepared() { - if (--pendingPrepareCount > 0) { - return; - } - - // The wrapper at index 0 is the one of type TRACK_TYPE_DEFAULT. - durationUs = sampleStreamWrappers[0].getDurationUs(); - isLive = sampleStreamWrappers[0].isLive(); - - int totalTrackGroupCount = 0; - for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { - totalTrackGroupCount += sampleStreamWrapper.getTrackGroups().length; - } - TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount]; - int trackGroupIndex = 0; - for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { - int wrapperTrackGroupCount = sampleStreamWrapper.getTrackGroups().length; - for (int j = 0; j < wrapperTrackGroupCount; j++) { - trackGroupArray[trackGroupIndex++] = sampleStreamWrapper.getTrackGroups().get(j); - } - } - trackGroups = new TrackGroupArray(trackGroupArray); - callback.onPeriodPrepared(this); - - // TODO[playlists]: Calculate the seek window. - Timeline timeline = - isLive ? SinglePeriodTimeline.createUnseekableFinalTimeline(this, durationUs) - : SinglePeriodTimeline.createSeekableFinalTimeline(this, durationUs); - sourceListener.onSourceInfoRefreshed(timeline, playlist); - } - - @Override - public void onContinueLoadingRequested(HlsSampleStreamWrapper sampleStreamWrapper) { - if (trackGroups == null) { - // Still preparing. - return; - } - callback.onContinueLoadingRequested(this); - } - - // Internal methods. - - private List buildSampleStreamWrappers() { - ArrayList sampleStreamWrappers = new ArrayList<>(); - String baseUri = playlist.baseUri; - - if (playlist instanceof HlsMediaPlaylist) { - Format format = Format.createContainerFormat("0", MimeTypes.APPLICATION_M3U8, null, - Format.NO_VALUE); - Variant[] variants = new Variant[] {new Variant(playlist.baseUri, format, null)}; - sampleStreamWrappers.add(buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants, - null, null)); - return sampleStreamWrappers; - } - - HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; - - // Build the default stream wrapper. - List selectedVariants = new ArrayList<>(masterPlaylist.variants); - ArrayList definiteVideoVariants = new ArrayList<>(); - ArrayList definiteAudioOnlyVariants = new ArrayList<>(); - for (int i = 0; i < selectedVariants.size(); i++) { - Variant variant = selectedVariants.get(i); - if (variant.format.height > 0 || variantHasExplicitCodecWithPrefix(variant, "avc")) { - definiteVideoVariants.add(variant); - } else if (variantHasExplicitCodecWithPrefix(variant, "mp4a")) { - definiteAudioOnlyVariants.add(variant); - } - } - if (!definiteVideoVariants.isEmpty()) { - // We've identified some variants as definitely containing video. Assume variants within the - // master playlist are marked consistently, and hence that we have the full set. Filter out - // any other variants, which are likely to be audio only. - selectedVariants = definiteVideoVariants; - } else if (definiteAudioOnlyVariants.size() < selectedVariants.size()) { - // We've identified some variants, but not all, as being audio only. Filter them out to leave - // the remaining variants, which are likely to contain video. - selectedVariants.removeAll(definiteAudioOnlyVariants); - } else { - // Leave the enabled variants unchanged. They're likely either all video or all audio. - } - if (!selectedVariants.isEmpty()) { - Variant[] variants = new Variant[selectedVariants.size()]; - selectedVariants.toArray(variants); - sampleStreamWrappers.add(buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants, - masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat)); - } - - // Build the audio stream wrapper if applicable. - List audioVariants = masterPlaylist.audios; - if (!audioVariants.isEmpty()) { - Variant[] variants = new Variant[audioVariants.size()]; - audioVariants.toArray(variants); - sampleStreamWrappers.add(buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO, baseUri, variants, null, - null)); - } - - // Build the text stream wrapper if applicable. - List subtitleVariants = masterPlaylist.subtitles; - if (!subtitleVariants.isEmpty()) { - Variant[] variants = new Variant[subtitleVariants.size()]; - subtitleVariants.toArray(variants); - sampleStreamWrappers.add(buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, baseUri, variants, null, - null)); - } - - return sampleStreamWrappers; - } - - private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, String baseUri, - Variant[] variants, Format muxedAudioFormat, Format muxedCaptionFormat) { - DataSource dataSource = dataSourceFactory.createDataSource(); - HlsChunkSource defaultChunkSource = new HlsChunkSource(baseUri, variants, dataSource, - timestampAdjusterProvider); - return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, - preparePositionUs, muxedAudioFormat, muxedCaptionFormat, minLoadableRetryCount, - eventDispatcher); - } - - private static boolean variantHasExplicitCodecWithPrefix(Variant variant, String prefix) { - String codecs = variant.codecs; - if (TextUtils.isEmpty(codecs)) { - return false; - } - String[] codecArray = codecs.split("(\\s*,\\s*)|(\\s*$)"); - for (String codec : codecArray) { - if (codec.startsWith(prefix)) { - return true; - } - } - return false; - } - } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index 297fbe1151..aae6e648cd 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -46,23 +46,25 @@ import java.util.ArrayList; private final LoaderErrorThrower manifestLoaderErrorThrower; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; + private final Callback callback; + private final Allocator allocator; private final TrackGroupArray trackGroups; private final TrackEncryptionBox[] trackEncryptionBoxes; private SsManifest manifest; private ChunkSampleStream[] sampleStreams; private CompositeSequenceableLoader sequenceableLoader; - private Callback callback; - private Allocator allocator; public SsMediaPeriod(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, EventDispatcher eventDispatcher, - LoaderErrorThrower manifestLoaderErrorThrower) { - this.manifest = manifest; + LoaderErrorThrower manifestLoaderErrorThrower, Callback callback, Allocator allocator) { this.chunkSourceFactory = chunkSourceFactory; this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.minLoadableRetryCount = minLoadableRetryCount; this.eventDispatcher = eventDispatcher; + this.callback = callback; + this.allocator = allocator; + trackGroups = buildTrackGroups(manifest); ProtectionElement protectionElement = manifest.protectionElement; if (protectionElement != null) { @@ -72,25 +74,23 @@ import java.util.ArrayList; } else { trackEncryptionBoxes = null; } + this.manifest = manifest; + sampleStreams = newSampleStreamArray(0); + sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); } public void updateManifest(SsManifest manifest) { this.manifest = manifest; - if (sampleStreams != null) { - for (ChunkSampleStream sampleStream : sampleStreams) { - sampleStream.getChunkSource().updateManifest(manifest); - } - callback.onContinueLoadingRequested(this); + for (ChunkSampleStream sampleStream : sampleStreams) { + sampleStream.getChunkSource().updateManifest(manifest); } + callback.onContinueLoadingRequested(this); } - @Override - public void preparePeriod(Callback callback, Allocator allocator, long positionUs) { - this.callback = callback; - this.allocator = allocator; - sampleStreams = newSampleStreamArray(0); - sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); - callback.onPeriodPrepared(this); + public void release() { + for (ChunkSampleStream sampleStream : sampleStreams) { + sampleStream.release(); + } } @Override @@ -166,19 +166,6 @@ import java.util.ArrayList; return positionUs; } - @Override - public void releasePeriod() { - if (sampleStreams != null) { - for (ChunkSampleStream sampleStream : sampleStreams) { - sampleStream.release(); - } - sampleStreams = null; - } - sequenceableLoader = null; - callback = null; - allocator = null; - } - // SequenceableLoader.Callback implementation @Override diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index af36757761..3af45e654e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.ParserException; 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.MediaPeriod.Callback; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.SeekWindow; import com.google.android.exoplayer2.source.SinglePeriodTimeline; @@ -30,12 +31,14 @@ import com.google.android.exoplayer2.source.Timeline; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; +import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.ArrayList; /** * A SmoothStreaming {@link MediaSource}. @@ -61,6 +64,7 @@ public final class SsMediaSource implements MediaSource, private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; private final SsManifestParser manifestParser; + private final ArrayList mediaPeriods; private MediaSource.Listener sourceListener; private DataSource manifestDataSource; @@ -71,7 +75,6 @@ public final class SsMediaSource implements MediaSource, private SeekWindow seekWindow; private Handler manifestRefreshHandler; - private SsMediaPeriod period; public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory, Handler eventHandler, @@ -90,6 +93,7 @@ public final class SsMediaSource implements MediaSource, this.minLoadableRetryCount = minLoadableRetryCount; this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); manifestParser = new SsManifestParser(); + mediaPeriods = new ArrayList<>(); } // MediaSource implementation. @@ -122,15 +126,29 @@ public final class SsMediaSource implements MediaSource, } @Override - public MediaPeriod createPeriod(int index) { + public void maybeThrowSourceInfoRefreshError() throws IOException { + manifestLoader.maybeThrowError(); + } + + @Override + public MediaPeriod createPeriod(int index, Callback callback, Allocator allocator, + long positionUs) { Assertions.checkArgument(index == 0); + SsMediaPeriod period = new SsMediaPeriod(manifest, chunkSourceFactory, minLoadableRetryCount, + eventDispatcher, manifestLoader, callback, allocator); + mediaPeriods.add(period); return period; } + @Override + public void releasePeriod(MediaPeriod period) { + ((SsMediaPeriod) period).release(); + mediaPeriods.remove(period); + } + @Override public void releaseSource() { sourceListener = null; - period = null; manifest = null; manifestDataSource = null; manifestLoadStartTimestamp = 0; @@ -153,11 +171,8 @@ public final class SsMediaSource implements MediaSource, loadDurationMs, loadable.bytesLoaded()); manifest = loadable.getResult(); manifestLoadStartTimestamp = elapsedRealtimeMs - loadDurationMs; - if (period == null) { - period = new SsMediaPeriod(manifest, chunkSourceFactory, minLoadableRetryCount, - eventDispatcher, manifestLoader); - } else { - period.updateManifest(manifest); + for (int i = 0; i < mediaPeriods.size(); i++) { + mediaPeriods.get(i).updateManifest(manifest); } Timeline timeline; if (manifest.isLive) { @@ -175,9 +190,9 @@ public final class SsMediaSource implements MediaSource, SeekWindow.createWindow(0, startTimeUs, 0, startTimeUs + manifest.dvrWindowLengthUs)); } } else if (manifest.durationUs == C.UNSET_TIME_US) { - timeline = SinglePeriodTimeline.createUnseekableFinalTimeline(this, C.UNSET_TIME_US); + timeline = SinglePeriodTimeline.createUnseekableFinalTimeline(0, C.UNSET_TIME_US); } else { - timeline = SinglePeriodTimeline.createSeekableFinalTimeline(this, manifest.durationUs); + timeline = SinglePeriodTimeline.createSeekableFinalTimeline(0, manifest.durationUs); } seekWindow = timeline.getSeekWindow(0); sourceListener.onSourceInfoRefreshed(timeline, manifest);