Don't reuse MediaPeriods.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=130266483
This commit is contained in:
andrewlewis 2016-08-15 03:32:19 -07:00 committed by Oliver Woodman
parent b5e41a903d
commit b120bea029
16 changed files with 1576 additions and 1425 deletions

View File

@ -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);

View File

@ -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<MediaPeriod, Integer> 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

View File

@ -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<ExtractorMediaPeriod.ExtractingLoadable>, 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;
}
}
}
}

View File

@ -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}.
* <p>
* 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.
*
* <p>Note that the built-in extractors for AAC, MPEG TS and FLV streams do not support seeking.
* <p>
* 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<ExtractorMediaSource.ExtractingLoadable>,
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;
}
}
}
}

View File

@ -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.
* <p>
* {@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.
* <p>
* This method should be called when the period is no longer required. It may be called in any
* state.
*/
void releasePeriod();
}

View File

@ -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.
* <p>
* {@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.

View File

@ -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<SampleStream, Integer> 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

View File

@ -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

View File

@ -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<SingleSampleMediaPeriod.SourceLoadable> {
/**
* 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<SampleStreamImpl> 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();
}
}
}
}

View File

@ -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<SingleSampleMediaSource.SourceLoadable> {
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<SampleStreamImpl> 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();
}
}
}
}

View File

@ -42,25 +42,27 @@ import java.util.List;
/* package */ final class DashMediaPeriod implements MediaPeriod,
SequenceableLoader.Callback<ChunkSampleStream<DashChunkSource>> {
/* 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<DashChunkSource>[] 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<DashChunkSource> sampleStream : sampleStreams) {
sampleStream.release();
}
sampleStreams = null;
}
sequenceableLoader = null;
callback = null;
allocator = null;
}
// SequenceableLoader.Callback implementation.
@Override

View File

@ -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--;
}

View File

@ -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<ParsingLoadable<HlsPlaylist>>, 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<SampleStream, Integer> 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<HlsPlaylist> 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<HlsSampleStreamWrapper> 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<HlsPlaylist> loadable, long elapsedRealtimeMs,
long loadDurationMs) {
eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded());
playlist = loadable.getResult();
List<HlsSampleStreamWrapper> sampleStreamWrapperList = buildSampleStreamWrappers();
sampleStreamWrappers = new HlsSampleStreamWrapper[sampleStreamWrapperList.size()];
sampleStreamWrapperList.toArray(sampleStreamWrappers);
pendingPrepareCount = sampleStreamWrappers.length;
for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
sampleStreamWrapper.prepare();
}
}
@Override
public void onLoadCanceled(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
long loadDurationMs, boolean released) {
eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded());
}
@Override
public int onLoadError(ParsingLoadable<HlsPlaylist> 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<HlsSampleStreamWrapper> buildSampleStreamWrappers() {
ArrayList<HlsSampleStreamWrapper> 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<Variant> selectedVariants = new ArrayList<>(masterPlaylist.variants);
ArrayList<Variant> definiteVideoVariants = new ArrayList<>();
ArrayList<Variant> 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<Variant> 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<Variant> 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;
}
}

View File

@ -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<ParsingLoadable<HlsPlaylist>>, 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<SampleStream, Integer> 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<HlsPlaylist> 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<HlsSampleStreamWrapper> 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<HlsPlaylist> loadable, long elapsedRealtimeMs,
long loadDurationMs) {
eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded());
playlist = loadable.getResult();
List<HlsSampleStreamWrapper> sampleStreamWrapperList = buildSampleStreamWrappers();
sampleStreamWrappers = new HlsSampleStreamWrapper[sampleStreamWrapperList.size()];
sampleStreamWrapperList.toArray(sampleStreamWrappers);
pendingPrepareCount = sampleStreamWrappers.length;
for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) {
sampleStreamWrapper.prepare();
}
}
@Override
public void onLoadCanceled(ParsingLoadable<HlsPlaylist> loadable, long elapsedRealtimeMs,
long loadDurationMs, boolean released) {
eventDispatcher.loadCompleted(loadable.dataSpec, loadable.type, elapsedRealtimeMs,
loadDurationMs, loadable.bytesLoaded());
}
@Override
public int onLoadError(ParsingLoadable<HlsPlaylist> 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<HlsSampleStreamWrapper> buildSampleStreamWrappers() {
ArrayList<HlsSampleStreamWrapper> 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<Variant> selectedVariants = new ArrayList<>(masterPlaylist.variants);
ArrayList<Variant> definiteVideoVariants = new ArrayList<>();
ArrayList<Variant> 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<Variant> 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<Variant> 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;
}
}

View File

@ -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<SsChunkSource>[] 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<SsChunkSource> sampleStream : sampleStreams) {
sampleStream.getChunkSource().updateManifest(manifest);
}
callback.onContinueLoadingRequested(this);
for (ChunkSampleStream<SsChunkSource> 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<SsChunkSource> sampleStream : sampleStreams) {
sampleStream.release();
}
}
@Override
@ -166,19 +166,6 @@ import java.util.ArrayList;
return positionUs;
}
@Override
public void releasePeriod() {
if (sampleStreams != null) {
for (ChunkSampleStream<SsChunkSource> sampleStream : sampleStreams) {
sampleStream.release();
}
sampleStreams = null;
}
sequenceableLoader = null;
callback = null;
allocator = null;
}
// SequenceableLoader.Callback implementation
@Override

View File

@ -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<SsMediaPeriod> 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);