From 87461821fe0c4d502c91a449b9578e6c13ca31ad Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 17 Jul 2014 11:28:05 +0100 Subject: [PATCH] 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. --- .../android/exoplayer/demo/Samples.java | 4 + .../android/exoplayer/chunk/Format.java | 5 + .../exoplayer/dash/DashMp4ChunkSource.java | 164 +++++++++--------- .../exoplayer/dash/DashWebmChunkSource.java | 116 +++++-------- .../android/exoplayer/dash/mpd/RangedUri.java | 53 ++++++ .../exoplayer/dash/mpd/Representation.java | 32 ++++ 6 files changed, 216 insertions(+), 158 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java index c2dc0ff1c7..01bf8dc527 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -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" + "&expire=19000000000&signature=88DC53943385CED8CF9F37ADD9E9843E3BF621E6." + "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[] { diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/Format.java b/library/src/main/java/com/google/android/exoplayer/chunk/Format.java index ea8344c6c2..875956c0ee 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/Format.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/Format.java @@ -100,6 +100,11 @@ public class Format { this.bandwidth = bitrate / 8; } + @Override + public int hashCode() { + return id.hashCode(); + } + /** * Implements equality based on {@link #id} only. */ diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java index 4bf07c1b3a..b6d5219825 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashMp4ChunkSource.java @@ -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.MediaChunk; 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.parser.SegmentIndex; import com.google.android.exoplayer.parser.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; 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.util.Arrays; @@ -47,26 +46,17 @@ import java.util.List; */ 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 DataSource dataSource; private final FormatEvaluator evaluator; private final Evaluation evaluation; private final int maxWidth; private final int maxHeight; - private final int numSegmentsPerChunk; private final Format[] formats; private final HashMap representations; private final HashMap extractors; + private final HashMap segmentIndexes; private boolean lastChunkWasInitialization; @@ -77,23 +67,11 @@ public class DashMp4ChunkSource implements ChunkSource { */ public DashMp4ChunkSource(DataSource dataSource, FormatEvaluator evaluator, 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.evaluator = evaluator; - this.numSegmentsPerChunk = numSegmentsPerChunk; this.formats = new Format[representations.length]; this.extractors = new HashMap(); + this.segmentIndexes = new HashMap(); this.representations = new HashMap(); this.trackInfo = new TrackInfo(representations[0].format.mimeType, representations[0].periodDuration * 1000); @@ -106,6 +84,10 @@ public class DashMp4ChunkSource implements ChunkSource { maxHeight = Math.max(formats[i].height, maxHeight); extractors.put(formats[i].id, new FragmentedMp4Extractor()); 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.maxHeight = maxHeight; @@ -161,29 +143,39 @@ public class DashMp4ChunkSource implements ChunkSource { Representation selectedRepresentation = representations.get(selectedFormat.id); FragmentedMp4Extractor extractor = extractors.get(selectedRepresentation.format.id); + + RangedUri pendingInitializationUri = null; + RangedUri pendingIndexUri = null; if (extractor.getTrack() == null) { - Chunk initializationChunk = newInitializationChunk(selectedRepresentation, extractor, - dataSource, evaluation.trigger); + pendingInitializationUri = selectedRepresentation.getInitializationUri(); + } + 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; out.chunk = initializationChunk; return; } - int nextIndex; + int nextSegmentNum; + DashSegmentIndex segmentIndex = segmentIndexes.get(selectedRepresentation.format.id); if (queue.isEmpty()) { - nextIndex = Util.binarySearchFloor(extractor.getSegmentIndex().timesUs, seekPositionUs, - true, true); + nextSegmentNum = segmentIndex.getSegmentNum(seekPositionUs); } else { - nextIndex = queue.get(out.queueSize - 1).nextChunkIndex; + nextSegmentNum = queue.get(out.queueSize - 1).nextChunkIndex; } - if (nextIndex == -1) { + if (nextSegmentNum == -1) { out.chunk = null; return; } - Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, extractor, dataSource, - extractor.getSegmentIndex(), nextIndex, evaluation.trigger, numSegmentsPerChunk); + Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, segmentIndex, extractor, + dataSource, nextSegmentNum, evaluation.trigger); lastChunkWasInitialization = false; out.chunk = nextMediaChunk; } @@ -198,75 +190,75 @@ public class DashMp4ChunkSource implements ChunkSource { // Do nothing. } - private static Chunk newInitializationChunk(Representation representation, - FragmentedMp4Extractor extractor, DataSource dataSource, int trigger) { - DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1, + private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri, + Representation representation, FragmentedMp4Extractor extractor, DataSource dataSource, + 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()); - 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, - FragmentedMp4Extractor extractor, DataSource dataSource, SegmentIndex sidx, int index, - int trigger, int numSegmentsPerChunk) { - - // Computes the segments to included in the next fetch. - int numSegmentsToFetch = Math.min(numSegmentsPerChunk, sidx.length - index); - int lastSegmentInChunk = index + numSegmentsToFetch - 1; - int nextIndex = lastSegmentInChunk == sidx.length - 1 ? -1 : lastSegmentInChunk + 1; - - 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, + private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex, + FragmentedMp4Extractor extractor, DataSource dataSource, int segmentNum, int trigger) { + int lastSegmentNum = segmentIndex.getLastSegmentNum(); + int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1; + long startTimeUs = segmentIndex.getTimeUs(segmentNum); + long endTimeUs = segmentNum < lastSegmentNum ? segmentIndex.getTimeUs(segmentNum + 1) + : startTimeUs + segmentIndex.getDurationUs(segmentNum); + RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum); + DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length, representation.getCacheKey()); 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 int expectedExtractorResult; + private final long indexAnchor; + private final Uri uri; public InitializationMp4Loadable(DataSource dataSource, DataSpec dataSpec, int trigger, - FragmentedMp4Extractor extractor, Representation representation) { - super(dataSource, dataSpec, representation.format, trigger); + Format format, FragmentedMp4Extractor extractor, int expectedExtractorResult, + long indexAnchor) { + super(dataSource, dataSpec, format, trigger); this.extractor = extractor; - this.representation = representation; + this.expectedExtractorResult = expectedExtractorResult; + this.indexAnchor = indexAnchor; + this.uri = dataSpec.uri; } @Override protected void consumeStream(NonBlockingInputStream stream) throws IOException { int result = extractor.read(stream, null); - if (result != EXPECTED_INITIALIZATION_RESULT) { - throw new ParserException("Invalid initialization data"); + if (result != expectedExtractorResult) { + throw new ParserException("Invalid extractor result. Expected " + + expectedExtractorResult + ", got " + result); } - validateSegmentIndex(extractor.getSegmentIndex()); - } - - 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); + if ((result & FragmentedMp4Extractor.RESULT_READ_SIDX) != 0) { + segmentIndexes.put(format.id, + new DashWrappingSegmentIndex(extractor.getSegmentIndex(), uri, indexAnchor)); } } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java index 24173d3617..3c3718fcd5 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashWebmChunkSource.java @@ -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.MediaChunk; 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.parser.SegmentIndex; import com.google.android.exoplayer.parser.webm.DefaultWebmExtractor; import com.google.android.exoplayer.parser.webm.WebmExtractor; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; 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.util.Arrays; @@ -48,34 +47,27 @@ import java.util.List; */ public class DashWebmChunkSource implements ChunkSource { - private static final String TAG = "DashWebmChunkSource"; - private final TrackInfo trackInfo; private final DataSource dataSource; private final FormatEvaluator evaluator; private final Evaluation evaluation; private final int maxWidth; private final int maxHeight; - private final int numSegmentsPerChunk; private final Format[] formats; private final HashMap representations; private final HashMap extractors; + private final HashMap segmentIndexes; private boolean lastChunkWasInitialization; - public DashWebmChunkSource( - DataSource dataSource, FormatEvaluator evaluator, Representation... representations) { - this(dataSource, evaluator, 1, representations); - } - public DashWebmChunkSource(DataSource dataSource, FormatEvaluator evaluator, - int numSegmentsPerChunk, Representation... representations) { + Representation... representations) { this.dataSource = dataSource; this.evaluator = evaluator; - this.numSegmentsPerChunk = numSegmentsPerChunk; this.formats = new Format[representations.length]; this.extractors = new HashMap(); + this.segmentIndexes = new HashMap(); this.representations = new HashMap(); this.trackInfo = new TrackInfo( representations[0].format.mimeType, representations[0].periodDuration * 1000); @@ -88,6 +80,10 @@ public class DashWebmChunkSource implements ChunkSource { maxHeight = Math.max(formats[i].height, maxHeight); extractors.put(formats[i].id, new DefaultWebmExtractor()); 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.maxHeight = maxHeight; @@ -143,28 +139,34 @@ public class DashWebmChunkSource implements ChunkSource { Representation selectedRepresentation = representations.get(selectedFormat.id); WebmExtractor extractor = extractors.get(selectedRepresentation.format.id); + if (!extractor.isPrepared()) { - Chunk initializationChunk = newInitializationChunk(selectedRepresentation, extractor, - dataSource, evaluation.trigger); + // TODO: This code forces cues to exist and to immediately follow the initialization + // 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; out.chunk = initializationChunk; return; } - int nextIndex; + int nextSegmentNum; + DashSegmentIndex segmentIndex = segmentIndexes.get(selectedRepresentation.format.id); if (queue.isEmpty()) { - nextIndex = Util.binarySearchFloor(extractor.getCues().timesUs, seekPositionUs, true, true); + nextSegmentNum = segmentIndex.getSegmentNum(seekPositionUs); } else { - nextIndex = queue.get(out.queueSize - 1).nextChunkIndex; + nextSegmentNum = queue.get(out.queueSize - 1).nextChunkIndex; } - if (nextIndex == -1) { + if (nextSegmentNum == -1) { out.chunk = null; return; } - Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, extractor, dataSource, - extractor.getCues(), nextIndex, evaluation.trigger, numSegmentsPerChunk); + Chunk nextMediaChunk = newMediaChunk(selectedRepresentation, segmentIndex, extractor, + dataSource, nextSegmentNum, evaluation.trigger); lastChunkWasInitialization = false; out.chunk = nextMediaChunk; } @@ -179,53 +181,38 @@ public class DashWebmChunkSource implements ChunkSource { // Do nothing. } - private static Chunk newInitializationChunk(Representation representation, + private Chunk newInitializationChunk(RangedUri initializationUri, Representation representation, WebmExtractor extractor, DataSource dataSource, int trigger) { - DataSpec dataSpec = new DataSpec(representation.uri, 0, representation.indexEnd + 1, - representation.getCacheKey()); - return new InitializationWebmLoadable(dataSource, dataSpec, trigger, extractor, representation); + DataSpec dataSpec = new DataSpec(initializationUri.getUri(), initializationUri.start, + initializationUri.length, representation.getCacheKey()); + return new InitializationWebmLoadable(dataSource, dataSpec, trigger, representation.format, + extractor); } - private static Chunk newMediaChunk(Representation representation, - WebmExtractor extractor, DataSource dataSource, SegmentIndex cues, int index, - int trigger, int numSegmentsPerChunk) { - - // Computes the segments to included in the next fetch. - int numSegmentsToFetch = Math.min(numSegmentsPerChunk, cues.length - index); - int lastSegmentInChunk = index + numSegmentsToFetch - 1; - int nextIndex = lastSegmentInChunk == cues.length - 1 ? -1 : lastSegmentInChunk + 1; - - 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, + private Chunk newMediaChunk(Representation representation, DashSegmentIndex segmentIndex, + WebmExtractor extractor, DataSource dataSource, int segmentNum, int trigger) { + int lastSegmentNum = segmentIndex.getLastSegmentNum(); + int nextSegmentNum = segmentNum == lastSegmentNum ? -1 : segmentNum + 1; + long startTimeUs = segmentIndex.getTimeUs(segmentNum); + long endTimeUs = segmentNum < lastSegmentNum ? segmentIndex.getTimeUs(segmentNum + 1) + : startTimeUs + segmentIndex.getDurationUs(segmentNum); + RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum); + DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length, representation.getCacheKey()); 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 Uri uri; public InitializationWebmLoadable(DataSource dataSource, DataSpec dataSpec, int trigger, - WebmExtractor extractor, Representation representation) { - super(dataSource, dataSpec, representation.format, trigger); + Format format, WebmExtractor extractor) { + super(dataSource, dataSpec, format, trigger); this.extractor = extractor; - this.representation = representation; + this.uri = dataSpec.uri; } @Override @@ -234,22 +221,7 @@ public class DashWebmChunkSource implements ChunkSource { if (!extractor.isPrepared()) { throw new ParserException("Invalid initialization data"); } - validateCues(extractor.getCues()); - } - - 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); - } + segmentIndexes.put(format.id, new DashWrappingSegmentIndex(extractor.getCues(), uri, 0)); } } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java index 43f52c0108..cd18f85599 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java @@ -42,6 +42,8 @@ public final class RangedUri { private final Uri baseUri; private final String stringUri; + private int hashCode; + /** * Constructs an ranged uri. *

@@ -82,4 +84,55 @@ public final class RangedUri { return uri; } + /** + * Attempts to merge this {@link RangedUri} with another. + *

+ * 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. + *

+ * 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()); + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java index 7b8b996888..e1d309266b 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer.dash.mpd; import com.google.android.exoplayer.chunk.Format; +import com.google.android.exoplayer.dash.DashSegmentIndex; import android.net.Uri; @@ -79,6 +80,37 @@ public class Representation { 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 * {@code contentId + "." + format.id + "." + revisionId}.