Define DashSegmentIndex wrapper.
This paves the way for SegmentTemplate and SegmentList based mpds, which will implement DashSegmentIndex directly rather than parsing an index from the media stream. - Define DashSegmentIndex. - Make use of DashSegmentIndex in chunk sources. - Define an implementation of DashSegmentIndex that wraps a SegmentIndex. - Add method that will allow Representations to return a DashSegmentIndex directly in the future. - Add support for non-contiguous index and initialization data in media streams. For the Webm case this isn't enabled yet due to extractor limitations. - Removed ability to fetch multiple chunks. This functionality does not extend properly to SegmentList and SegmentTemplate variants of DASH.
This commit is contained in:
parent
d7d14037b8
commit
87461821fe
@ -129,6 +129,10 @@ package com.google.android.exoplayer.demo;
|
|||||||
+ "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
|
+ "as=fmp4_audio_cenc,fmp4_sd_hd_cenc&sparams=ip,ipbits,expire,as&ip=0.0.0.0&ipbits=0"
|
||||||
+ "&expire=19000000000&signature=88DC53943385CED8CF9F37ADD9E9843E3BF621E6."
|
+ "&expire=19000000000&signature=88DC53943385CED8CF9F37ADD9E9843E3BF621E6."
|
||||||
+ "22727BB612D24AA4FACE4EF62726F9461A9BF57A&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true),
|
+ "22727BB612D24AA4FACE4EF62726F9461A9BF57A&key=ik0", DemoUtil.TYPE_DASH_VOD, true, true),
|
||||||
|
new Sample("WV: 30s license duration", "f9a34cab7b05881a",
|
||||||
|
"http://dash.edgesuite.net/digitalprimates/fraunhofer/480p_video/heaac_2_0_with_video/ElephantsDream/elephants_dream_480p_heaac2_0.mpd", DemoUtil.TYPE_DASH_VOD, false, true),
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static final Sample[] MISC = new Sample[] {
|
public static final Sample[] MISC = new Sample[] {
|
||||||
|
@ -100,6 +100,11 @@ public class Format {
|
|||||||
this.bandwidth = bitrate / 8;
|
this.bandwidth = bitrate / 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return id.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements equality based on {@link #id} only.
|
* Implements equality based on {@link #id} only.
|
||||||
*/
|
*/
|
||||||
|
@ -27,15 +27,14 @@ import com.google.android.exoplayer.chunk.FormatEvaluator;
|
|||||||
import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
|
import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
|
||||||
import com.google.android.exoplayer.chunk.MediaChunk;
|
import com.google.android.exoplayer.chunk.MediaChunk;
|
||||||
import com.google.android.exoplayer.chunk.Mp4MediaChunk;
|
import com.google.android.exoplayer.chunk.Mp4MediaChunk;
|
||||||
|
import com.google.android.exoplayer.dash.mpd.RangedUri;
|
||||||
import com.google.android.exoplayer.dash.mpd.Representation;
|
import com.google.android.exoplayer.dash.mpd.Representation;
|
||||||
import com.google.android.exoplayer.parser.SegmentIndex;
|
|
||||||
import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
|
import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor;
|
||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
import com.google.android.exoplayer.upstream.DataSpec;
|
import com.google.android.exoplayer.upstream.DataSpec;
|
||||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||||
import com.google.android.exoplayer.util.Util;
|
|
||||||
|
|
||||||
import android.util.Log;
|
import android.net.Uri;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -47,26 +46,17 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public class DashMp4ChunkSource implements ChunkSource {
|
public class DashMp4ChunkSource implements ChunkSource {
|
||||||
|
|
||||||
public static final int DEFAULT_NUM_SEGMENTS_PER_CHUNK = 1;
|
|
||||||
|
|
||||||
private static final int EXPECTED_INITIALIZATION_RESULT =
|
|
||||||
FragmentedMp4Extractor.RESULT_END_OF_STREAM
|
|
||||||
| FragmentedMp4Extractor.RESULT_READ_MOOV
|
|
||||||
| FragmentedMp4Extractor.RESULT_READ_SIDX;
|
|
||||||
|
|
||||||
private static final String TAG = "DashMp4ChunkSource";
|
|
||||||
|
|
||||||
private final TrackInfo trackInfo;
|
private final TrackInfo trackInfo;
|
||||||
private final DataSource dataSource;
|
private final DataSource dataSource;
|
||||||
private final FormatEvaluator evaluator;
|
private final FormatEvaluator evaluator;
|
||||||
private final Evaluation evaluation;
|
private final Evaluation evaluation;
|
||||||
private final int maxWidth;
|
private final int maxWidth;
|
||||||
private final int maxHeight;
|
private final int maxHeight;
|
||||||
private final int numSegmentsPerChunk;
|
|
||||||
|
|
||||||
private final Format[] formats;
|
private final Format[] formats;
|
||||||
private final HashMap<String, Representation> representations;
|
private final HashMap<String, Representation> representations;
|
||||||
private final HashMap<String, FragmentedMp4Extractor> extractors;
|
private final HashMap<String, FragmentedMp4Extractor> extractors;
|
||||||
|
private final HashMap<String, DashSegmentIndex> segmentIndexes;
|
||||||
|
|
||||||
private boolean lastChunkWasInitialization;
|
private boolean lastChunkWasInitialization;
|
||||||
|
|
||||||
@ -77,23 +67,11 @@ public class DashMp4ChunkSource implements ChunkSource {
|
|||||||
*/
|
*/
|
||||||
public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator,
|
public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator,
|
||||||
Representation... representations) {
|
Representation... representations) {
|
||||||
this(dataSource, evaluator, DEFAULT_NUM_SEGMENTS_PER_CHUNK, representations);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param dataSource A {@link DataSource} suitable for loading the media data.
|
|
||||||
* @param evaluator Selects from the available formats.
|
|
||||||
* @param numSegmentsPerChunk The number of segments (as defined in the stream's segment index)
|
|
||||||
* that should be grouped into a single chunk.
|
|
||||||
* @param representations The representations to be considered by the source.
|
|
||||||
*/
|
|
||||||
public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator,
|
|
||||||
int numSegmentsPerChunk, Representation... representations) {
|
|
||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
this.evaluator = evaluator;
|
this.evaluator = evaluator;
|
||||||
this.numSegmentsPerChunk = numSegmentsPerChunk;
|
|
||||||
this.formats = new Format[representations.length];
|
this.formats = new Format[representations.length];
|
||||||
this.extractors = new HashMap<String, FragmentedMp4Extractor>();
|
this.extractors = new HashMap<String, FragmentedMp4Extractor>();
|
||||||
|
this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
|
||||||
this.representations = new HashMap<String, Representation>();
|
this.representations = new HashMap<String, Representation>();
|
||||||
this.trackInfo = new TrackInfo(representations[0].format.mimeType,
|
this.trackInfo = new TrackInfo(representations[0].format.mimeType,
|
||||||
representations[0].periodDuration * 1000);
|
representations[0].periodDuration * 1000);
|
||||||
@ -106,6 +84,10 @@ public class DashMp4ChunkSource implements ChunkSource {
|
|||||||
maxHeight = Math.max(formats[i].height, maxHeight);
|
maxHeight = Math.max(formats[i].height, maxHeight);
|
||||||
extractors.put(formats[i].id, new FragmentedMp4Extractor());
|
extractors.put(formats[i].id, new FragmentedMp4Extractor());
|
||||||
this.representations.put(formats[i].id, representations[i]);
|
this.representations.put(formats[i].id, representations[i]);
|
||||||
|
DashSegmentIndex segmentIndex = representations[i].getIndex();
|
||||||
|
if (segmentIndex != null) {
|
||||||
|
segmentIndexes.put(formats[i].id, segmentIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.maxWidth = maxWidth;
|
this.maxWidth = maxWidth;
|
||||||
this.maxHeight = maxHeight;
|
this.maxHeight = maxHeight;
|
||||||
@ -161,29 +143,39 @@ public class DashMp4ChunkSource implements ChunkSource {
|
|||||||
|
|
||||||
Representation selectedRepresentation = representations.get(selectedFormat.id);
|
Representation selectedRepresentation = representations.get(selectedFormat.id);
|
||||||
FragmentedMp4Extractor extractor = extractors.get(selectedRepresentation.format.id);
|
FragmentedMp4Extractor extractor = extractors.get(selectedRepresentation.format.id);
|
||||||
|
|
||||||
|
RangedUri pendingInitializationUri = null;
|
||||||
|
RangedUri pendingIndexUri = null;
|
||||||
if (extractor.getTrack() == null) {
|
if (extractor.getTrack() == null) {
|
||||||
Chunk initializationChunk = newInitializationChunk(selectedRepresentation, extractor,
|
pendingInitializationUri = selectedRepresentation.getInitializationUri();
|
||||||
dataSource, evaluation.trigger);
|
}
|
||||||
|
if (!segmentIndexes.containsKey(selectedRepresentation.format.id)) {
|
||||||
|
pendingIndexUri = selectedRepresentation.getIndexUri();
|
||||||
|
}
|
||||||
|
if (pendingInitializationUri != null || pendingIndexUri != null) {
|
||||||
|
// We have initialization and/or index requests to make.
|
||||||
|
Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri,
|
||||||
|
selectedRepresentation, extractor, dataSource, evaluation.trigger);
|
||||||
lastChunkWasInitialization = true;
|
lastChunkWasInitialization = true;
|
||||||
out.chunk = initializationChunk;
|
out.chunk = initializationChunk;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int nextIndex;
|
int nextSegmentNum;
|
||||||
|
DashSegmentIndex segmentIndex = segmentIndexes.get(selectedRepresentation.format.id);
|
||||||
if (queue.isEmpty()) {
|
if (queue.isEmpty()) {
|
||||||
nextIndex = Util.binarySearchFloor(extractor.getSegmentIndex().timesUs, seekPositionUs,
|
nextSegmentNum = segmentIndex.getSegmentNum(seekPositionUs);
|
||||||
true, true);
|
|
||||||
} else {
|
} else {
|
||||||
nextIndex = queue.get(out.queueSize - 1).nextChunkIndex;
|
nextSegmentNum = queue.get(out.queueSize - 1).nextChunkIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextIndex == -1) {
|
if (nextSegmentNum == -1) {
|
||||||
out.chunk = null;
|
out.chunk = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, extractor, dataSource,
|
Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, segmentIndex, extractor,
|
||||||
extractor.getSegmentIndex(), nextIndex, evaluation.trigger, numSegmentsPerChunk);
|
dataSource, nextSegmentNum, evaluation.trigger);
|
||||||
lastChunkWasInitialization = false;
|
lastChunkWasInitialization = false;
|
||||||
out.chunk = nextMediaChunk;
|
out.chunk = nextMediaChunk;
|
||||||
}
|
}
|
||||||
@ -198,75 +190,75 @@ public class DashMp4ChunkSource implements ChunkSource {
|
|||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Chunk newInitializationChunk(Representation representation,
|
private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri,
|
||||||
FragmentedMp4Extractor extractor, DataSource dataSource, int trigger) {
|
Representation representation, FragmentedMp4Extractor extractor, DataSource dataSource,
|
||||||
DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1,
|
int trigger) {
|
||||||
|
int expectedExtractorResult = FragmentedMp4Extractor.RESULT_END_OF_STREAM;
|
||||||
|
long indexAnchor = 0;
|
||||||
|
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 at once.
|
||||||
|
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_MOOV;
|
||||||
|
requestUri = initializationUri.attemptMerge(indexUri);
|
||||||
|
if (requestUri != null) {
|
||||||
|
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_SIDX;
|
||||||
|
indexAnchor = indexUri.start + indexUri.length;
|
||||||
|
} else {
|
||||||
|
requestUri = initializationUri;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
requestUri = indexUri;
|
||||||
|
indexAnchor = indexUri.start + indexUri.length;
|
||||||
|
expectedExtractorResult |= FragmentedMp4Extractor.RESULT_READ_SIDX;
|
||||||
|
}
|
||||||
|
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
|
||||||
representation.getCacheKey());
|
representation.getCacheKey());
|
||||||
return new InitializationMp4Loadable(dataSource, dataSpec, trigger, extractor, representation);
|
return new InitializationMp4Loadable(dataSource, dataSpec, trigger, representation.format,
|
||||||
|
extractor, expectedExtractorResult, indexAnchor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Chunk newMediaChunk(Representation representation,
|
private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex,
|
||||||
FragmentedMp4Extractor extractor, DataSource dataSource, SegmentIndex sidx, int index,
|
FragmentedMp4Extractor extractor, DataSource dataSource, int segmentNum, int trigger) {
|
||||||
int trigger, int numSegmentsPerChunk) {
|
int lastSegmentNum = segmentIndex.getLastSegmentNum();
|
||||||
|
int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1;
|
||||||
// Computes the segments to included in the next fetch.
|
long startTimeUs = segmentIndex.getTimeUs(segmentNum);
|
||||||
int numSegmentsToFetch = Math.min(numSegmentsPerChunk, sidx.length - index);
|
long endTimeUs = segmentNum < lastSegmentNum ? segmentIndex.getTimeUs(segmentNum + 1)
|
||||||
int lastSegmentInChunk = index + numSegmentsToFetch - 1;
|
: startTimeUs + segmentIndex.getDurationUs(segmentNum);
|
||||||
int nextIndex = lastSegmentInChunk == sidx.length - 1 ? -1 : lastSegmentInChunk + 1;
|
RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum);
|
||||||
|
DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length,
|
||||||
long startTimeUs = sidx.timesUs[index];
|
|
||||||
|
|
||||||
// Compute the end time, prefer to use next segment start time if there is a next segment.
|
|
||||||
long endTimeUs = nextIndex == -1 ?
|
|
||||||
sidx.timesUs[lastSegmentInChunk] + sidx.durationsUs[lastSegmentInChunk] :
|
|
||||||
sidx.timesUs[nextIndex];
|
|
||||||
|
|
||||||
long offset = (int) representation.indexEnd + 1 + sidx.offsets[index];
|
|
||||||
|
|
||||||
// Compute combined segments byte length.
|
|
||||||
long size = 0;
|
|
||||||
for (int i = index; i <= lastSegmentInChunk; i++) {
|
|
||||||
size += sidx.sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
DataSpec dataSpec = new DataSpec(representation.uri, offset, size,
|
|
||||||
representation.getCacheKey());
|
representation.getCacheKey());
|
||||||
return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs,
|
return new Mp4MediaChunk(dataSource, dataSpec, representation.format, trigger, startTimeUs,
|
||||||
endTimeUs, nextIndex, extractor, false, 0);
|
endTimeUs, nextSegmentNum, extractor, false, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class InitializationMp4Loadable extends Chunk {
|
private class InitializationMp4Loadable extends Chunk {
|
||||||
|
|
||||||
private final Representation representation;
|
|
||||||
private final FragmentedMp4Extractor extractor;
|
private final FragmentedMp4Extractor extractor;
|
||||||
|
private final int expectedExtractorResult;
|
||||||
|
private final long indexAnchor;
|
||||||
|
private final Uri uri;
|
||||||
|
|
||||||
public InitializationMp4Loadable(DataSource dataSource, DataSpec dataSpec, int trigger,
|
public InitializationMp4Loadable(DataSource dataSource, DataSpec dataSpec, int trigger,
|
||||||
FragmentedMp4Extractor extractor, Representation representation) {
|
Format format, FragmentedMp4Extractor extractor, int expectedExtractorResult,
|
||||||
super(dataSource, dataSpec, representation.format, trigger);
|
long indexAnchor) {
|
||||||
|
super(dataSource, dataSpec, format, trigger);
|
||||||
this.extractor = extractor;
|
this.extractor = extractor;
|
||||||
this.representation = representation;
|
this.expectedExtractorResult = expectedExtractorResult;
|
||||||
|
this.indexAnchor = indexAnchor;
|
||||||
|
this.uri = dataSpec.uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void consumeStream(NonBlockingInputStream stream) throws IOException {
|
protected void consumeStream(NonBlockingInputStream stream) throws IOException {
|
||||||
int result = extractor.read(stream, null);
|
int result = extractor.read(stream, null);
|
||||||
if (result != EXPECTED_INITIALIZATION_RESULT) {
|
if (result != expectedExtractorResult) {
|
||||||
throw new ParserException("Invalid initialization data");
|
throw new ParserException("Invalid extractor result. Expected "
|
||||||
|
+ expectedExtractorResult + ", got " + result);
|
||||||
}
|
}
|
||||||
validateSegmentIndex(extractor.getSegmentIndex());
|
if ((result & FragmentedMp4Extractor.RESULT_READ_SIDX) != 0) {
|
||||||
}
|
segmentIndexes.put(format.id,
|
||||||
|
new DashWrappingSegmentIndex(extractor.getSegmentIndex(), uri, indexAnchor));
|
||||||
private void validateSegmentIndex(SegmentIndex segmentIndex) {
|
|
||||||
long expectedIndexLen = representation.indexEnd - representation.indexStart + 1;
|
|
||||||
if (segmentIndex.sizeBytes != expectedIndexLen) {
|
|
||||||
Log.w(TAG, "Sidx length mismatch: sidxLen = " + segmentIndex.sizeBytes +
|
|
||||||
", ExpectedLen = " + expectedIndexLen);
|
|
||||||
}
|
|
||||||
long sidxContentLength = segmentIndex.offsets[segmentIndex.length - 1] +
|
|
||||||
segmentIndex.sizes[segmentIndex.length - 1] + representation.indexEnd + 1;
|
|
||||||
if (sidxContentLength != representation.contentLength) {
|
|
||||||
Log.w(TAG, "ContentLength mismatch: Actual = " + sidxContentLength +
|
|
||||||
", Expected = " + representation.contentLength);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,16 +27,15 @@ import com.google.android.exoplayer.chunk.FormatEvaluator;
|
|||||||
import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
|
import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
|
||||||
import com.google.android.exoplayer.chunk.MediaChunk;
|
import com.google.android.exoplayer.chunk.MediaChunk;
|
||||||
import com.google.android.exoplayer.chunk.WebmMediaChunk;
|
import com.google.android.exoplayer.chunk.WebmMediaChunk;
|
||||||
|
import com.google.android.exoplayer.dash.mpd.RangedUri;
|
||||||
import com.google.android.exoplayer.dash.mpd.Representation;
|
import com.google.android.exoplayer.dash.mpd.Representation;
|
||||||
import com.google.android.exoplayer.parser.SegmentIndex;
|
|
||||||
import com.google.android.exoplayer.parser.webm.DefaultWebmExtractor;
|
import com.google.android.exoplayer.parser.webm.DefaultWebmExtractor;
|
||||||
import com.google.android.exoplayer.parser.webm.WebmExtractor;
|
import com.google.android.exoplayer.parser.webm.WebmExtractor;
|
||||||
import com.google.android.exoplayer.upstream.DataSource;
|
import com.google.android.exoplayer.upstream.DataSource;
|
||||||
import com.google.android.exoplayer.upstream.DataSpec;
|
import com.google.android.exoplayer.upstream.DataSpec;
|
||||||
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
import com.google.android.exoplayer.upstream.NonBlockingInputStream;
|
||||||
import com.google.android.exoplayer.util.Util;
|
|
||||||
|
|
||||||
import android.util.Log;
|
import android.net.Uri;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -48,34 +47,27 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public class DashWebmChunkSource implements ChunkSource {
|
public class DashWebmChunkSource implements ChunkSource {
|
||||||
|
|
||||||
private static final String TAG = "DashWebmChunkSource";
|
|
||||||
|
|
||||||
private final TrackInfo trackInfo;
|
private final TrackInfo trackInfo;
|
||||||
private final DataSource dataSource;
|
private final DataSource dataSource;
|
||||||
private final FormatEvaluator evaluator;
|
private final FormatEvaluator evaluator;
|
||||||
private final Evaluation evaluation;
|
private final Evaluation evaluation;
|
||||||
private final int maxWidth;
|
private final int maxWidth;
|
||||||
private final int maxHeight;
|
private final int maxHeight;
|
||||||
private final int numSegmentsPerChunk;
|
|
||||||
|
|
||||||
private final Format[] formats;
|
private final Format[] formats;
|
||||||
private final HashMap<String, Representation> representations;
|
private final HashMap<String, Representation> representations;
|
||||||
private final HashMap<String, WebmExtractor> extractors;
|
private final HashMap<String, WebmExtractor> extractors;
|
||||||
|
private final HashMap<String, DashSegmentIndex> segmentIndexes;
|
||||||
|
|
||||||
private boolean lastChunkWasInitialization;
|
private boolean lastChunkWasInitialization;
|
||||||
|
|
||||||
public DashWebmChunkSource(
|
|
||||||
DataSource dataSource, FormatEvaluator evaluator, Representation... representations) {
|
|
||||||
this(dataSource, evaluator, 1, representations);
|
|
||||||
}
|
|
||||||
|
|
||||||
public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator,
|
public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator,
|
||||||
int numSegmentsPerChunk, Representation... representations) {
|
Representation... representations) {
|
||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
this.evaluator = evaluator;
|
this.evaluator = evaluator;
|
||||||
this.numSegmentsPerChunk = numSegmentsPerChunk;
|
|
||||||
this.formats = new Format[representations.length];
|
this.formats = new Format[representations.length];
|
||||||
this.extractors = new HashMap<String, WebmExtractor>();
|
this.extractors = new HashMap<String, WebmExtractor>();
|
||||||
|
this.segmentIndexes = new HashMap<String, DashSegmentIndex>();
|
||||||
this.representations = new HashMap<String, Representation>();
|
this.representations = new HashMap<String, Representation>();
|
||||||
this.trackInfo = new TrackInfo(
|
this.trackInfo = new TrackInfo(
|
||||||
representations[0].format.mimeType, representations[0].periodDuration * 1000);
|
representations[0].format.mimeType, representations[0].periodDuration * 1000);
|
||||||
@ -88,6 +80,10 @@ public class DashWebmChunkSource implements ChunkSource {
|
|||||||
maxHeight = Math.max(formats[i].height, maxHeight);
|
maxHeight = Math.max(formats[i].height, maxHeight);
|
||||||
extractors.put(formats[i].id, new DefaultWebmExtractor());
|
extractors.put(formats[i].id, new DefaultWebmExtractor());
|
||||||
this.representations.put(formats[i].id, representations[i]);
|
this.representations.put(formats[i].id, representations[i]);
|
||||||
|
DashSegmentIndex segmentIndex = representations[i].getIndex();
|
||||||
|
if (segmentIndex != null) {
|
||||||
|
segmentIndexes.put(formats[i].id, segmentIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.maxWidth = maxWidth;
|
this.maxWidth = maxWidth;
|
||||||
this.maxHeight = maxHeight;
|
this.maxHeight = maxHeight;
|
||||||
@ -143,28 +139,34 @@ public class DashWebmChunkSource implements ChunkSource {
|
|||||||
|
|
||||||
Representation selectedRepresentation = representations.get(selectedFormat.id);
|
Representation selectedRepresentation = representations.get(selectedFormat.id);
|
||||||
WebmExtractor extractor = extractors.get(selectedRepresentation.format.id);
|
WebmExtractor extractor = extractors.get(selectedRepresentation.format.id);
|
||||||
|
|
||||||
if (!extractor.isPrepared()) {
|
if (!extractor.isPrepared()) {
|
||||||
Chunk initializationChunk = newInitializationChunk(selectedRepresentation, extractor,
|
// TODO: This code forces cues to exist and to immediately follow the initialization
|
||||||
dataSource, evaluation.trigger);
|
// data. Webm extractor should be generalized to allow cues to be optional. See [redacted].
|
||||||
|
RangedUri initializationUri = selectedRepresentation.getInitializationUri().attemptMerge(
|
||||||
|
selectedRepresentation.getIndexUri());
|
||||||
|
Chunk initializationChunk = newInitializationChunk(initializationUri, selectedRepresentation,
|
||||||
|
extractor, dataSource, evaluation.trigger);
|
||||||
lastChunkWasInitialization = true;
|
lastChunkWasInitialization = true;
|
||||||
out.chunk = initializationChunk;
|
out.chunk = initializationChunk;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int nextIndex;
|
int nextSegmentNum;
|
||||||
|
DashSegmentIndex segmentIndex = segmentIndexes.get(selectedRepresentation.format.id);
|
||||||
if (queue.isEmpty()) {
|
if (queue.isEmpty()) {
|
||||||
nextIndex = Util.binarySearchFloor(extractor.getCues().timesUs, seekPositionUs, true, true);
|
nextSegmentNum = segmentIndex.getSegmentNum(seekPositionUs);
|
||||||
} else {
|
} else {
|
||||||
nextIndex = queue.get(out.queueSize - 1).nextChunkIndex;
|
nextSegmentNum = queue.get(out.queueSize - 1).nextChunkIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextIndex == -1) {
|
if (nextSegmentNum == -1) {
|
||||||
out.chunk = null;
|
out.chunk = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, extractor, dataSource,
|
Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, segmentIndex, extractor,
|
||||||
extractor.getCues(), nextIndex, evaluation.trigger, numSegmentsPerChunk);
|
dataSource, nextSegmentNum, evaluation.trigger);
|
||||||
lastChunkWasInitialization = false;
|
lastChunkWasInitialization = false;
|
||||||
out.chunk = nextMediaChunk;
|
out.chunk = nextMediaChunk;
|
||||||
}
|
}
|
||||||
@ -179,53 +181,38 @@ public class DashWebmChunkSource implements ChunkSource {
|
|||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Chunk newInitializationChunk(Representation representation,
|
private Chunk newInitializationChunk(RangedUri initializationUri, Representation representation,
|
||||||
WebmExtractor extractor, DataSource dataSource, int trigger) {
|
WebmExtractor extractor, DataSource dataSource, int trigger) {
|
||||||
DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1,
|
DataSpec dataSpec = new DataSpec(initializationUri.getUri(), initializationUri.start,
|
||||||
representation.getCacheKey());
|
initializationUri.length, representation.getCacheKey());
|
||||||
return new InitializationWebmLoadable(dataSource, dataSpec, trigger, extractor, representation);
|
return new InitializationWebmLoadable(dataSource, dataSpec, trigger, representation.format,
|
||||||
|
extractor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Chunk newMediaChunk(Representation representation,
|
private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex,
|
||||||
WebmExtractor extractor, DataSource dataSource, SegmentIndex cues, int index,
|
WebmExtractor extractor, DataSource dataSource, int segmentNum, int trigger) {
|
||||||
int trigger, int numSegmentsPerChunk) {
|
int lastSegmentNum = segmentIndex.getLastSegmentNum();
|
||||||
|
int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1;
|
||||||
// Computes the segments to included in the next fetch.
|
long startTimeUs = segmentIndex.getTimeUs(segmentNum);
|
||||||
int numSegmentsToFetch = Math.min(numSegmentsPerChunk, cues.length - index);
|
long endTimeUs = segmentNum < lastSegmentNum ? segmentIndex.getTimeUs(segmentNum + 1)
|
||||||
int lastSegmentInChunk = index + numSegmentsToFetch - 1;
|
: startTimeUs + segmentIndex.getDurationUs(segmentNum);
|
||||||
int nextIndex = lastSegmentInChunk == cues.length - 1 ? -1 : lastSegmentInChunk + 1;
|
RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum);
|
||||||
|
DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length,
|
||||||
long startTimeUs = cues.timesUs[index];
|
|
||||||
|
|
||||||
// Compute the end time, prefer to use next segment start time if there is a next segment.
|
|
||||||
long endTimeUs = nextIndex == -1 ?
|
|
||||||
cues.timesUs[lastSegmentInChunk] + cues.durationsUs[lastSegmentInChunk] :
|
|
||||||
cues.timesUs[nextIndex];
|
|
||||||
|
|
||||||
long offset = cues.offsets[index];
|
|
||||||
|
|
||||||
// Compute combined segments byte length.
|
|
||||||
long size = 0;
|
|
||||||
for (int i = index; i <= lastSegmentInChunk; i++) {
|
|
||||||
size += cues.sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
DataSpec dataSpec = new DataSpec(representation.uri, offset, size,
|
|
||||||
representation.getCacheKey());
|
representation.getCacheKey());
|
||||||
return new WebmMediaChunk(dataSource, dataSpec, representation.format, trigger, extractor,
|
return new WebmMediaChunk(dataSource, dataSpec, representation.format, trigger, extractor,
|
||||||
startTimeUs, endTimeUs, nextIndex);
|
startTimeUs, endTimeUs, nextSegmentNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class InitializationWebmLoadable extends Chunk {
|
private class InitializationWebmLoadable extends Chunk {
|
||||||
|
|
||||||
private final Representation representation;
|
|
||||||
private final WebmExtractor extractor;
|
private final WebmExtractor extractor;
|
||||||
|
private final Uri uri;
|
||||||
|
|
||||||
public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
|
public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
|
||||||
WebmExtractor extractor, Representation representation) {
|
Format format, WebmExtractor extractor) {
|
||||||
super(dataSource, dataSpec, representation.format, trigger);
|
super(dataSource, dataSpec, format, trigger);
|
||||||
this.extractor = extractor;
|
this.extractor = extractor;
|
||||||
this.representation = representation;
|
this.uri = dataSpec.uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -234,22 +221,7 @@ public class DashWebmChunkSource implements ChunkSource {
|
|||||||
if (!extractor.isPrepared()) {
|
if (!extractor.isPrepared()) {
|
||||||
throw new ParserException("Invalid initialization data");
|
throw new ParserException("Invalid initialization data");
|
||||||
}
|
}
|
||||||
validateCues(extractor.getCues());
|
segmentIndexes.put(format.id, new DashWrappingSegmentIndex(extractor.getCues(), uri, 0));
|
||||||
}
|
|
||||||
|
|
||||||
private void validateCues(SegmentIndex cues) {
|
|
||||||
long expectedSizeBytes = representation.indexEnd - representation.indexStart + 1;
|
|
||||||
if (cues.sizeBytes != expectedSizeBytes) {
|
|
||||||
Log.w(TAG, "Cues length mismatch: got " + cues.sizeBytes +
|
|
||||||
" but expected " + expectedSizeBytes);
|
|
||||||
}
|
|
||||||
long expectedContentLength = cues.offsets[cues.length - 1] +
|
|
||||||
cues.sizes[cues.length - 1] + representation.indexEnd + 1;
|
|
||||||
if (representation.contentLength > 0
|
|
||||||
&& expectedContentLength != representation.contentLength) {
|
|
||||||
Log.w(TAG, "ContentLength mismatch: got " + expectedContentLength +
|
|
||||||
" but expected " + representation.contentLength);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,8 @@ public final class RangedUri {
|
|||||||
private final Uri baseUri;
|
private final Uri baseUri;
|
||||||
private final String stringUri;
|
private final String stringUri;
|
||||||
|
|
||||||
|
private int hashCode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an ranged uri.
|
* Constructs an ranged uri.
|
||||||
* <p>
|
* <p>
|
||||||
@ -82,4 +84,55 @@ public final class RangedUri {
|
|||||||
return uri;
|
return uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to merge this {@link RangedUri} with another.
|
||||||
|
* <p>
|
||||||
|
* A merge is successful if both instances define the same {@link Uri}, and if one starte the
|
||||||
|
* byte after the other ends, forming a contiguous region with no overlap.
|
||||||
|
* <p>
|
||||||
|
* If {@code other} is null then the merge is considered unsuccessful, and null is returned.
|
||||||
|
*
|
||||||
|
* @param other The {@link RangedUri} to merge.
|
||||||
|
* @return The merged {@link RangedUri} if the merge was successful. Null otherwise.
|
||||||
|
*/
|
||||||
|
public RangedUri attemptMerge(RangedUri other) {
|
||||||
|
if (other == null || !getUri().equals(other.getUri())) {
|
||||||
|
return null;
|
||||||
|
} else if (length != -1 && start + length == other.start) {
|
||||||
|
return new RangedUri(baseUri, stringUri, start,
|
||||||
|
other.length == -1 ? -1 : length + other.length);
|
||||||
|
} else if (other.length != -1 && other.start + other.length == start) {
|
||||||
|
return new RangedUri(baseUri, stringUri, other.start,
|
||||||
|
length == -1 ? -1 : other.length + length);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
if (hashCode == 0) {
|
||||||
|
int result = 17;
|
||||||
|
result = 31 * result + (int) start;
|
||||||
|
result = 31 * result + (int) length;
|
||||||
|
result = 31 * result + getUri().hashCode();
|
||||||
|
hashCode = result;
|
||||||
|
}
|
||||||
|
return hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj == null || getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RangedUri other = (RangedUri) obj;
|
||||||
|
return this.start == other.start
|
||||||
|
&& this.length == other.length
|
||||||
|
&& getUri().equals(other.getUri());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
package com.google.android.exoplayer.dash.mpd;
|
package com.google.android.exoplayer.dash.mpd;
|
||||||
|
|
||||||
import com.google.android.exoplayer.chunk.Format;
|
import com.google.android.exoplayer.chunk.Format;
|
||||||
|
import com.google.android.exoplayer.dash.DashSegmentIndex;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
@ -79,6 +80,37 @@ public class Representation {
|
|||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a {@link RangedUri} defining the location of the representation's initialization data.
|
||||||
|
* May be null if no initialization data exists.
|
||||||
|
*
|
||||||
|
* @return A {@link RangedUri} defining the location of the initialization data, or null.
|
||||||
|
*/
|
||||||
|
public RangedUri getInitializationUri() {
|
||||||
|
return new RangedUri(uri, null, initializationStart,
|
||||||
|
initializationEnd - initializationStart + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a {@link RangedUri} defining the location of the representation's segment index. Null if
|
||||||
|
* the representation provides an index directly.
|
||||||
|
*
|
||||||
|
* @return The location of the segment index, or null.
|
||||||
|
*/
|
||||||
|
public RangedUri getIndexUri() {
|
||||||
|
return new RangedUri(uri, null, indexStart, indexEnd - indexStart + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a segment index, if the representation is able to provide one directly. Null if the
|
||||||
|
* segment index is defined externally.
|
||||||
|
*
|
||||||
|
* @return The segment index, or null.
|
||||||
|
*/
|
||||||
|
public DashSegmentIndex getIndex() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a cache key for the {@link Representation}, in the format
|
* Generates a cache key for the {@link Representation}, in the format
|
||||||
* {@code contentId + "." + format.id + "." + revisionId}.
|
* {@code contentId + "." + format.id + "." + revisionId}.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user