Feed timestamps from loaded chunks back to the playlist tracker

This is the first step towards allowing discontinuities in the
playlist tracking. Also changed durationSecs for durationUs in
MediaPlaylist.Segment.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=138207732
This commit is contained in:
aquilescanta 2016-11-04 10:48:14 -07:00 committed by Oliver Woodman
parent a5a2bc89f4
commit 992cfdecc2
6 changed files with 84 additions and 34 deletions

View file

@ -72,7 +72,6 @@ public class HlsMediaPlaylistParserTest extends TestCase {
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
assertEquals(2679, mediaPlaylist.mediaSequence);
assertEquals(8, mediaPlaylist.targetDurationSecs);
assertEquals(3, mediaPlaylist.version);
assertEquals(true, mediaPlaylist.hasEndTag);
List<HlsMediaPlaylist.Segment> segments = mediaPlaylist.segments;
@ -80,7 +79,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals(5, segments.size());
assertEquals(4, segments.get(0).discontinuitySequenceNumber);
assertEquals(7.975, segments.get(0).durationSecs);
assertEquals(7975000, segments.get(0).durationUs);
assertEquals(false, segments.get(0).isEncrypted);
assertEquals(null, segments.get(0).encryptionKeyUri);
assertEquals(null, segments.get(0).encryptionIV);
@ -89,7 +88,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals("https://priv.example.com/fileSequence2679.ts", segments.get(0).url);
assertEquals(4, segments.get(1).discontinuitySequenceNumber);
assertEquals(7.975, segments.get(1).durationSecs);
assertEquals(7975000, segments.get(1).durationUs);
assertEquals(true, segments.get(1).isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2680", segments.get(1).encryptionKeyUri);
assertEquals("0x1566B", segments.get(1).encryptionIV);
@ -98,7 +97,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals("https://priv.example.com/fileSequence2680.ts", segments.get(1).url);
assertEquals(4, segments.get(2).discontinuitySequenceNumber);
assertEquals(7.941, segments.get(2).durationSecs);
assertEquals(7941000, segments.get(2).durationUs);
assertEquals(false, segments.get(2).isEncrypted);
assertEquals(null, segments.get(2).encryptionKeyUri);
assertEquals(null, segments.get(2).encryptionIV);
@ -107,7 +106,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals("https://priv.example.com/fileSequence2681.ts", segments.get(2).url);
assertEquals(5, segments.get(3).discontinuitySequenceNumber);
assertEquals(7.975, segments.get(3).durationSecs);
assertEquals(7975000, segments.get(3).durationUs);
assertEquals(true, segments.get(3).isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2682", segments.get(3).encryptionKeyUri);
// 0xA7A == 2682.
@ -118,7 +117,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals("https://priv.example.com/fileSequence2682.ts", segments.get(3).url);
assertEquals(5, segments.get(4).discontinuitySequenceNumber);
assertEquals(7.975, segments.get(4).durationSecs);
assertEquals(7975000, segments.get(4).durationUs);
assertEquals(true, segments.get(4).isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2682", segments.get(4).encryptionKeyUri);
// 0xA7B == 2683.

View file

@ -267,7 +267,7 @@ import java.util.Locale;
if (previous != null && !switchingVariant) {
startTimeUs = previous.getAdjustedEndTimeUs();
}
long endTimeUs = startTimeUs + (long) (segment.durationSecs * C.MICROS_PER_SECOND);
long endTimeUs = startTimeUs + segment.durationUs;
Format format = variants[newVariantIndex].format;
Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url);
@ -322,7 +322,7 @@ import java.util.Locale;
// This flag ensures the change of pid between streams does not affect the sample queues.
@DefaultTsPayloadReaderFactory.Flags
int esReaderFactoryFlags = 0;
String codecs = variants[newVariantIndex].format.codecs;
String codecs = format.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
@ -353,7 +353,7 @@ import java.util.Locale;
// Configure the data source and spec for the chunk.
DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength,
null);
out.chunk = new HlsMediaChunk(dataSource, dataSpec, format,
out.chunk = new HlsMediaChunk(dataSource, dataSpec, variants[newVariantIndex],
trackSelection.getSelectionReason(), trackSelection.getSelectionData(),
startTimeUs, endTimeUs, chunkMediaSequence, segment.discontinuitySequenceNumber,
isTimestampMaster, timestampAdjuster, extractor, extractorNeedsInit, switchingVariant,
@ -367,6 +367,11 @@ import java.util.Locale;
* @param chunk The chunk whose load has been completed.
*/
public void onChunkLoadCompleted(Chunk chunk) {
if (chunk instanceof HlsMediaChunk) {
HlsMediaChunk mediaChunk = (HlsMediaChunk) chunk;
playlistTracker.onChunkLoaded(mediaChunk.hlsUrl, mediaChunk.chunkIndex,
mediaChunk.getAdjustedStartTimeUs());
}
if (chunk instanceof HlsInitializationChunk) {
lastLoadedInitializationChunk = (HlsInitializationChunk) chunk;
} else if (chunk instanceof EncryptionKeyChunk) {

View file

@ -15,12 +15,12 @@
*/
package com.google.android.exoplayer2.source.hls;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Util;
@ -49,6 +49,11 @@ import java.util.concurrent.atomic.AtomicInteger;
*/
public final Extractor extractor;
/**
* The url of the playlist from which this chunk was obtained.
*/
public final HlsUrl hlsUrl;
private final boolean isEncrypted;
private final boolean extractorNeedsInit;
private final boolean shouldSpliceIn;
@ -64,7 +69,7 @@ import java.util.concurrent.atomic.AtomicInteger;
/**
* @param dataSource The source from which the data should be loaded.
* @param dataSpec Defines the data to be loaded.
* @param trackFormat See {@link #trackFormat}.
* @param hlsUrl The url of the playlist from which this chunk was obtained.
* @param trackSelectionReason See {@link #trackSelectionReason}.
* @param trackSelectionData See {@link #trackSelectionData}.
* @param startTimeUs The start time of the media contained by the chunk, in microseconds.
@ -81,13 +86,14 @@ import java.util.concurrent.atomic.AtomicInteger;
* @param encryptionKey For AES encryption chunks, the encryption key.
* @param encryptionIv For AES encryption chunks, the encryption initialization vector.
*/
public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat,
public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, HlsUrl hlsUrl,
int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs,
int chunkIndex, int discontinuitySequenceNumber, boolean isMasterTimestampSource,
TimestampAdjuster timestampAdjuster, Extractor extractor, boolean extractorNeedsInit,
boolean shouldSpliceIn, byte[] encryptionKey, byte[] encryptionIv) {
super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, trackFormat,
super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format,
trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex);
this.hlsUrl = hlsUrl;
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
this.isMasterTimestampSource = isMasterTimestampSource;
this.timestampAdjuster = timestampAdjuster;

View file

@ -31,7 +31,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public static final class Segment implements Comparable<Long> {
public final String url;
public final double durationSecs;
public final long durationUs;
public final int discontinuitySequenceNumber;
public final long startTimeUs;
public final boolean isEncrypted;
@ -44,11 +44,11 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
this(uri, 0, -1, C.TIME_UNSET, false, null, null, byterangeOffset, byterangeLength);
}
public Segment(String uri, double durationSecs, int discontinuitySequenceNumber,
public Segment(String uri, long durationUs, int discontinuitySequenceNumber,
long startTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV,
long byterangeOffset, long byterangeLength) {
this.url = uri;
this.durationSecs = durationSecs;
this.durationUs = durationUs;
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
this.startTimeUs = startTimeUs;
this.isEncrypted = isEncrypted;
@ -64,28 +64,23 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
}
public Segment copyWithStartTimeUs(long startTimeUs) {
return new Segment(url, durationSecs, discontinuitySequenceNumber, startTimeUs, isEncrypted,
return new Segment(url, durationUs, discontinuitySequenceNumber, startTimeUs, isEncrypted,
encryptionKeyUri, encryptionIV, byterangeOffset, byterangeLength);
}
}
public static final String ENCRYPTION_METHOD_NONE = "NONE";
public static final String ENCRYPTION_METHOD_AES_128 = "AES-128";
public final int mediaSequence;
public final int targetDurationSecs;
public final int version;
public final Segment initializationSegment;
public final List<Segment> segments;
public final boolean hasEndTag;
public final long durationUs;
public HlsMediaPlaylist(String baseUri, int mediaSequence, int targetDurationSecs, int version,
public HlsMediaPlaylist(String baseUri, int mediaSequence, int version,
boolean hasEndTag, Segment initializationSegment, List<Segment> segments) {
super(baseUri, HlsPlaylist.TYPE_MEDIA);
this.mediaSequence = mediaSequence;
this.targetDurationSecs = targetDurationSecs;
this.version = version;
this.hasEndTag = hasEndTag;
this.initializationSegment = initializationSegment;
@ -94,8 +89,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
if (!segments.isEmpty()) {
Segment first = segments.get(0);
Segment last = segments.get(segments.size() - 1);
durationUs = last.startTimeUs + (long) (last.durationSecs * C.MICROS_PER_SECOND)
- first.startTimeUs;
durationUs = last.startTimeUs + last.durationUs - first.startTimeUs;
} else {
durationUs = 0;
}
@ -121,7 +115,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
}
public HlsMediaPlaylist copyWithSegments(List<Segment> segments) {
return new HlsMediaPlaylist(baseUri, mediaSequence, targetDurationSecs, version, hasEndTag,
return new HlsMediaPlaylist(baseUri, mediaSequence, version, hasEndTag,
initializationSegment, segments);
}

View file

@ -217,7 +217,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
Segment initializationSegment = null;
List<Segment> segments = new ArrayList<>();
double segmentDurationSecs = 0.0;
long segmentDurationUs = 0;
int discontinuitySequenceNumber = 0;
long segmentStartTimeUs = 0;
long segmentByteRangeOffset = 0;
@ -252,7 +252,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} else if (line.startsWith(TAG_VERSION)) {
version = parseIntAttr(line, REGEX_VERSION);
} else if (line.startsWith(TAG_MEDIA_DURATION)) {
segmentDurationSecs = parseDoubleAttr(line, REGEX_MEDIA_DURATION);
segmentDurationUs =
(long) (parseDoubleAttr(line, REGEX_MEDIA_DURATION) * C.MICROS_PER_SECOND);
} else if (line.startsWith(TAG_KEY)) {
String method = parseStringAttr(line, REGEX_METHOD);
isEncrypted = METHOD_AES128.equals(method);
@ -287,11 +288,11 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
if (segmentByteRangeLength == C.LENGTH_UNSET) {
segmentByteRangeOffset = 0;
}
segments.add(new Segment(line, segmentDurationSecs, discontinuitySequenceNumber,
segments.add(new Segment(line, segmentDurationUs, discontinuitySequenceNumber,
segmentStartTimeUs, isEncrypted, encryptionKeyUri, segmentEncryptionIV,
segmentByteRangeOffset, segmentByteRangeLength));
segmentStartTimeUs += (long) (segmentDurationSecs * C.MICROS_PER_SECOND);
segmentDurationSecs = 0.0;
segmentStartTimeUs += segmentDurationUs;
segmentDurationUs = 0;
if (segmentByteRangeLength != C.LENGTH_UNSET) {
segmentByteRangeOffset += segmentByteRangeLength;
}
@ -300,7 +301,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
hasEndTag = true;
}
}
return new HlsMediaPlaylist(baseUri, mediaSequence, targetDurationSecs, version, hasEndTag,
return new HlsMediaPlaylist(baseUri, mediaSequence, version, hasEndTag,
initializationSegment, segments);
}

View file

@ -21,6 +21,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
@ -62,6 +63,12 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
}
/**
* Determines the minimum amount of time by which a media playlist segment's start time has to
* drift from the actual start time of the chunk it refers to for it to be adjusted.
*/
private static final long TIMESTAMP_ADJUSTMENT_THRESHOLD_US = 500000;
/**
* Period for refreshing playlists.
*/
@ -182,6 +189,17 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
return isLive;
}
/**
* Called when a chunk from a media playlist is loaded.
*
* @param hlsUrl The url of the playlist from which the chunk was obtained.
* @param chunkMediaSequence The media sequence number of the loaded chunk.
* @param adjustedStartTimeUs The adjusted start time of the loaded chunk.
*/
public void onChunkLoaded(HlsUrl hlsUrl, int chunkMediaSequence, long adjustedStartTimeUs) {
playlistBundles.get(hlsUrl).adjustTimestampsOfPlaylist(chunkMediaSequence, adjustedStartTimeUs);
}
// Loader.Callback implementation.
@Override
@ -262,7 +280,6 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
}
/**
* TODO: Allow chunks to feed adjusted timestamps back to the playlist tracker.
* TODO: Track discontinuities for media playlists that don't include the discontinuity number.
*/
private HlsMediaPlaylist adjustPlaylistTimestamps(HlsMediaPlaylist oldPlaylist,
@ -293,7 +310,7 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
HlsMediaPlaylist.Segment lastSegment = oldSegments.get(oldPlaylistSize - 1);
for (int i = newPlaylistSize - newSegmentsCount; i < newPlaylistSize; i++) {
lastSegment = newPlaylist.segments.get(i).copyWithStartTimeUs(
lastSegment.startTimeUs + (long) lastSegment.durationSecs * C.MICROS_PER_SECOND);
lastSegment.startTimeUs + lastSegment.durationUs);
newSegments.add(lastSegment);
}
return newPlaylist.copyWithSegments(newSegments);
@ -343,6 +360,34 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
this.callback = callback;
}
public void adjustTimestampsOfPlaylist(int chunkMediaSequence, long adjustedStartTimeUs) {
ArrayList<Segment> segments = new ArrayList<>(latestPlaylistSnapshot.segments);
int indexOfChunk = chunkMediaSequence - latestPlaylistSnapshot.mediaSequence;
if (indexOfChunk < 0) {
return;
}
Segment actualSegment = segments.get(indexOfChunk);
long timestampDriftUs = Math.abs(actualSegment.startTimeUs - adjustedStartTimeUs);
if (timestampDriftUs < TIMESTAMP_ADJUSTMENT_THRESHOLD_US) {
return;
}
segments.set(indexOfChunk, actualSegment.copyWithStartTimeUs(adjustedStartTimeUs));
// Propagate the adjustment backwards.
for (int i = indexOfChunk - 1; i >= 0; i--) {
Segment segment = segments.get(i);
segments.set(i,
segment.copyWithStartTimeUs(segments.get(i + 1).startTimeUs - segment.durationUs));
}
// Propagate the adjustment forward.
int segmentsSize = segments.size();
for (int i = indexOfChunk + 1; i < segmentsSize; i++) {
Segment segment = segments.get(i);
segments.set(i,
segment.copyWithStartTimeUs(segments.get(i - 1).startTimeUs + segment.durationUs));
}
latestPlaylistSnapshot = latestPlaylistSnapshot.copyWithSegments(segments);
}
// Loader.Callback implementation.
@Override