Ignore sequence numbers when switching variants in HLS

According to the spec, there is no mandatory relation between segments'
sequence numbers of different variants. This CL ignores sequence numbers
when switching variants:

* In vod, the switching playback position is obtained by adding the
    duration of previous segments.
* In live playback this is not possible. It is assumed that the
    different live media playlists do not drift apart too much, so
    the playback position is obtained by subtracting the duration
    in reverse order.

In later CLs, the described mechanisms will become the fallback methods
by replacing them with the use of EXT-X-PROGRAM-DATE-TIME information
or more reliable sources.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=128072663
This commit is contained in:
aquilescanta 2016-07-21 10:00:52 -07:00 committed by Oliver Woodman
parent 98c5b2b8d0
commit 68156ac7a4
4 changed files with 137 additions and 29 deletions

View File

@ -88,6 +88,15 @@ public abstract class Chunk implements Loadable {
this.endTimeUs = endTimeUs;
}
/**
* Gets the duration of the chunk in microseconds.
*
* @return The duration of the chunk in microseconds.
*/
public final long getDurationUs() {
return endTimeUs - startTimeUs;
}
/**
* Gets the number of bytes that have been loaded.
*

View File

@ -59,7 +59,11 @@ public class HlsChunkSource {
* The default time for which a media playlist should be blacklisted.
*/
public static final long DEFAULT_PLAYLIST_BLACKLIST_MS = 60000;
/**
* Subtracted value to lookup position when switching between variants in live streams to avoid
* gaps in playback in case playlist drift apart.
*/
private static final double LIVE_VARIANT_SWITCH_SAFETY_EXTRA_SECS = 2.0;
private static final String TAG = "HlsChunkSource";
private static final String AAC_FILE_EXTENSION = ".aac";
private static final String MP3_FILE_EXTENSION = ".mp3";
@ -220,33 +224,41 @@ public class HlsChunkSource {
* @param out A holder to populate.
*/
public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, ChunkHolder out) {
int previousChunkVariantIndex =
previous != null ? getVariantIndex(previous.format) : -1;
updateFormatEvaluation(previous, playbackPositionUs);
int variantIndex = 0;
for (int i = 0; i < variants.length; i++) {
if (variants[i].format == evaluation.format) {
variantIndex = i;
break;
}
}
boolean switchingVariant = previous != null
&& variants[variantIndex].format != previous.format;
HlsMediaPlaylist mediaPlaylist = variantPlaylists[variantIndex];
int newVariantIndex = getVariantIndex(evaluation.format);
boolean switchingVariant = previousChunkVariantIndex != newVariantIndex;
HlsMediaPlaylist mediaPlaylist = variantPlaylists[newVariantIndex];
if (mediaPlaylist == null) {
// We don't have the media playlist for the next variant. Request it now.
out.chunk = newMediaPlaylistChunk(variantIndex, evaluation.trigger, evaluation.data);
out.chunk = newMediaPlaylistChunk(newVariantIndex, evaluation.trigger, evaluation.data);
return;
}
int chunkMediaSequence;
if (previous == null) {
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs,
true, true) + mediaPlaylist.mediaSequence;
if (live) {
if (previous == null) {
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs,
true, true) + mediaPlaylist.mediaSequence;
} else {
chunkMediaSequence = getLiveNextChunkSequenceNumber(previous.chunkIndex,
previousChunkVariantIndex, newVariantIndex);
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
fatalError = new BehindLiveWindowException();
return;
}
}
} else {
chunkMediaSequence = switchingVariant ? previous.chunkIndex : previous.chunkIndex + 1;
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
fatalError = new BehindLiveWindowException();
return;
// Not live.
if (previous == null) {
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs,
true, true) + mediaPlaylist.mediaSequence;
} else if (switchingVariant) {
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments,
previous.startTimeUs, true, true) + mediaPlaylist.mediaSequence;
} else {
chunkMediaSequence = previous.getNextChunkIndex();
}
}
@ -254,8 +266,8 @@ public class HlsChunkSource {
if (chunkIndex >= mediaPlaylist.segments.size()) {
if (!mediaPlaylist.live) {
out.endOfStream = true;
} else if (shouldRerequestLiveMediaPlaylist(variantIndex)) {
out.chunk = newMediaPlaylistChunk(variantIndex, evaluation.trigger, evaluation.data);
} else if (shouldRerequestLiveMediaPlaylist(newVariantIndex)) {
out.chunk = newMediaPlaylistChunk(newVariantIndex, evaluation.trigger, evaluation.data);
}
return;
}
@ -268,7 +280,7 @@ public class HlsChunkSource {
Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri);
if (!keyUri.equals(encryptionKeyUri)) {
// Encryption is specified and the key has changed.
out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, variantIndex,
out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, newVariantIndex,
evaluation.trigger, evaluation.data);
return;
}
@ -289,15 +301,15 @@ public class HlsChunkSource {
if (previous == null) {
startTimeUs = 0;
} else if (switchingVariant) {
startTimeUs = previous.startTimeUs;
startTimeUs = previous.getAdjustedStartTimeUs();
} else {
startTimeUs = previous.endTimeUs;
startTimeUs = previous.getAdjustedEndTimeUs();
}
} else /* Not live */ {
startTimeUs = segment.startTimeUs;
}
long endTimeUs = startTimeUs + (long) (segment.durationSecs * C.MICROS_PER_SECOND);
Format format = variants[variantIndex].format;
Format format = variants[newVariantIndex].format;
// Configure the extractor that will read the chunk.
Extractor extractor;
@ -332,7 +344,7 @@ public class HlsChunkSource {
return;
}
int workaroundFlags = 0;
String codecs = variants[variantIndex].codecs;
String codecs = variants[newVariantIndex].codecs;
if (!TextUtils.isEmpty(codecs)) {
// Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
// exist. If we know from the codec attribute that they don't exist, then we can explicitly
@ -356,6 +368,48 @@ public class HlsChunkSource {
extractorNeedsInit, switchingVariant, encryptionKey, encryptionIv);
}
/**
* Returns the media sequence number of a chunk in a new variant for a live stream variant switch.
*
* @param previousChunkIndex The index of the last chunk in the old variant.
* @param oldVariantIndex The index of the old variant.
* @param newVariantIndex The index of the new variant.
* @return Media sequence number of the chunk to switch to in a live stream in the variant that
* corresponds to the given {@code newVariantIndex}.
*/
private int getLiveNextChunkSequenceNumber(int previousChunkIndex, int oldVariantIndex,
int newVariantIndex) {
if (oldVariantIndex == newVariantIndex) {
return previousChunkIndex + 1;
}
HlsMediaPlaylist oldMediaPlaylist = variantPlaylists[oldVariantIndex];
HlsMediaPlaylist newMediaPlaylist = variantPlaylists[newVariantIndex];
double offsetToLiveInstantSecs = 0;
for (int i = previousChunkIndex - oldMediaPlaylist.mediaSequence;
i < oldMediaPlaylist.segments.size(); i++) {
offsetToLiveInstantSecs += oldMediaPlaylist.segments.get(i).durationSecs;
}
long currentTimeMs = SystemClock.elapsedRealtime();
offsetToLiveInstantSecs +=
(double) (currentTimeMs - variantLastPlaylistLoadTimesMs[oldVariantIndex]) / 1000;
offsetToLiveInstantSecs += LIVE_VARIANT_SWITCH_SAFETY_EXTRA_SECS;
offsetToLiveInstantSecs -=
(double) (currentTimeMs - variantLastPlaylistLoadTimesMs[newVariantIndex]) / 1000;
if (offsetToLiveInstantSecs < 0) {
// The instant we are looking for is not contained in the playlist, we need it to be
// refreshed.
return newMediaPlaylist.mediaSequence + newMediaPlaylist.segments.size() + 1;
}
for (int i = newMediaPlaylist.segments.size() - 1; i >= 0; i--) {
offsetToLiveInstantSecs -= newMediaPlaylist.segments.get(i).durationSecs;
if (offsetToLiveInstantSecs < 0) {
return newMediaPlaylist.mediaSequence + i;
}
}
// We have fallen behind the live window.
return newMediaPlaylist.mediaSequence - 1;
}
/**
* Invoked when the {@link HlsSampleStreamWrapper} has finished loading a chunk obtained from this
* source.
@ -484,7 +538,7 @@ public class HlsChunkSource {
if (previous != null) {
// Use start time of the previous chunk rather than its end time because switching format
// will require downloading overlapping segments.
bufferedDurationUs = Math.max(0, previous.startTimeUs - playbackPositionUs);
bufferedDurationUs = Math.max(0, previous.getAdjustedStartTimeUs() - playbackPositionUs);
} else {
bufferedDurationUs = 0;
}
@ -580,6 +634,16 @@ public class HlsChunkSource {
throw new IllegalStateException("Invalid format: " + format);
}
private int getVariantIndex(Format format) {
for (int i = 0; i < variants.length; i++) {
if (variants[i].format == format) {
return i;
}
}
// Should never happen.
throw new IllegalStateException("Invalid format: " + format);
}
// Private classes.
private static final class MediaPlaylistChunk extends DataChunk {

View File

@ -54,6 +54,8 @@ import java.util.concurrent.atomic.AtomicInteger;
private final boolean shouldSpliceIn;
private int bytesLoaded;
private HlsSampleStreamWrapper extractorOutput;
private long adjustedEndTimeUs;
private volatile boolean loadCanceled;
private volatile boolean loadCompleted;
@ -65,7 +67,7 @@ import java.util.concurrent.atomic.AtomicInteger;
* @param formatEvaluatorData See {@link #formatEvaluatorData}.
* @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 chunkIndex The media sequence number of the chunk.
* @param discontinuitySequenceNumber The discontinuity sequence number of the chunk.
* @param extractor The extractor to decode samples from the data.
* @param extractorNeedsInit Whether the extractor needs initializing with the target
@ -87,6 +89,7 @@ import java.util.concurrent.atomic.AtomicInteger;
this.extractorNeedsInit = extractorNeedsInit;
this.shouldSpliceIn = shouldSpliceIn;
// Note: this.dataSource and dataSource may be different.
adjustedEndTimeUs = startTimeUs;
this.isEncrypted = this.dataSource instanceof Aes128DataSource;
uid = UID_SOURCE.getAndIncrement();
}
@ -98,12 +101,31 @@ import java.util.concurrent.atomic.AtomicInteger;
* @param output The output that will receive the loaded samples.
*/
public void init(HlsSampleStreamWrapper output) {
extractorOutput = output;
output.init(uid, shouldSpliceIn);
if (extractorNeedsInit) {
extractor.init(output);
}
}
/**
* Gets the start time in microseconds by subtracting the duration from the adjusted end time.
*
* @return The start time in microseconds.
*/
public long getAdjustedStartTimeUs() {
return adjustedEndTimeUs - getDurationUs();
}
/**
* Gets the presentation time in microseconds of the last sample contained in the chunk
*
* @return The presentation time in microseconds of the last sample contained in the chunk.
*/
public long getAdjustedEndTimeUs() {
return adjustedEndTimeUs;
}
@Override
public boolean isLoadCompleted() {
return loadCompleted;
@ -152,6 +174,10 @@ import java.util.concurrent.atomic.AtomicInteger;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
result = extractor.read(input, null);
}
long adjustedEndTimeUs = extractorOutput.getLargestQueuedTimestampUs();
if (adjustedEndTimeUs != Long.MIN_VALUE) {
this.adjustedEndTimeUs = adjustedEndTimeUs;
}
} finally {
bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
}

View File

@ -239,6 +239,15 @@ import java.util.List;
loader.release();
}
public long getLargestQueuedTimestampUs() {
long largestQueuedTimestampUs = Long.MIN_VALUE;
for (int i = 0; i < sampleQueues.size(); i++) {
largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs,
sampleQueues.valueAt(i).getLargestQueuedTimestampUs());
}
return largestQueuedTimestampUs;
}
// SampleStream implementation.
/* package */ boolean isReady(int group) {