Allow injection of custom ChunkSources

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=127737169
This commit is contained in:
olly 2016-07-18 11:17:09 -07:00 committed by Oliver Woodman
parent 5360ddc519
commit 74b43e26bd
10 changed files with 873 additions and 753 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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