Improve DASH/SS seek performance

This change enables feeding decoders from the closest sync frame
before a specified seek position, where-as previously we'd
always feed decoders from the start of the chunk. This avoids
decoding and discarding many audio samples during each seek. The
same benefit also applies to video chunks containing more than
one key-frame.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=190539547
This commit is contained in:
olly 2018-03-26 15:55:34 -07:00 committed by Oliver Woodman
parent f7cd9f7d1d
commit eb34a2a102
9 changed files with 115 additions and 26 deletions

View File

@ -2,6 +2,8 @@
### dev-v2 (not yet released) ### ### dev-v2 (not yet released) ###
* Optimize seeking in FMP4 by enabling seeking to the nearest sync sample within
a fragment. This benefits standalone FMP4 playbacks, DASH and SmoothStreaming.
* Optimize seeking in FMP4. * Optimize seeking in FMP4.
* Match codecs starting with "mp4a" to different Audio MimeTypes * Match codecs starting with "mp4a" to different Audio MimeTypes
([#3779](https://github.com/google/ExoPlayer/issues/3779)). ([#3779](https://github.com/google/ExoPlayer/issues/3779)).

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.source.chunk; package com.google.android.exoplayer2.source.chunk;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
@ -24,6 +25,12 @@ import com.google.android.exoplayer2.upstream.DataSpec;
*/ */
public abstract class BaseMediaChunk extends MediaChunk { public abstract class BaseMediaChunk extends MediaChunk {
/**
* The media time from which output will begin, or {@link C#TIME_UNSET} if the whole chunk should
* be output.
*/
public final long seekTimeUs;
private BaseMediaChunkOutput output; private BaseMediaChunkOutput output;
private int[] firstSampleIndices; private int[] firstSampleIndices;
@ -35,6 +42,8 @@ public abstract class BaseMediaChunk extends MediaChunk {
* @param trackSelectionData See {@link #trackSelectionData}. * @param trackSelectionData See {@link #trackSelectionData}.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @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 endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param seekTimeUs The media time from which output will begin, or {@link C#TIME_UNSET} if the
* whole chunk should be output.
* @param chunkIndex The index of the chunk. * @param chunkIndex The index of the chunk.
*/ */
public BaseMediaChunk( public BaseMediaChunk(
@ -45,9 +54,11 @@ public abstract class BaseMediaChunk extends MediaChunk {
Object trackSelectionData, Object trackSelectionData,
long startTimeUs, long startTimeUs,
long endTimeUs, long endTimeUs,
long seekTimeUs,
long chunkIndex) { long chunkIndex) {
super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs,
endTimeUs, chunkIndex); endTimeUs, chunkIndex);
this.seekTimeUs = seekTimeUs;
} }
/** /**

View File

@ -96,18 +96,23 @@ public final class ChunkExtractorWrapper implements ExtractorOutput {
} }
/** /**
* Initializes the wrapper to output to {@link TrackOutput}s provided by the specified * Initializes the wrapper to output to {@link TrackOutput}s provided by the specified {@link
* {@link TrackOutputProvider}, and configures the extractor to receive data from a new chunk. * TrackOutputProvider}, and configures the extractor to receive data from a new chunk.
* *
* @param trackOutputProvider The provider of {@link TrackOutput}s that will receive sample data. * @param trackOutputProvider The provider of {@link TrackOutput}s that will receive sample data.
* @param seekTimeUs The seek position within the new chunk, or {@link C#TIME_UNSET} to output the
* whole chunk.
*/ */
public void init(TrackOutputProvider trackOutputProvider) { public void init(TrackOutputProvider trackOutputProvider, long seekTimeUs) {
this.trackOutputProvider = trackOutputProvider; this.trackOutputProvider = trackOutputProvider;
if (!extractorInitialized) { if (!extractorInitialized) {
extractor.init(this); extractor.init(this);
if (seekTimeUs != C.TIME_UNSET) {
extractor.seek(/* position= */ 0, seekTimeUs);
}
extractorInitialized = true; extractorInitialized = true;
} else { } else {
extractor.seek(0, 0); extractor.seek(/* position= */ 0, seekTimeUs == C.TIME_UNSET ? 0 : seekTimeUs);
for (int i = 0; i < bindingTrackOutputs.size(); i++) { for (int i = 0; i < bindingTrackOutputs.size(); i++) {
bindingTrackOutputs.valueAt(i).bind(trackOutputProvider); bindingTrackOutputs.valueAt(i).bind(trackOutputProvider);
} }

View File

@ -242,7 +242,7 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
for (int i = 0; i < mediaChunks.size(); i++) { for (int i = 0; i < mediaChunks.size(); i++) {
BaseMediaChunk mediaChunk = mediaChunks.get(i); BaseMediaChunk mediaChunk = mediaChunks.get(i);
long mediaChunkStartTimeUs = mediaChunk.startTimeUs; long mediaChunkStartTimeUs = mediaChunk.startTimeUs;
if (mediaChunkStartTimeUs == positionUs) { if (mediaChunkStartTimeUs == positionUs && mediaChunk.seekTimeUs == C.TIME_UNSET) {
seekToMediaChunk = mediaChunk; seekToMediaChunk = mediaChunk;
break; break;
} else if (mediaChunkStartTimeUs > positionUs) { } else if (mediaChunkStartTimeUs > positionUs) {

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.source.chunk; package com.google.android.exoplayer2.source.chunk;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
@ -46,6 +47,8 @@ public class ContainerMediaChunk extends BaseMediaChunk {
* @param trackSelectionData See {@link #trackSelectionData}. * @param trackSelectionData See {@link #trackSelectionData}.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds. * @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 endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param seekTimeUs The media time from which output will begin, or {@link C#TIME_UNSET} if the
* whole chunk should be output.
* @param chunkIndex The index of the chunk. * @param chunkIndex The index of the chunk.
* @param chunkCount The number of chunks in the underlying media that are spanned by this * @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 * instance. Normally equal to one, but may be larger if multiple chunks as defined by the
@ -61,12 +64,21 @@ public class ContainerMediaChunk extends BaseMediaChunk {
Object trackSelectionData, Object trackSelectionData,
long startTimeUs, long startTimeUs,
long endTimeUs, long endTimeUs,
long seekTimeUs,
long chunkIndex, long chunkIndex,
int chunkCount, int chunkCount,
long sampleOffsetUs, long sampleOffsetUs,
ChunkExtractorWrapper extractorWrapper) { ChunkExtractorWrapper extractorWrapper) {
super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, super(
endTimeUs, chunkIndex); dataSource,
dataSpec,
trackFormat,
trackSelectionReason,
trackSelectionData,
startTimeUs,
endTimeUs,
seekTimeUs,
chunkIndex);
this.chunkCount = chunkCount; this.chunkCount = chunkCount;
this.sampleOffsetUs = sampleOffsetUs; this.sampleOffsetUs = sampleOffsetUs;
this.extractorWrapper = extractorWrapper; this.extractorWrapper = extractorWrapper;
@ -111,7 +123,8 @@ public class ContainerMediaChunk extends BaseMediaChunk {
// Configure the output and set it as the target for the extractor wrapper. // Configure the output and set it as the target for the extractor wrapper.
BaseMediaChunkOutput output = getOutput(); BaseMediaChunkOutput output = getOutput();
output.setSampleOffsetUs(sampleOffsetUs); output.setSampleOffsetUs(sampleOffsetUs);
extractorWrapper.init(output); extractorWrapper.init(
output, seekTimeUs == C.TIME_UNSET ? 0 : (seekTimeUs - sampleOffsetUs));
} }
// Load and decode the sample data. // Load and decode the sample data.
try { try {

View File

@ -78,7 +78,7 @@ public final class InitializationChunk extends Chunk {
ExtractorInput input = new DefaultExtractorInput(dataSource, ExtractorInput input = new DefaultExtractorInput(dataSource,
loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec));
if (bytesLoaded == 0) { if (bytesLoaded == 0) {
extractorWrapper.init(null); extractorWrapper.init(/* trackOutputProvider= */ null, C.TIME_UNSET);
} }
// Load and decode the initialization data. // Load and decode the initialization data.
try { try {

View File

@ -61,8 +61,16 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk {
long chunkIndex, long chunkIndex,
int trackType, int trackType,
Format sampleFormat) { Format sampleFormat) {
super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs, super(
endTimeUs, chunkIndex); dataSource,
dataSpec,
trackFormat,
trackSelectionReason,
trackSelectionData,
startTimeUs,
endTimeUs,
C.TIME_UNSET,
chunkIndex);
this.trackType = trackType; this.trackType = trackType;
this.sampleFormat = sampleFormat; this.sampleFormat = sampleFormat;
} }

View File

@ -328,9 +328,18 @@ public class DefaultDashChunkSource implements DashChunkSource {
int maxSegmentCount = int maxSegmentCount =
(int) Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1); (int) Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1);
out.chunk = newMediaChunk(representationHolder, dataSource, trackType, long seekTimeUs = previous == null ? loadPositionUs : C.TIME_UNSET;
trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(), out.chunk =
trackSelection.getSelectionData(), segmentNum, maxSegmentCount); newMediaChunk(
representationHolder,
dataSource,
trackType,
trackSelection.getSelectedFormat(),
trackSelection.getSelectionReason(),
trackSelection.getSelectionData(),
segmentNum,
maxSegmentCount,
seekTimeUs);
} }
@Override @Override
@ -442,7 +451,8 @@ public class DefaultDashChunkSource implements DashChunkSource {
int trackSelectionReason, int trackSelectionReason,
Object trackSelectionData, Object trackSelectionData,
long firstSegmentNum, long firstSegmentNum,
int maxSegmentCount) { int maxSegmentCount,
long seekTimeUs) {
Representation representation = representationHolder.representation; Representation representation = representationHolder.representation;
long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum); long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum);
RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum); RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum);
@ -469,9 +479,19 @@ public class DefaultDashChunkSource implements DashChunkSource {
DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl), DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl),
segmentUri.start, segmentUri.length, representation.getCacheKey()); segmentUri.start, segmentUri.length, representation.getCacheKey());
long sampleOffsetUs = -representation.presentationTimeOffsetUs; long sampleOffsetUs = -representation.presentationTimeOffsetUs;
return new ContainerMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, return new ContainerMediaChunk(
trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, segmentCount, dataSource,
sampleOffsetUs, representationHolder.extractorWrapper); dataSpec,
trackFormat,
trackSelectionReason,
trackSelectionData,
startTimeUs,
endTimeUs,
seekTimeUs,
firstSegmentNum,
segmentCount,
sampleOffsetUs,
representationHolder.extractorWrapper);
} }
} }

View File

@ -203,6 +203,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex); long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex);
long chunkEndTimeUs = chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex); long chunkEndTimeUs = chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex);
long chunkSeekTimeUs = previous == null ? loadPositionUs : C.TIME_UNSET;
int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset; int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset;
int trackSelectionIndex = trackSelection.getSelectedIndex(); int trackSelectionIndex = trackSelection.getSelectedIndex();
@ -211,9 +212,19 @@ public class DefaultSsChunkSource implements SsChunkSource {
int manifestTrackIndex = trackSelection.getIndexInTrackGroup(trackSelectionIndex); int manifestTrackIndex = trackSelection.getIndexInTrackGroup(trackSelectionIndex);
Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex); Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex);
out.chunk = newMediaChunk(trackSelection.getSelectedFormat(), dataSource, uri, null, out.chunk =
currentAbsoluteChunkIndex, chunkStartTimeUs, chunkEndTimeUs, newMediaChunk(
trackSelection.getSelectionReason(), trackSelection.getSelectionData(), extractorWrapper); trackSelection.getSelectedFormat(),
dataSource,
uri,
null,
currentAbsoluteChunkIndex,
chunkStartTimeUs,
chunkEndTimeUs,
chunkSeekTimeUs,
trackSelection.getSelectionReason(),
trackSelection.getSelectionData(),
extractorWrapper);
} }
@Override @Override
@ -229,15 +240,34 @@ public class DefaultSsChunkSource implements SsChunkSource {
// Private methods. // Private methods.
private static MediaChunk newMediaChunk(Format format, DataSource dataSource, Uri uri, private static MediaChunk newMediaChunk(
String cacheKey, int chunkIndex, long chunkStartTimeUs, long chunkEndTimeUs, Format format,
int trackSelectionReason, Object trackSelectionData, ChunkExtractorWrapper extractorWrapper) { DataSource dataSource,
Uri uri,
String cacheKey,
int chunkIndex,
long chunkStartTimeUs,
long chunkEndTimeUs,
long chunkSeekTimeUs,
int trackSelectionReason,
Object trackSelectionData,
ChunkExtractorWrapper extractorWrapper) {
DataSpec dataSpec = new DataSpec(uri, 0, C.LENGTH_UNSET, cacheKey); DataSpec dataSpec = new DataSpec(uri, 0, C.LENGTH_UNSET, cacheKey);
// In SmoothStreaming each chunk contains sample timestamps relative to the start of the chunk. // 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. // To convert them the absolute timestamps, we need to set sampleOffsetUs to chunkStartTimeUs.
long sampleOffsetUs = chunkStartTimeUs; long sampleOffsetUs = chunkStartTimeUs;
return new ContainerMediaChunk(dataSource, dataSpec, format, trackSelectionReason, return new ContainerMediaChunk(
trackSelectionData, chunkStartTimeUs, chunkEndTimeUs, chunkIndex, 1, sampleOffsetUs, dataSource,
dataSpec,
format,
trackSelectionReason,
trackSelectionData,
chunkStartTimeUs,
chunkEndTimeUs,
chunkSeekTimeUs,
chunkIndex,
/* chunkCount= */ 1,
sampleOffsetUs,
extractorWrapper); extractorWrapper);
} }