From b3726cf7619b95e2c011b74bc99283aea956805f Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 23 Nov 2016 03:00:16 -0800 Subject: [PATCH] Support DASH multi-segment fetches Note that multi-segment fetching is only possible in the case that segments in a representation are defined to have the same Uri and adjacent ranges (this is very rarely true for live streams, but is quite often true for on-demand). In the case that merging is requested but not possible, the implementation will request one at a time. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=140012443 --- .../source/chunk/ContainerMediaChunk.java | 16 ++++- .../exoplayer2/source/chunk/MediaChunk.java | 2 +- .../source/dash/DefaultDashChunkSource.java | 62 ++++++++++++++----- .../smoothstreaming/DefaultSsChunkSource.java | 2 +- 4 files changed, 60 insertions(+), 22 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java index 130dddc5eb..a5af3cc42f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java @@ -32,8 +32,9 @@ import java.io.IOException; */ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackMetadataOutput { - private final ChunkExtractorWrapper extractorWrapper; + private final int chunkCount; private final long sampleOffsetUs; + private final ChunkExtractorWrapper extractorWrapper; private final Format sampleFormat; private volatile int bytesLoaded; @@ -49,6 +50,9 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackMe * @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param chunkIndex The index of the chunk. + * @param chunkCount The number of chunks in the underlying media that are spanned by this + * instance. Normally equal to one, but may be larger if multiple chunks as defined by the + * underlying media are being merged into a single load. * @param sampleOffsetUs An offset to add to the sample timestamps parsed by the extractor. * @param extractorWrapper A wrapped extractor to use for parsing the data. * @param sampleFormat The {@link Format} of the samples in the chunk, if known. May be null if @@ -56,15 +60,21 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackMe */ public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat, int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, - int chunkIndex, long sampleOffsetUs, ChunkExtractorWrapper extractorWrapper, + int chunkIndex, int chunkCount, long sampleOffsetUs, ChunkExtractorWrapper extractorWrapper, Format sampleFormat) { super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); - this.extractorWrapper = extractorWrapper; + this.chunkCount = chunkCount; this.sampleOffsetUs = sampleOffsetUs; + this.extractorWrapper = extractorWrapper; this.sampleFormat = sampleFormat; } + @Override + public int getNextChunkIndex() { + return chunkIndex + chunkCount; + } + @Override public boolean isLoadCompleted() { return loadCompleted; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java index d3e211c09f..3a02884fff 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java @@ -53,7 +53,7 @@ public abstract class MediaChunk extends Chunk { /** * Returns the next chunk index. */ - public final int getNextChunkIndex() { + public int getNextChunkIndex() { return chunkIndex + 1; } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 919a0231ea..0e3d127796 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.dash; +import android.net.Uri; import android.os.SystemClock; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -54,9 +55,15 @@ public class DefaultDashChunkSource implements DashChunkSource { public static final class Factory implements DashChunkSource.Factory { private final DataSource.Factory dataSourceFactory; + private final int maxSegmentsPerLoad; public Factory(DataSource.Factory dataSourceFactory) { + this(dataSourceFactory, 1); + } + + public Factory(DataSource.Factory dataSourceFactory, int maxSegmentsPerLoad) { this.dataSourceFactory = dataSourceFactory; + this.maxSegmentsPerLoad = maxSegmentsPerLoad; } @Override @@ -65,7 +72,8 @@ public class DefaultDashChunkSource implements DashChunkSource { TrackSelection trackSelection, long elapsedRealtimeOffsetMs) { DataSource dataSource = dataSourceFactory.createDataSource(); return new DefaultDashChunkSource(manifestLoaderErrorThrower, manifest, periodIndex, - adaptationSetIndex, trackSelection, dataSource, elapsedRealtimeOffsetMs); + adaptationSetIndex, trackSelection, dataSource, elapsedRealtimeOffsetMs, + maxSegmentsPerLoad); } } @@ -76,6 +84,7 @@ public class DefaultDashChunkSource implements DashChunkSource { private final RepresentationHolder[] representationHolders; private final DataSource dataSource; private final long elapsedRealtimeOffsetMs; + private final int maxSegmentsPerLoad; private DashManifest manifest; private int periodIndex; @@ -93,10 +102,13 @@ public class DefaultDashChunkSource implements DashChunkSource { * @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. + * @param maxSegmentsPerLoad The maximum number of segments to combine into a single request. + * Note that segments will only be combined if their {@link Uri}s are the same and if their + * data ranges are adjacent. */ public DefaultDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, DashManifest manifest, int periodIndex, int adaptationSetIndex, TrackSelection trackSelection, - DataSource dataSource, long elapsedRealtimeOffsetMs) { + DataSource dataSource, long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad) { this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.manifest = manifest; this.adaptationSetIndex = adaptationSetIndex; @@ -104,6 +116,7 @@ public class DefaultDashChunkSource implements DashChunkSource { this.dataSource = dataSource; this.periodIndex = periodIndex; this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs; + this.maxSegmentsPerLoad = maxSegmentsPerLoad; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); List representations = getRepresentations(); @@ -219,9 +232,10 @@ public class DefaultDashChunkSource implements DashChunkSource { return; } + int maxSegmentCount = Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1); Chunk nextMediaChunk = newMediaChunk(representationHolder, dataSource, trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(), - trackSelection.getSelectionData(), sampleFormat, segmentNum); + trackSelection.getSelectionData(), sampleFormat, segmentNum, maxSegmentCount); out.chunk = nextMediaChunk; } @@ -260,7 +274,7 @@ public class DefaultDashChunkSource implements DashChunkSource { RepresentationHolder representationHolder = representationHolders[trackSelection.indexOf(chunk.trackFormat)]; int lastAvailableSegmentNum = representationHolder.getLastSegmentNum(); - if (((MediaChunk) chunk).chunkIndex >= lastAvailableSegmentNum) { + if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) { missingLastSegment = true; return true; } @@ -284,7 +298,7 @@ public class DefaultDashChunkSource implements DashChunkSource { } } - private Chunk newInitializationChunk(RepresentationHolder representationHolder, + private static Chunk newInitializationChunk(RepresentationHolder representationHolder, DataSource dataSource, Format trackFormat, int trackSelectionReason, Object trackSelectionData, RangedUri initializationUri, RangedUri indexUri) { RangedUri requestUri; @@ -305,24 +319,38 @@ public class DefaultDashChunkSource implements DashChunkSource { trackSelectionReason, trackSelectionData, representationHolder.extractorWrapper); } - private Chunk newMediaChunk(RepresentationHolder representationHolder, DataSource dataSource, - Format trackFormat, int trackSelectionReason, - Object trackSelectionData, Format sampleFormat, int segmentNum) { + private static Chunk newMediaChunk(RepresentationHolder representationHolder, + DataSource dataSource, Format trackFormat, int trackSelectionReason, + Object trackSelectionData, Format sampleFormat, int firstSegmentNum, int maxSegmentCount) { Representation representation = representationHolder.representation; - long startTimeUs = representationHolder.getSegmentStartTimeUs(segmentNum); - long endTimeUs = representationHolder.getSegmentEndTimeUs(segmentNum); - RangedUri segmentUri = representationHolder.getSegmentUrl(segmentNum); - DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(representation.baseUrl), - segmentUri.start, segmentUri.length, representation.getCacheKey()); - + long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum); + RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum); + String baseUrl = representation.baseUrl; if (representationHolder.extractorWrapper == null) { + long endTimeUs = representationHolder.getSegmentEndTimeUs(firstSegmentNum); + DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl), + segmentUri.start, segmentUri.length, representation.getCacheKey()); return new SingleSampleMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, - trackSelectionData, startTimeUs, endTimeUs, segmentNum, trackFormat); + trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, trackFormat); } else { + int segmentCount = 1; + for (int i = 1; i < maxSegmentCount; i++) { + RangedUri nextSegmentUri = representationHolder.getSegmentUrl(firstSegmentNum + i); + RangedUri mergedSegmentUri = segmentUri.attemptMerge(nextSegmentUri, baseUrl); + if (mergedSegmentUri == null) { + // Unable to merge segment fetches because the URIs do not merge. + break; + } + segmentUri = mergedSegmentUri; + segmentCount++; + } + long endTimeUs = representationHolder.getSegmentEndTimeUs(firstSegmentNum + segmentCount - 1); + DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl), + segmentUri.start, segmentUri.length, representation.getCacheKey()); long sampleOffsetUs = -representation.presentationTimeOffsetUs; return new ContainerMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, - trackSelectionData, startTimeUs, endTimeUs, segmentNum, sampleOffsetUs, - representationHolder.extractorWrapper, sampleFormat); + trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, segmentCount, + sampleOffsetUs, representationHolder.extractorWrapper, sampleFormat); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index f51280e0b9..aa197806e2 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -218,7 +218,7 @@ public class DefaultSsChunkSource implements SsChunkSource { // To convert them the absolute timestamps, we need to set sampleOffsetUs to chunkStartTimeUs. long sampleOffsetUs = chunkStartTimeUs; return new ContainerMediaChunk(dataSource, dataSpec, format, trackSelectionReason, - trackSelectionData, chunkStartTimeUs, chunkEndTimeUs, chunkIndex, sampleOffsetUs, + trackSelectionData, chunkStartTimeUs, chunkEndTimeUs, chunkIndex, 1, sampleOffsetUs, extractorWrapper, format); }