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.AdaptiveEvaluator;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSmoothStreamingChunkSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SmoothStreamingMediaSource;
|
||||
import com.google.android.exoplayer2.text.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
@ -131,7 +133,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
|
||||
private SubtitleLayout subtitleLayout;
|
||||
private Button retryButton;
|
||||
|
||||
private DataSource.Factory dataSourceFactory;
|
||||
private DataSource.Factory manifestDataSourceFactory;
|
||||
private DataSource.Factory mediaDataSourceFactory;
|
||||
private FormatEvaluator.Factory formatEvaluatorFactory;
|
||||
private SimpleExoPlayer player;
|
||||
private MappingTrackSelector trackSelector;
|
||||
@ -148,8 +151,9 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
String userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
|
||||
manifestDataSourceFactory = new DefaultDataSourceFactory(this, userAgent);
|
||||
BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||
dataSourceFactory = new DefaultDataSourceFactory(this, userAgent, bandwidthMeter);
|
||||
mediaDataSourceFactory = new DefaultDataSourceFactory(this, userAgent, bandwidthMeter);
|
||||
formatEvaluatorFactory = new AdaptiveEvaluator.Factory(bandwidthMeter);
|
||||
|
||||
mainHandler = new Handler();
|
||||
@ -343,16 +347,21 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback,
|
||||
int type = Util.inferContentType(lastPathSegment);
|
||||
switch (type) {
|
||||
case Util.TYPE_SS:
|
||||
return new SmoothStreamingMediaSource(uri, dataSourceFactory, formatEvaluatorFactory,
|
||||
mainHandler, eventLogger);
|
||||
DefaultSmoothStreamingChunkSource.Factory factory =
|
||||
new DefaultSmoothStreamingChunkSource.Factory(mediaDataSourceFactory,
|
||||
formatEvaluatorFactory);
|
||||
return new SmoothStreamingMediaSource(uri, manifestDataSourceFactory, factory, mainHandler,
|
||||
eventLogger);
|
||||
case Util.TYPE_DASH:
|
||||
return new DashMediaSource(uri, dataSourceFactory, formatEvaluatorFactory, mainHandler,
|
||||
DefaultDashChunkSource.Factory factory2 = new DefaultDashChunkSource.Factory(
|
||||
mediaDataSourceFactory, formatEvaluatorFactory);
|
||||
return new DashMediaSource(uri, mediaDataSourceFactory, factory2, mainHandler,
|
||||
eventLogger);
|
||||
case Util.TYPE_HLS:
|
||||
return new HlsMediaSource(uri, dataSourceFactory, formatEvaluatorFactory, mainHandler,
|
||||
return new HlsMediaSource(uri, mediaDataSourceFactory, formatEvaluatorFactory, mainHandler,
|
||||
eventLogger);
|
||||
case Util.TYPE_OTHER:
|
||||
return new ExtractorMediaSource(uri, dataSourceFactory, new DefaultExtractorsFactory(),
|
||||
return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(),
|
||||
mainHandler, eventLogger);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
|
@ -22,7 +22,7 @@ import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import android.test.InstrumentationTestCase;
|
||||
|
||||
/**
|
||||
* Tests {@link DashChunkSource}.
|
||||
* Tests {@link DefaultDashChunkSource}.
|
||||
*/
|
||||
public class DashChunkSourceTest extends InstrumentationTestCase {
|
||||
|
||||
|
@ -15,463 +15,24 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.source.dash;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.Format.DecreasingBandwidthComparator;
|
||||
import com.google.android.exoplayer2.extractor.ChunkIndex;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
|
||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.chunk.Chunk;
|
||||
import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper;
|
||||
import com.google.android.exoplayer2.source.chunk.ChunkHolder;
|
||||
import com.google.android.exoplayer2.source.chunk.ChunkSource;
|
||||
import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk;
|
||||
import com.google.android.exoplayer2.source.chunk.FormatEvaluator;
|
||||
import com.google.android.exoplayer2.source.chunk.FormatEvaluator.Evaluation;
|
||||
import com.google.android.exoplayer2.source.chunk.InitializationChunk;
|
||||
import com.google.android.exoplayer2.source.chunk.MediaChunk;
|
||||
import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk;
|
||||
import com.google.android.exoplayer2.source.dash.mpd.MediaPresentationDescription;
|
||||
import com.google.android.exoplayer2.source.dash.mpd.RangedUri;
|
||||
import com.google.android.exoplayer2.source.dash.mpd.Representation;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;
|
||||
import com.google.android.exoplayer2.upstream.Loader;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
import android.os.SystemClock;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An {@link ChunkSource} for DASH streams.
|
||||
*/
|
||||
public class DashChunkSource implements ChunkSource {
|
||||
public interface DashChunkSource extends ChunkSource {
|
||||
|
||||
private final Loader manifestLoader;
|
||||
private final int adaptationSetIndex;
|
||||
private final TrackGroup trackGroup;
|
||||
private final RepresentationHolder[] representationHolders;
|
||||
private final Format[] enabledFormats;
|
||||
private final boolean[] adaptiveFormatBlacklistFlags;
|
||||
private final DataSource dataSource;
|
||||
private final FormatEvaluator adaptiveFormatEvaluator;
|
||||
private final long elapsedRealtimeOffsetUs;
|
||||
private final Evaluation evaluation;
|
||||
interface Factory {
|
||||
|
||||
private MediaPresentationDescription manifest;
|
||||
|
||||
private boolean lastChunkWasInitialization;
|
||||
private IOException fatalError;
|
||||
private boolean missingLastSegment;
|
||||
|
||||
/**
|
||||
* @param manifestLoader The {@link Loader} being used to load manifests.
|
||||
* @param manifest The initial manifest.
|
||||
* @param periodIndex The index of the period in the manifest.
|
||||
* @param adaptationSetIndex The index of the adaptation set in the period.
|
||||
* @param trackGroup The track group corresponding to the adaptation set.
|
||||
* @param tracks The indices of the selected tracks within the adaptation set.
|
||||
* @param dataSource A {@link DataSource} suitable for loading the media data.
|
||||
* @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats.
|
||||
* @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between
|
||||
* server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified
|
||||
* as the server's unix time minus the local elapsed time. If unknown, set to 0.
|
||||
*/
|
||||
public DashChunkSource(Loader manifestLoader, MediaPresentationDescription manifest,
|
||||
int periodIndex, int adaptationSetIndex, TrackGroup trackGroup, int[] tracks,
|
||||
DataSource dataSource, FormatEvaluator adaptiveFormatEvaluator,
|
||||
long elapsedRealtimeOffsetMs) {
|
||||
this.manifestLoader = manifestLoader;
|
||||
this.manifest = manifest;
|
||||
this.adaptationSetIndex = adaptationSetIndex;
|
||||
this.trackGroup = trackGroup;
|
||||
this.dataSource = dataSource;
|
||||
this.adaptiveFormatEvaluator = adaptiveFormatEvaluator;
|
||||
this.elapsedRealtimeOffsetUs = elapsedRealtimeOffsetMs * 1000;
|
||||
this.evaluation = new Evaluation();
|
||||
|
||||
long periodDurationUs = getPeriodDurationUs(periodIndex);
|
||||
List<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);
|
||||
}
|
||||
DashChunkSource createDashChunkSource(Loader manifestLoader,
|
||||
MediaPresentationDescription manifest, int periodIndex, int adaptationSetIndex,
|
||||
TrackGroup trackGroup, int[] tracks, long elapsedRealtimeOffsetMs);
|
||||
|
||||
}
|
||||
|
||||
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.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.chunk.ChunkSampleStream;
|
||||
import com.google.android.exoplayer2.source.chunk.FormatEvaluator;
|
||||
import com.google.android.exoplayer2.source.dash.mpd.AdaptationSet;
|
||||
import com.google.android.exoplayer2.source.dash.mpd.MediaPresentationDescription;
|
||||
import com.google.android.exoplayer2.source.dash.mpd.Period;
|
||||
import com.google.android.exoplayer2.source.dash.mpd.Representation;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.Loader;
|
||||
|
||||
import android.util.Pair;
|
||||
@ -47,8 +45,7 @@ import java.util.List;
|
||||
/* package */ final class DashMediaPeriod implements MediaPeriod,
|
||||
SequenceableLoader.Callback<ChunkSampleStream<DashChunkSource>> {
|
||||
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
private final FormatEvaluator.Factory formatEvaluatorFactory;
|
||||
private final DashChunkSource.Factory chunkSourceFactory;
|
||||
private final int minLoadableRetryCount;
|
||||
private final EventDispatcher eventDispatcher;
|
||||
private final long elapsedRealtimeOffset;
|
||||
@ -66,13 +63,11 @@ import java.util.List;
|
||||
private Period period;
|
||||
|
||||
public DashMediaPeriod(MediaPresentationDescription manifest, int index,
|
||||
DataSource.Factory dataSourceFactory, FormatEvaluator.Factory formatEvaluatorFactory,
|
||||
int minLoadableRetryCount, EventDispatcher eventDispatcher, long elapsedRealtimeOffset,
|
||||
Loader loader) {
|
||||
DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount,
|
||||
EventDispatcher eventDispatcher, long elapsedRealtimeOffset, Loader loader) {
|
||||
this.manifest = manifest;
|
||||
this.index = index;
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.formatEvaluatorFactory = formatEvaluatorFactory;
|
||||
this.chunkSourceFactory = chunkSourceFactory;
|
||||
this.minLoadableRetryCount = minLoadableRetryCount;
|
||||
this.eventDispatcher = eventDispatcher;
|
||||
this.elapsedRealtimeOffset = elapsedRealtimeOffset;
|
||||
@ -242,16 +237,12 @@ import java.util.List;
|
||||
private ChunkSampleStream<DashChunkSource> buildSampleStream(TrackSelection selection,
|
||||
long positionUs) {
|
||||
int[] selectedTracks = selection.getTracks();
|
||||
FormatEvaluator adaptiveEvaluator = selectedTracks.length > 1
|
||||
? formatEvaluatorFactory.createFormatEvaluator() : null;
|
||||
int adaptationSetIndex = trackGroupAdaptationSetIndices[selection.group];
|
||||
AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex);
|
||||
int adaptationSetType = adaptationSet.type;
|
||||
DataSource dataSource = dataSourceFactory.createDataSource();
|
||||
DashChunkSource chunkSource = new DashChunkSource(loader, manifest, index, adaptationSetIndex,
|
||||
trackGroups.get(selection.group), selectedTracks, dataSource, adaptiveEvaluator,
|
||||
DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource(loader, manifest, index,
|
||||
adaptationSetIndex, trackGroups.get(selection.group), selectedTracks,
|
||||
elapsedRealtimeOffset);
|
||||
return new ChunkSampleStream<>(adaptationSetType, chunkSource, this, allocator, positionUs,
|
||||
return new ChunkSampleStream<>(adaptationSet.type, chunkSource, this, allocator, positionUs,
|
||||
minLoadableRetryCount, eventDispatcher);
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,6 @@ import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
|
||||
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.chunk.FormatEvaluator;
|
||||
import com.google.android.exoplayer2.source.dash.mpd.MediaPresentationDescription;
|
||||
import com.google.android.exoplayer2.source.dash.mpd.MediaPresentationDescriptionParser;
|
||||
import com.google.android.exoplayer2.source.dash.mpd.UtcTimingElement;
|
||||
@ -56,8 +55,8 @@ public final class DashMediaSource implements MediaSource {
|
||||
|
||||
private static final String TAG = "DashMediaSource";
|
||||
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
private final FormatEvaluator.Factory formatEvaluatorFactory;
|
||||
private final DataSource.Factory manifestDataSourceFactory;
|
||||
private final DashChunkSource.Factory chunkSourceFactory;
|
||||
private final int minLoadableRetryCount;
|
||||
private final EventDispatcher eventDispatcher;
|
||||
private final MediaPresentationDescriptionParser manifestParser;
|
||||
@ -74,19 +73,19 @@ public final class DashMediaSource implements MediaSource {
|
||||
private DashMediaPeriod[] periods;
|
||||
private long elapsedRealtimeOffset;
|
||||
|
||||
public DashMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory,
|
||||
FormatEvaluator.Factory formatEvaluatorFactory, Handler eventHandler,
|
||||
public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory,
|
||||
DashChunkSource.Factory chunkSourceFactory, Handler eventHandler,
|
||||
AdaptiveMediaSourceEventListener eventListener) {
|
||||
this(manifestUri, dataSourceFactory, formatEvaluatorFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT,
|
||||
eventHandler, eventListener);
|
||||
this(manifestUri, manifestDataSourceFactory, chunkSourceFactory,
|
||||
DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, eventListener);
|
||||
}
|
||||
|
||||
public DashMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory,
|
||||
FormatEvaluator.Factory formatEvaluatorFactory, int minLoadableRetryCount,
|
||||
public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory,
|
||||
DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount,
|
||||
Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) {
|
||||
this.manifestUri = manifestUri;
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.formatEvaluatorFactory = formatEvaluatorFactory;
|
||||
this.manifestDataSourceFactory = manifestDataSourceFactory;
|
||||
this.chunkSourceFactory = chunkSourceFactory;
|
||||
this.minLoadableRetryCount = minLoadableRetryCount;
|
||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||
manifestParser = new MediaPresentationDescriptionParser();
|
||||
@ -97,7 +96,7 @@ public final class DashMediaSource implements MediaSource {
|
||||
|
||||
@Override
|
||||
public void prepareSource() {
|
||||
dataSource = dataSourceFactory.createDataSource();
|
||||
dataSource = manifestDataSourceFactory.createDataSource();
|
||||
loader = new Loader("Loader:DashMediaSource");
|
||||
manifestRefreshHandler = new Handler();
|
||||
startLoadingManifest();
|
||||
@ -244,8 +243,8 @@ public final class DashMediaSource implements MediaSource {
|
||||
int periodCount = manifest.getPeriodCount();
|
||||
periods = new DashMediaPeriod[periodCount];
|
||||
for (int i = 0; i < periodCount; i++) {
|
||||
periods[i] = new DashMediaPeriod(manifest, i, dataSourceFactory, formatEvaluatorFactory,
|
||||
minLoadableRetryCount, eventDispatcher, elapsedRealtimeOffset, loader);
|
||||
periods[i] = new DashMediaPeriod(manifest, i, chunkSourceFactory, minLoadableRetryCount,
|
||||
eventDispatcher, elapsedRealtimeOffset, loader);
|
||||
}
|
||||
scheduleManifestRefresh();
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.Format.DecreasingBandwidthComparator;
|
||||
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
|
||||
import com.google.android.exoplayer2.extractor.mp4.Track;
|
||||
import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox;
|
||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.chunk.Chunk;
|
||||
import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper;
|
||||
import com.google.android.exoplayer2.source.chunk.ChunkHolder;
|
||||
import com.google.android.exoplayer2.source.chunk.ChunkSource;
|
||||
import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk;
|
||||
import com.google.android.exoplayer2.source.chunk.FormatEvaluator;
|
||||
import com.google.android.exoplayer2.source.chunk.FormatEvaluator.Evaluation;
|
||||
import com.google.android.exoplayer2.source.chunk.MediaChunk;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SmoothStreamingManifest.StreamElement;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.upstream.Loader;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* An {@link ChunkSource} for SmoothStreaming.
|
||||
* A {@link ChunkSource} for SmoothStreaming.
|
||||
*/
|
||||
public class SmoothStreamingChunkSource implements ChunkSource {
|
||||
public interface SmoothStreamingChunkSource extends ChunkSource {
|
||||
|
||||
private final Loader manifestLoader;
|
||||
private final int elementIndex;
|
||||
private final TrackGroup trackGroup;
|
||||
private final ChunkExtractorWrapper[] extractorWrappers;
|
||||
private final Format[] enabledFormats;
|
||||
private final boolean[] adaptiveFormatBlacklistFlags;
|
||||
private final DataSource dataSource;
|
||||
private final Evaluation evaluation;
|
||||
private final FormatEvaluator adaptiveFormatEvaluator;
|
||||
interface Factory {
|
||||
|
||||
private SmoothStreamingManifest manifest;
|
||||
private int currentManifestChunkOffset;
|
||||
SmoothStreamingChunkSource createChunkSource(Loader manifestLoader,
|
||||
SmoothStreamingManifest manifest, int elementIndex, TrackGroup trackGroup, int[] tracks,
|
||||
TrackEncryptionBox[] trackEncryptionBoxes);
|
||||
|
||||
private IOException fatalError;
|
||||
|
||||
/**
|
||||
* @param manifestLoader The {@link Loader} being used to load manifests.
|
||||
* @param manifest The initial manifest.
|
||||
* @param elementIndex The index of the stream element in the manifest.
|
||||
* @param trackGroup The track group corresponding to the stream element.
|
||||
* @param tracks The indices of the selected tracks within the stream element.
|
||||
* @param dataSource A {@link DataSource} suitable for loading the media data.
|
||||
* @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats.
|
||||
* @param trackEncryptionBoxes Track encryption boxes for the stream.
|
||||
*/
|
||||
public SmoothStreamingChunkSource(Loader manifestLoader, SmoothStreamingManifest manifest,
|
||||
int elementIndex, TrackGroup trackGroup, int[] tracks, DataSource dataSource,
|
||||
FormatEvaluator adaptiveFormatEvaluator, TrackEncryptionBox[] trackEncryptionBoxes) {
|
||||
this.manifestLoader = manifestLoader;
|
||||
this.manifest = manifest;
|
||||
this.elementIndex = elementIndex;
|
||||
this.trackGroup = trackGroup;
|
||||
this.dataSource = dataSource;
|
||||
this.adaptiveFormatEvaluator = adaptiveFormatEvaluator;
|
||||
this.evaluation = new Evaluation();
|
||||
|
||||
StreamElement streamElement = manifest.streamElements[elementIndex];
|
||||
Format[] formats = streamElement.formats;
|
||||
extractorWrappers = new ChunkExtractorWrapper[formats.length];
|
||||
for (int j = 0; j < formats.length; j++) {
|
||||
int nalUnitLengthFieldLength = streamElement.type == C.TRACK_TYPE_VIDEO ? 4 : -1;
|
||||
Track track = new Track(j, streamElement.type, streamElement.timescale, C.UNSET_TIME_US,
|
||||
manifest.durationUs, formats[j], Track.TRANSFORMATION_NONE, trackEncryptionBoxes,
|
||||
nalUnitLengthFieldLength, null, null);
|
||||
FragmentedMp4Extractor extractor = new FragmentedMp4Extractor(
|
||||
FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME
|
||||
| FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, track);
|
||||
extractorWrappers[j] = new ChunkExtractorWrapper(extractor, formats[j], false);
|
||||
}
|
||||
|
||||
enabledFormats = new Format[tracks.length];
|
||||
for (int i = 0; i < tracks.length; i++) {
|
||||
enabledFormats[i] = trackGroup.getFormat(tracks[i]);
|
||||
}
|
||||
Arrays.sort(enabledFormats, new DecreasingBandwidthComparator());
|
||||
if (adaptiveFormatEvaluator != null) {
|
||||
adaptiveFormatEvaluator.enable(enabledFormats);
|
||||
adaptiveFormatBlacklistFlags = new boolean[tracks.length];
|
||||
} else {
|
||||
adaptiveFormatBlacklistFlags = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void updateManifest(SmoothStreamingManifest newManifest) {
|
||||
StreamElement currentElement = manifest.streamElements[elementIndex];
|
||||
int currentElementChunkCount = currentElement.chunkCount;
|
||||
StreamElement newElement = newManifest.streamElements[elementIndex];
|
||||
if (currentElementChunkCount == 0 || newElement.chunkCount == 0) {
|
||||
// There's no overlap between the old and new elements because at least one is empty.
|
||||
currentManifestChunkOffset += currentElementChunkCount;
|
||||
} else {
|
||||
long currentElementEndTimeUs = currentElement.getStartTimeUs(currentElementChunkCount - 1)
|
||||
+ currentElement.getChunkDurationUs(currentElementChunkCount - 1);
|
||||
long newElementStartTimeUs = newElement.getStartTimeUs(0);
|
||||
if (currentElementEndTimeUs <= newElementStartTimeUs) {
|
||||
// There's no overlap between the old and new elements.
|
||||
currentManifestChunkOffset += currentElementChunkCount;
|
||||
} else {
|
||||
// The new element overlaps with the old one.
|
||||
currentManifestChunkOffset += currentElement.getChunkIndex(newElementStartTimeUs);
|
||||
}
|
||||
}
|
||||
manifest = newManifest;
|
||||
}
|
||||
|
||||
// ChunkSource implementation.
|
||||
|
||||
@Override
|
||||
public void maybeThrowError() throws IOException {
|
||||
if (fatalError != null) {
|
||||
throw fatalError;
|
||||
} else {
|
||||
manifestLoader.maybeThrowError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreferredQueueSize(long playbackPositionUs, List<? 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);
|
||||
}
|
||||
void updateManifest(SmoothStreamingManifest newManifest);
|
||||
|
||||
}
|
||||
|
@ -29,7 +29,6 @@ import com.google.android.exoplayer2.source.SequenceableLoader;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.chunk.ChunkSampleStream;
|
||||
import com.google.android.exoplayer2.source.chunk.FormatEvaluator;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SmoothStreamingManifest.ProtectionElement;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SmoothStreamingManifest.StreamElement;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
@ -66,7 +65,7 @@ public final class SmoothStreamingMediaSource implements MediaPeriod, MediaSourc
|
||||
|
||||
private final Uri manifestUri;
|
||||
private final DataSource.Factory dataSourceFactory;
|
||||
private final FormatEvaluator.Factory formatEvaluatorFactory;
|
||||
private final SmoothStreamingChunkSource.Factory chunkSourceFactory;
|
||||
private final int minLoadableRetryCount;
|
||||
private final EventDispatcher eventDispatcher;
|
||||
private final SmoothStreamingManifestParser manifestParser;
|
||||
@ -88,20 +87,20 @@ public final class SmoothStreamingMediaSource implements MediaPeriod, MediaSourc
|
||||
private TrackGroupArray trackGroups;
|
||||
private int[] trackGroupElementIndices;
|
||||
|
||||
public SmoothStreamingMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory,
|
||||
FormatEvaluator.Factory formatEvaluatorFactory, Handler eventHandler,
|
||||
public SmoothStreamingMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory,
|
||||
SmoothStreamingChunkSource.Factory chunkSourceFactory, Handler eventHandler,
|
||||
AdaptiveMediaSourceEventListener eventListener) {
|
||||
this(manifestUri, dataSourceFactory, formatEvaluatorFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT,
|
||||
eventHandler, eventListener);
|
||||
this(manifestUri, manifestDataSourceFactory, chunkSourceFactory,
|
||||
DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, eventListener);
|
||||
}
|
||||
|
||||
public SmoothStreamingMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory,
|
||||
FormatEvaluator.Factory formatEvaluatorFactory, int minLoadableRetryCount,
|
||||
SmoothStreamingChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount,
|
||||
Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) {
|
||||
this.manifestUri = Util.toLowerInvariant(manifestUri.getLastPathSegment()).equals("manifest")
|
||||
? manifestUri : Uri.withAppendedPath(manifestUri, "Manifest");
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.formatEvaluatorFactory = formatEvaluatorFactory;
|
||||
this.chunkSourceFactory = chunkSourceFactory;
|
||||
this.minLoadableRetryCount = minLoadableRetryCount;
|
||||
this.eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||
manifestParser = new SmoothStreamingManifestParser();
|
||||
@ -354,17 +353,12 @@ public final class SmoothStreamingMediaSource implements MediaPeriod, MediaSourc
|
||||
private ChunkSampleStream<SmoothStreamingChunkSource> buildSampleStream(TrackSelection selection,
|
||||
long positionUs) {
|
||||
int[] selectedTracks = selection.getTracks();
|
||||
FormatEvaluator adaptiveEvaluator = selectedTracks.length > 1
|
||||
? formatEvaluatorFactory.createFormatEvaluator() : null;
|
||||
int streamElementIndex = trackGroupElementIndices[selection.group];
|
||||
StreamElement streamElement = manifest.streamElements[streamElementIndex];
|
||||
int streamElementType = streamElement.type;
|
||||
DataSource dataSource = dataSourceFactory.createDataSource();
|
||||
SmoothStreamingChunkSource chunkSource = new SmoothStreamingChunkSource(manifestLoader,
|
||||
manifest, streamElementIndex, trackGroups.get(selection.group), selectedTracks, dataSource,
|
||||
adaptiveEvaluator, trackEncryptionBoxes);
|
||||
return new ChunkSampleStream<>(streamElementType, chunkSource, this, allocator, positionUs,
|
||||
minLoadableRetryCount, eventDispatcher);
|
||||
SmoothStreamingChunkSource chunkSource = chunkSourceFactory.createChunkSource(manifestLoader,
|
||||
manifest, streamElementIndex, trackGroups.get(selection.group), selectedTracks,
|
||||
trackEncryptionBoxes);
|
||||
return new ChunkSampleStream<>(manifest.streamElements[streamElementIndex].type, chunkSource,
|
||||
this, allocator, positionUs, minLoadableRetryCount, eventDispatcher);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
@ -34,6 +34,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.source.chunk.FormatEvaluator;
|
||||
import com.google.android.exoplayer2.source.chunk.FormatEvaluator.AdaptiveEvaluator;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
||||
@ -423,12 +424,15 @@ public final class DashTest extends ActivityInstrumentationTestCase2<HostActivit
|
||||
|
||||
@Override
|
||||
public MediaSource buildSource(HostActivity host, String userAgent) {
|
||||
DataSource.Factory manifestDataSourceFactory = new DefaultDataSourceFactory(host, userAgent);
|
||||
BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(host, userAgent,
|
||||
DataSource.Factory mediaDataSourceFactory = new DefaultDataSourceFactory(host, userAgent,
|
||||
bandwidthMeter);
|
||||
FormatEvaluator.Factory formatEvaluatorFactory = new AdaptiveEvaluator.Factory(
|
||||
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);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user