mirror of
https://github.com/androidx/media.git
synced 2025-05-08 08:00:49 +08:00
Don't reuse MediaPeriods.
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=130266483
This commit is contained in:
parent
b5e41a903d
commit
b120bea029
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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--;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user