mirror of
https://github.com/androidx/media.git
synced 2025-05-04 14:10:40 +08:00
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:
parent
98c5b2b8d0
commit
68156ac7a4
@ -88,6 +88,15 @@ public abstract class Chunk implements Loadable {
|
|||||||
this.endTimeUs = endTimeUs;
|
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.
|
* Gets the number of bytes that have been loaded.
|
||||||
*
|
*
|
||||||
|
@ -59,7 +59,11 @@ public class HlsChunkSource {
|
|||||||
* The default time for which a media playlist should be blacklisted.
|
* The default time for which a media playlist should be blacklisted.
|
||||||
*/
|
*/
|
||||||
public static final long DEFAULT_PLAYLIST_BLACKLIST_MS = 60000;
|
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 TAG = "HlsChunkSource";
|
||||||
private static final String AAC_FILE_EXTENSION = ".aac";
|
private static final String AAC_FILE_EXTENSION = ".aac";
|
||||||
private static final String MP3_FILE_EXTENSION = ".mp3";
|
private static final String MP3_FILE_EXTENSION = ".mp3";
|
||||||
@ -220,42 +224,50 @@ public class HlsChunkSource {
|
|||||||
* @param out A holder to populate.
|
* @param out A holder to populate.
|
||||||
*/
|
*/
|
||||||
public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, ChunkHolder out) {
|
public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, ChunkHolder out) {
|
||||||
|
int previousChunkVariantIndex =
|
||||||
|
previous != null ? getVariantIndex(previous.format) : -1;
|
||||||
updateFormatEvaluation(previous, playbackPositionUs);
|
updateFormatEvaluation(previous, playbackPositionUs);
|
||||||
int variantIndex = 0;
|
int newVariantIndex = getVariantIndex(evaluation.format);
|
||||||
for (int i = 0; i < variants.length; i++) {
|
boolean switchingVariant = previousChunkVariantIndex != newVariantIndex;
|
||||||
if (variants[i].format == evaluation.format) {
|
HlsMediaPlaylist mediaPlaylist = variantPlaylists[newVariantIndex];
|
||||||
variantIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
boolean switchingVariant = previous != null
|
|
||||||
&& variants[variantIndex].format != previous.format;
|
|
||||||
|
|
||||||
HlsMediaPlaylist mediaPlaylist = variantPlaylists[variantIndex];
|
|
||||||
if (mediaPlaylist == null) {
|
if (mediaPlaylist == null) {
|
||||||
// We don't have the media playlist for the next variant. Request it now.
|
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int chunkMediaSequence;
|
int chunkMediaSequence;
|
||||||
|
if (live) {
|
||||||
if (previous == null) {
|
if (previous == null) {
|
||||||
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs,
|
chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs,
|
||||||
true, true) + mediaPlaylist.mediaSequence;
|
true, true) + mediaPlaylist.mediaSequence;
|
||||||
} else {
|
} else {
|
||||||
chunkMediaSequence = switchingVariant ? previous.chunkIndex : previous.chunkIndex + 1;
|
chunkMediaSequence = getLiveNextChunkSequenceNumber(previous.chunkIndex,
|
||||||
|
previousChunkVariantIndex, newVariantIndex);
|
||||||
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
|
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
|
||||||
fatalError = new BehindLiveWindowException();
|
fatalError = new BehindLiveWindowException();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence;
|
int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence;
|
||||||
if (chunkIndex >= mediaPlaylist.segments.size()) {
|
if (chunkIndex >= mediaPlaylist.segments.size()) {
|
||||||
if (!mediaPlaylist.live) {
|
if (!mediaPlaylist.live) {
|
||||||
out.endOfStream = true;
|
out.endOfStream = true;
|
||||||
} else if (shouldRerequestLiveMediaPlaylist(variantIndex)) {
|
} else if (shouldRerequestLiveMediaPlaylist(newVariantIndex)) {
|
||||||
out.chunk = newMediaPlaylistChunk(variantIndex, evaluation.trigger, evaluation.data);
|
out.chunk = newMediaPlaylistChunk(newVariantIndex, evaluation.trigger, evaluation.data);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -268,7 +280,7 @@ public class HlsChunkSource {
|
|||||||
Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri);
|
Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri);
|
||||||
if (!keyUri.equals(encryptionKeyUri)) {
|
if (!keyUri.equals(encryptionKeyUri)) {
|
||||||
// Encryption is specified and the key has changed.
|
// 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);
|
evaluation.trigger, evaluation.data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -289,15 +301,15 @@ public class HlsChunkSource {
|
|||||||
if (previous == null) {
|
if (previous == null) {
|
||||||
startTimeUs = 0;
|
startTimeUs = 0;
|
||||||
} else if (switchingVariant) {
|
} else if (switchingVariant) {
|
||||||
startTimeUs = previous.startTimeUs;
|
startTimeUs = previous.getAdjustedStartTimeUs();
|
||||||
} else {
|
} else {
|
||||||
startTimeUs = previous.endTimeUs;
|
startTimeUs = previous.getAdjustedEndTimeUs();
|
||||||
}
|
}
|
||||||
} else /* Not live */ {
|
} else /* Not live */ {
|
||||||
startTimeUs = segment.startTimeUs;
|
startTimeUs = segment.startTimeUs;
|
||||||
}
|
}
|
||||||
long endTimeUs = startTimeUs + (long) (segment.durationSecs * C.MICROS_PER_SECOND);
|
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.
|
// Configure the extractor that will read the chunk.
|
||||||
Extractor extractor;
|
Extractor extractor;
|
||||||
@ -332,7 +344,7 @@ public class HlsChunkSource {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int workaroundFlags = 0;
|
int workaroundFlags = 0;
|
||||||
String codecs = variants[variantIndex].codecs;
|
String codecs = variants[newVariantIndex].codecs;
|
||||||
if (!TextUtils.isEmpty(codecs)) {
|
if (!TextUtils.isEmpty(codecs)) {
|
||||||
// Sometimes AAC and H264 streams are declared in TS chunks even though they don't really
|
// 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
|
// 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);
|
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
|
* Invoked when the {@link HlsSampleStreamWrapper} has finished loading a chunk obtained from this
|
||||||
* source.
|
* source.
|
||||||
@ -484,7 +538,7 @@ public class HlsChunkSource {
|
|||||||
if (previous != null) {
|
if (previous != null) {
|
||||||
// Use start time of the previous chunk rather than its end time because switching format
|
// Use start time of the previous chunk rather than its end time because switching format
|
||||||
// will require downloading overlapping segments.
|
// will require downloading overlapping segments.
|
||||||
bufferedDurationUs = Math.max(0, previous.startTimeUs - playbackPositionUs);
|
bufferedDurationUs = Math.max(0, previous.getAdjustedStartTimeUs() - playbackPositionUs);
|
||||||
} else {
|
} else {
|
||||||
bufferedDurationUs = 0;
|
bufferedDurationUs = 0;
|
||||||
}
|
}
|
||||||
@ -580,6 +634,16 @@ public class HlsChunkSource {
|
|||||||
throw new IllegalStateException("Invalid format: " + format);
|
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 classes.
|
||||||
|
|
||||||
private static final class MediaPlaylistChunk extends DataChunk {
|
private static final class MediaPlaylistChunk extends DataChunk {
|
||||||
|
@ -54,6 +54,8 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||||||
private final boolean shouldSpliceIn;
|
private final boolean shouldSpliceIn;
|
||||||
|
|
||||||
private int bytesLoaded;
|
private int bytesLoaded;
|
||||||
|
private HlsSampleStreamWrapper extractorOutput;
|
||||||
|
private long adjustedEndTimeUs;
|
||||||
private volatile boolean loadCanceled;
|
private volatile boolean loadCanceled;
|
||||||
private volatile boolean loadCompleted;
|
private volatile boolean loadCompleted;
|
||||||
|
|
||||||
@ -65,7 +67,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||||||
* @param formatEvaluatorData See {@link #formatEvaluatorData}.
|
* @param formatEvaluatorData See {@link #formatEvaluatorData}.
|
||||||
* @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 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 discontinuitySequenceNumber The discontinuity sequence number of the chunk.
|
||||||
* @param extractor The extractor to decode samples from the data.
|
* @param extractor The extractor to decode samples from the data.
|
||||||
* @param extractorNeedsInit Whether the extractor needs initializing with the target
|
* @param extractorNeedsInit Whether the extractor needs initializing with the target
|
||||||
@ -87,6 +89,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||||||
this.extractorNeedsInit = extractorNeedsInit;
|
this.extractorNeedsInit = extractorNeedsInit;
|
||||||
this.shouldSpliceIn = shouldSpliceIn;
|
this.shouldSpliceIn = shouldSpliceIn;
|
||||||
// Note: this.dataSource and dataSource may be different.
|
// Note: this.dataSource and dataSource may be different.
|
||||||
|
adjustedEndTimeUs = startTimeUs;
|
||||||
this.isEncrypted = this.dataSource instanceof Aes128DataSource;
|
this.isEncrypted = this.dataSource instanceof Aes128DataSource;
|
||||||
uid = UID_SOURCE.getAndIncrement();
|
uid = UID_SOURCE.getAndIncrement();
|
||||||
}
|
}
|
||||||
@ -98,12 +101,31 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||||||
* @param output The output that will receive the loaded samples.
|
* @param output The output that will receive the loaded samples.
|
||||||
*/
|
*/
|
||||||
public void init(HlsSampleStreamWrapper output) {
|
public void init(HlsSampleStreamWrapper output) {
|
||||||
|
extractorOutput = output;
|
||||||
output.init(uid, shouldSpliceIn);
|
output.init(uid, shouldSpliceIn);
|
||||||
if (extractorNeedsInit) {
|
if (extractorNeedsInit) {
|
||||||
extractor.init(output);
|
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
|
@Override
|
||||||
public boolean isLoadCompleted() {
|
public boolean isLoadCompleted() {
|
||||||
return loadCompleted;
|
return loadCompleted;
|
||||||
@ -152,6 +174,10 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||||||
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
|
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
|
||||||
result = extractor.read(input, null);
|
result = extractor.read(input, null);
|
||||||
}
|
}
|
||||||
|
long adjustedEndTimeUs = extractorOutput.getLargestQueuedTimestampUs();
|
||||||
|
if (adjustedEndTimeUs != Long.MIN_VALUE) {
|
||||||
|
this.adjustedEndTimeUs = adjustedEndTimeUs;
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
|
bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
|
||||||
}
|
}
|
||||||
|
@ -239,6 +239,15 @@ import java.util.List;
|
|||||||
loader.release();
|
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.
|
// SampleStream implementation.
|
||||||
|
|
||||||
/* package */ boolean isReady(int group) {
|
/* package */ boolean isReady(int group) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user