Apply SeekParameters to DASH + SmoothStreaming playbacks

Issue: #2882

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=181314086
This commit is contained in:
olly 2018-01-09 06:52:54 -08:00 committed by Oliver Woodman
parent 4ee971052b
commit ff1bb2f702
13 changed files with 213 additions and 46 deletions

View File

@ -27,8 +27,7 @@
performed. The `SeekParameters` class contains defaults for exact seeking and
seeking to the closest sync points before, either side or after specified seek
positions.
* Note: `SeekParameters` are only currently effective when playing
`ExtractorMediaSource`s (i.e. progressive streams).
* Note: `SeekParameters` are not currently supported when playing HLS streams.
* DASH: Support DASH manifest EventStream elements.
* HLS: Add opt-in support for chunkless preparation in HLS. This allows an
HLS source to finish preparation without downloading any chunks, which can

View File

@ -69,4 +69,22 @@ public final class SeekParameters {
this.toleranceBeforeUs = toleranceBeforeUs;
this.toleranceAfterUs = toleranceAfterUs;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
SeekParameters other = (SeekParameters) obj;
return toleranceBeforeUs == other.toleranceBeforeUs
&& toleranceAfterUs == other.toleranceAfterUs;
}
@Override
public int hashCode() {
return (31 * (int) toleranceBeforeUs) + (int) toleranceAfterUs;
}
}

View File

@ -378,28 +378,8 @@ import java.util.Arrays;
return 0;
}
SeekPoints seekPoints = seekMap.getSeekPoints(positionUs);
long minPositionUs =
Util.subtractWithOverflowDefault(
positionUs, seekParameters.toleranceBeforeUs, Long.MIN_VALUE);
long maxPositionUs =
Util.addWithOverflowDefault(positionUs, seekParameters.toleranceAfterUs, Long.MAX_VALUE);
long firstPointUs = seekPoints.first.timeUs;
boolean firstPointValid = minPositionUs <= firstPointUs && firstPointUs <= maxPositionUs;
long secondPointUs = seekPoints.second.timeUs;
boolean secondPointValid = minPositionUs <= secondPointUs && secondPointUs <= maxPositionUs;
if (firstPointValid && secondPointValid) {
if (Math.abs(firstPointUs - positionUs) <= Math.abs(secondPointUs - positionUs)) {
return firstPointUs;
} else {
return secondPointUs;
}
} else if (firstPointValid) {
return firstPointUs;
} else if (secondPointValid) {
return secondPointUs;
} else {
return minPositionUs;
}
return Util.resolveSeekPositionUs(
positionUs, seekParameters, seekPoints.first.timeUs, seekPoints.second.timeUs);
}
// SampleStream methods.

View File

@ -19,6 +19,7 @@ import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.SampleQueue;
@ -42,7 +43,8 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
private static final String TAG = "ChunkSampleStream";
private final int primaryTrackType;
public final int primaryTrackType;
private final int[] embeddedTrackTypes;
private final boolean[] embeddedTracksSelected;
private final T chunkSource;
@ -180,6 +182,21 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
}
}
/**
* Adjusts a seek position given the specified {@link SeekParameters}. Chunk boundaries are used
* as sync points.
*
* @param positionUs The seek position in microseconds.
* @param seekParameters Parameters that control how the seek is performed.
* @return The adjusted seek position, in microseconds.
*/
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
// TODO: Using this method to adjust a seek position and then passing the adjusted position to
// seekToUs does not handle small discrepancies between the chunk boundary timestamps obtained
// from the chunk source and the timestamps of the samples in the chunks.
return chunkSource.getAdjustedSeekPositionUs(positionUs, seekParameters);
}
/**
* Seeks to the specified position in microseconds.
*

View File

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.source.chunk;
import com.google.android.exoplayer2.SeekParameters;
import java.io.IOException;
import java.util.List;
@ -23,6 +24,16 @@ import java.util.List;
*/
public interface ChunkSource {
/**
* Adjusts a seek position given the specified {@link SeekParameters}. Chunk boundaries are used
* as sync points.
*
* @param positionUs The seek position in microseconds.
* @param seekParameters Parameters that control how the seek is performed.
* @return The adjusted seek position, in microseconds.
*/
long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters);
/**
* If the source is currently having difficulty providing chunks, then this method throws the
* underlying error. Otherwise does nothing.

View File

@ -34,6 +34,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.upstream.DataSource;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
@ -762,6 +763,44 @@ public final class Util {
return Math.round((double) mediaDuration / speed);
}
/**
* Resolves a seek given the requested seek position, a {@link SeekParameters} and two candidate
* sync points.
*
* @param positionUs The requested seek position, in microseocnds.
* @param seekParameters The {@link SeekParameters}.
* @param firstSyncUs The first candidate seek point, in micrseconds.
* @param secondSyncUs The second candidate seek point, in microseconds. May equal {@code
* firstSyncUs} if there's only one candidate.
* @return The resolved seek position, in microseconds.
*/
public static long resolveSeekPositionUs(
long positionUs, SeekParameters seekParameters, long firstSyncUs, long secondSyncUs) {
if (SeekParameters.EXACT.equals(seekParameters)) {
return positionUs;
}
long minPositionUs =
subtractWithOverflowDefault(positionUs, seekParameters.toleranceBeforeUs, Long.MIN_VALUE);
long maxPositionUs =
addWithOverflowDefault(positionUs, seekParameters.toleranceAfterUs, Long.MAX_VALUE);
boolean firstSyncPositionValid = minPositionUs <= firstSyncUs && firstSyncUs <= maxPositionUs;
boolean secondSyncPositionValid =
minPositionUs <= secondSyncUs && secondSyncUs <= maxPositionUs;
if (firstSyncPositionValid && secondSyncPositionValid) {
if (Math.abs(firstSyncUs - positionUs) <= Math.abs(secondSyncUs - positionUs)) {
return firstSyncUs;
} else {
return secondSyncUs;
}
} else if (firstSyncPositionValid) {
return firstSyncUs;
} else if (secondSyncPositionValid) {
return secondSyncUs;
} else {
return minPositionUs;
}
}
/**
* Converts a list of integers to a primitive array.
*

View File

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.source.dash;
import android.os.SystemClock;
import com.google.android.exoplayer2.source.chunk.ChunkSource;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.trackselection.TrackSelection;
@ -25,15 +26,40 @@ import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
*/
public interface DashChunkSource extends ChunkSource {
/** Factory for {@link DashChunkSource}s. */
interface Factory {
DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower,
DashManifest manifest, int periodIndex, int[] adaptationSetIndices,
TrackSelection trackSelection, int type, long elapsedRealtimeOffsetMs,
boolean enableEventMessageTrack, boolean enableCea608Track);
/**
* @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.
* @param manifest The initial manifest.
* @param periodIndex The index of the corresponding period in the manifest.
* @param adaptationSetIndices The indices of the corresponding adaptation sets in the period.
* @param trackSelection The track selection.
* @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 enableEventMessageTrack Whether the chunks generated by the source may output an event
* message track.
* @param enableCea608Track Whether the chunks generated by the source may output a CEA-608
* track.
* @return The created {@link DashChunkSource}.
*/
DashChunkSource createDashChunkSource(
LoaderErrorThrower manifestLoaderErrorThrower,
DashManifest manifest,
int periodIndex,
int[] adaptationSetIndices,
TrackSelection trackSelection,
int type,
long elapsedRealtimeOffsetMs,
boolean enableEventMessageTrack,
boolean enableCea608Track);
}
/**
* Updates the manifest.
*
* @param newManifest The new manifest.
*/
void updateManifest(DashManifest newManifest, int periodIndex);
}

View File

@ -309,6 +309,11 @@ import java.util.Map;
@Override
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
for (ChunkSampleStream<DashChunkSource> sampleStream : sampleStreams) {
if (sampleStream.primaryTrackType == C.TRACK_TYPE_VIDEO) {
return sampleStream.getAdjustedSeekPositionUs(positionUs, seekParameters);
}
}
return positionUs;
}

View File

@ -19,6 +19,7 @@ import android.net.Uri;
import android.os.SystemClock;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.extractor.ChunkIndex;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.SeekMap;
@ -142,6 +143,24 @@ public class DefaultDashChunkSource implements DashChunkSource {
}
}
@Override
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
// Segments are aligned across representations, so any segment index will do.
for (RepresentationHolder representationHolder : representationHolders) {
if (representationHolder.segmentIndex != null) {
int segmentNum = representationHolder.getSegmentNum(positionUs);
long firstSyncUs = representationHolder.getSegmentStartTimeUs(segmentNum);
long secondSyncUs =
firstSyncUs < positionUs && segmentNum < representationHolder.getSegmentCount() - 1
? representationHolder.getSegmentStartTimeUs(segmentNum + 1)
: firstSyncUs;
return Util.resolveSeekPositionUs(positionUs, seekParameters, firstSyncUs, secondSyncUs);
}
}
// We don't have a segment index to adjust the seek position with yet.
return positionUs;
}
@Override
public void updateManifest(DashManifest newManifest, int newPeriodIndex) {
try {

View File

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.smoothstreaming;
import android.net.Uri;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer2.extractor.mp4.Track;
import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox;
@ -34,6 +35,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.List;
@ -62,7 +64,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
}
private final LoaderErrorThrower manifestLoaderErrorThrower;
private final int elementIndex;
private final int streamElementIndex;
private final TrackSelection trackSelection;
private final ChunkExtractorWrapper[] extractorWrappers;
private final DataSource dataSource;
@ -75,22 +77,25 @@ public class DefaultSsChunkSource implements SsChunkSource {
/**
* @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.
* @param manifest The initial manifest.
* @param elementIndex The index of the stream element in the manifest.
* @param streamElementIndex The index of the stream element in the manifest.
* @param trackSelection The track selection.
* @param dataSource A {@link DataSource} suitable for loading the media data.
* @param trackEncryptionBoxes Track encryption boxes for the stream.
*/
public DefaultSsChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, SsManifest manifest,
int elementIndex, TrackSelection trackSelection, DataSource dataSource,
public DefaultSsChunkSource(
LoaderErrorThrower manifestLoaderErrorThrower,
SsManifest manifest,
int streamElementIndex,
TrackSelection trackSelection,
DataSource dataSource,
TrackEncryptionBox[] trackEncryptionBoxes) {
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
this.manifest = manifest;
this.elementIndex = elementIndex;
this.streamElementIndex = streamElementIndex;
this.trackSelection = trackSelection;
this.dataSource = dataSource;
StreamElement streamElement = manifest.streamElements[elementIndex];
StreamElement streamElement = manifest.streamElements[streamElementIndex];
extractorWrappers = new ChunkExtractorWrapper[trackSelection.length()];
for (int i = 0; i < extractorWrappers.length; i++) {
int manifestTrackIndex = trackSelection.getIndexInTrackGroup(i);
@ -106,11 +111,23 @@ public class DefaultSsChunkSource implements SsChunkSource {
}
}
@Override
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
StreamElement streamElement = manifest.streamElements[streamElementIndex];
int chunkIndex = streamElement.getChunkIndex(positionUs);
long firstSyncUs = streamElement.getStartTimeUs(chunkIndex);
long secondSyncUs =
firstSyncUs < positionUs && chunkIndex < streamElement.chunkCount - 1
? streamElement.getStartTimeUs(chunkIndex + 1)
: firstSyncUs;
return Util.resolveSeekPositionUs(positionUs, seekParameters, firstSyncUs, secondSyncUs);
}
@Override
public void updateManifest(SsManifest newManifest) {
StreamElement currentElement = manifest.streamElements[elementIndex];
StreamElement currentElement = manifest.streamElements[streamElementIndex];
int currentElementChunkCount = currentElement.chunkCount;
StreamElement newElement = newManifest.streamElements[elementIndex];
StreamElement newElement = newManifest.streamElements[streamElementIndex];
if (currentElementChunkCount == 0 || newElement.chunkCount == 0) {
// There's no overlap between the old and new elements because at least one is empty.
currentManifestChunkOffset += currentElementChunkCount;
@ -155,7 +172,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
return;
}
StreamElement streamElement = manifest.streamElements[elementIndex];
StreamElement streamElement = manifest.streamElements[streamElementIndex];
if (streamElement.chunkCount == 0) {
// There aren't any chunks for us to load.
out.endOfStream = !manifest.isLive;
@ -229,7 +246,7 @@ public class DefaultSsChunkSource implements SsChunkSource {
return C.TIME_UNSET;
}
StreamElement currentElement = manifest.streamElements[elementIndex];
StreamElement currentElement = manifest.streamElements[streamElementIndex];
int lastChunkIndex = currentElement.chunkCount - 1;
long lastChunkEndTimeUs = currentElement.getStartTimeUs(lastChunkIndex)
+ currentElement.getChunkDurationUs(lastChunkIndex);

View File

@ -26,14 +26,31 @@ import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
*/
public interface SsChunkSource extends ChunkSource {
/** Factory for {@link SsChunkSource}s. */
interface Factory {
SsChunkSource createChunkSource(LoaderErrorThrower manifestLoaderErrorThrower,
SsManifest manifest, int elementIndex, TrackSelection trackSelection,
/**
* Creates a new {@link SsChunkSource}.
*
* @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.
* @param manifest The initial manifest.
* @param streamElementIndex The index of the corresponding stream element in the manifest.
* @param trackSelection The track selection.
* @param trackEncryptionBoxes Track encryption boxes for the stream.
* @return The created {@link SsChunkSource}.
*/
SsChunkSource createChunkSource(
LoaderErrorThrower manifestLoaderErrorThrower,
SsManifest manifest,
int streamElementIndex,
TrackSelection trackSelection,
TrackEncryptionBox[] trackEncryptionBoxes);
}
/**
* Updates the manifest.
*
* @param newManifest The new manifest.
*/
void updateManifest(SsManifest newManifest);
}

View File

@ -185,6 +185,11 @@ import java.util.ArrayList;
@Override
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
for (ChunkSampleStream<SsChunkSource> sampleStream : sampleStreams) {
if (sampleStream.primaryTrackType == C.TRACK_TYPE_VIDEO) {
return sampleStream.getAdjustedSeekPositionUs(positionUs, seekParameters);
}
}
return positionUs;
}

View File

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.testutil;
import android.net.Uri;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.source.chunk.Chunk;
import com.google.android.exoplayer2.source.chunk.ChunkHolder;
import com.google.android.exoplayer2.source.chunk.ChunkSource;
@ -28,6 +29,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.List;
@ -71,6 +74,17 @@ public final class FakeChunkSource implements ChunkSource {
this.dataSet = dataSet;
}
@Override
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
int chunkIndex = dataSet.getChunkIndexByPosition(positionUs);
long firstSyncUs = dataSet.getStartTime(chunkIndex);
long secondSyncUs =
firstSyncUs < positionUs && chunkIndex < dataSet.getChunkCount() - 1
? dataSet.getStartTime(chunkIndex + 1)
: firstSyncUs;
return Util.resolveSeekPositionUs(positionUs, seekParameters, firstSyncUs, secondSyncUs);
}
@Override
public void maybeThrowError() throws IOException {
// Do nothing.