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