Track HLS discontinuities when playlist does not declare sequence

This is an initial version that does not handle cross-playlists
adjustment in an ideal way.

Issue:#1789

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=144692969
This commit is contained in:
aquilescanta 2017-01-17 03:58:15 -08:00 committed by Oliver Woodman
parent 7d7a159195
commit 6e481178ea
5 changed files with 136 additions and 96 deletions

View file

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls.playlist;
import android.net.Uri;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
@ -73,59 +74,64 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals(2679, mediaPlaylist.mediaSequence);
assertEquals(3, mediaPlaylist.version);
assertEquals(true, mediaPlaylist.hasEndTag);
List<HlsMediaPlaylist.Segment> segments = mediaPlaylist.segments;
assertTrue(mediaPlaylist.hasEndTag);
List<Segment> segments = mediaPlaylist.segments;
assertNotNull(segments);
assertEquals(5, segments.size());
assertEquals(4, segments.get(0).discontinuitySequenceNumber);
assertEquals(7975000, segments.get(0).durationUs);
assertEquals(false, segments.get(0).isEncrypted);
assertEquals(null, segments.get(0).encryptionKeyUri);
assertEquals(null, segments.get(0).encryptionIV);
assertEquals(51370, segments.get(0).byterangeLength);
assertEquals(0, segments.get(0).byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2679.ts", segments.get(0).url);
Segment segment = segments.get(0);
assertEquals(4, mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence);
assertEquals(7975000, segment.durationUs);
assertFalse(segment.isEncrypted);
assertEquals(null, segment.encryptionKeyUri);
assertEquals(null, segment.encryptionIV);
assertEquals(51370, segment.byterangeLength);
assertEquals(0, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2679.ts", segment.url);
assertEquals(4, segments.get(1).discontinuitySequenceNumber);
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);
assertEquals(51501, segments.get(1).byterangeLength);
assertEquals(2147483648L, segments.get(1).byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2680.ts", segments.get(1).url);
segment = segments.get(1);
assertEquals(0, segment.relativeDiscontinuitySequence);
assertEquals(7975000, segment.durationUs);
assertTrue(segment.isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2680", segment.encryptionKeyUri);
assertEquals("0x1566B", segment.encryptionIV);
assertEquals(51501, segment.byterangeLength);
assertEquals(2147483648L, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2680.ts", segment.url);
assertEquals(4, segments.get(2).discontinuitySequenceNumber);
assertEquals(7941000, segments.get(2).durationUs);
assertEquals(false, segments.get(2).isEncrypted);
assertEquals(null, segments.get(2).encryptionKeyUri);
assertEquals(null, segments.get(2).encryptionIV);
assertEquals(51501, segments.get(2).byterangeLength);
assertEquals(2147535149L, segments.get(2).byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2681.ts", segments.get(2).url);
segment = segments.get(2);
assertEquals(0, segment.relativeDiscontinuitySequence);
assertEquals(7941000, segment.durationUs);
assertFalse(segment.isEncrypted);
assertEquals(null, segment.encryptionKeyUri);
assertEquals(null, segment.encryptionIV);
assertEquals(51501, segment.byterangeLength);
assertEquals(2147535149L, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2681.ts", segment.url);
assertEquals(5, segments.get(3).discontinuitySequenceNumber);
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);
segment = segments.get(3);
assertEquals(1, segment.relativeDiscontinuitySequence);
assertEquals(7975000, segment.durationUs);
assertTrue(segment.isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri);
// 0xA7A == 2682.
assertNotNull(segments.get(3).encryptionIV);
assertEquals("A7A", segments.get(3).encryptionIV.toUpperCase(Locale.getDefault()));
assertEquals(51740, segments.get(3).byterangeLength);
assertEquals(2147586650L, segments.get(3).byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2682.ts", segments.get(3).url);
assertNotNull(segment.encryptionIV);
assertEquals("A7A", segment.encryptionIV.toUpperCase(Locale.getDefault()));
assertEquals(51740, segment.byterangeLength);
assertEquals(2147586650L, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2682.ts", segment.url);
assertEquals(5, segments.get(4).discontinuitySequenceNumber);
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);
segment = segments.get(4);
assertEquals(1, segment.relativeDiscontinuitySequence);
assertEquals(7975000, segment.durationUs);
assertTrue(segment.isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri);
// 0xA7B == 2683.
assertNotNull(segments.get(4).encryptionIV);
assertEquals("A7B", segments.get(4).encryptionIV.toUpperCase(Locale.getDefault()));
assertEquals(C.LENGTH_UNSET, segments.get(4).byterangeLength);
assertEquals(0, segments.get(4).byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2683.ts", segments.get(4).url);
assertNotNull(segment.encryptionIV);
assertEquals("A7B", segment.encryptionIV.toUpperCase(Locale.getDefault()));
assertEquals(C.LENGTH_UNSET, segment.byterangeLength);
assertEquals(0, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2683.ts", segment.url);
} catch (IOException exception) {
fail(exception.getMessage());
}

View file

@ -270,8 +270,10 @@ import java.util.Locale;
// Compute start time of the next chunk.
long startTimeUs = mediaPlaylist.startTimeUs + segment.relativeStartTimeUs;
int discontinuitySequence = mediaPlaylist.discontinuitySequence
+ segment.relativeDiscontinuitySequence;
TimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster(
segment.discontinuitySequenceNumber, startTimeUs);
discontinuitySequence, startTimeUs);
// Configure the data source and spec for the chunk.
Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url);
@ -279,9 +281,8 @@ import java.util.Locale;
null);
out.chunk = new HlsMediaChunk(dataSource, dataSpec, initDataSpec, variants[newVariantIndex],
trackSelection.getSelectionReason(), trackSelection.getSelectionData(),
startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence,
segment.discontinuitySequenceNumber, isTimestampMaster, timestampAdjuster, previous,
encryptionKey, encryptionIv);
startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, discontinuitySequence,
isTimestampMaster, timestampAdjuster, previous, encryptionKey, encryptionIv);
}
/**

View file

@ -31,7 +31,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public final String url;
public final long durationUs;
public final int discontinuitySequenceNumber;
public final int relativeDiscontinuitySequence;
public final long relativeStartTimeUs;
public final boolean isEncrypted;
public final String encryptionKeyUri;
@ -43,12 +43,12 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
this(uri, 0, -1, C.TIME_UNSET, false, null, null, byterangeOffset, byterangeLength);
}
public Segment(String uri, long durationUs, int discontinuitySequenceNumber,
public Segment(String uri, long durationUs, int relativeDiscontinuitySequence,
long relativeStartTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV,
long byterangeOffset, long byterangeLength) {
this.url = uri;
this.durationUs = durationUs;
this.discontinuitySequenceNumber = discontinuitySequenceNumber;
this.relativeDiscontinuitySequence = relativeDiscontinuitySequence;
this.relativeStartTimeUs = relativeStartTimeUs;
this.isEncrypted = isEncrypted;
this.encryptionKeyUri = encryptionKeyUri;
@ -67,6 +67,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public final long startOffsetUs;
public final long startTimeUs;
public final boolean hasDiscontinuitySequence;
public final int discontinuitySequence;
public final int mediaSequence;
public final int version;
public final long targetDurationUs;
@ -76,11 +78,14 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
public final List<Segment> segments;
public final long durationUs;
public HlsMediaPlaylist(String baseUri, long startOffsetUs, long startTimeUs, int mediaSequence,
int version, long targetDurationUs, boolean hasEndTag, boolean hasProgramDateTime,
public HlsMediaPlaylist(String baseUri, long startOffsetUs, long startTimeUs,
boolean hasDiscontinuitySequence, int discontinuitySequence, int mediaSequence, int version,
long targetDurationUs, boolean hasEndTag, boolean hasProgramDateTime,
Segment initializationSegment, List<Segment> segments) {
super(baseUri, HlsPlaylist.TYPE_MEDIA);
this.startTimeUs = startTimeUs;
this.hasDiscontinuitySequence = hasDiscontinuitySequence;
this.discontinuitySequence = discontinuitySequence;
this.mediaSequence = mediaSequence;
this.version = version;
this.targetDurationUs = targetDurationUs;
@ -123,19 +128,18 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
}
/**
* Returns a playlist identical to this one except for the start time, which is set to the
* specified value. If the start time already equals the specified value then the playlist will
* return itself.
* Returns a playlist identical to this one except for the start time, the discontinuity sequence
* and {@code hasDiscontinuitySequence} values. The first two are set to the specified values,
* {@code hasDiscontinuitySequence} is set to true.
*
* @param startTimeUs The start time for the returned playlist.
* @param discontinuitySequence The discontinuity sequence for the returned playlist.
* @return The playlist.
*/
public HlsMediaPlaylist copyWithStartTimeUs(long startTimeUs) {
if (this.startTimeUs == startTimeUs) {
return this;
}
return new HlsMediaPlaylist(baseUri, startOffsetUs, startTimeUs, mediaSequence, version,
targetDurationUs, hasEndTag, hasProgramDateTime, initializationSegment, segments);
public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) {
return new HlsMediaPlaylist(baseUri, startOffsetUs, startTimeUs, true, discontinuitySequence,
mediaSequence, version, targetDurationUs, hasEndTag, hasProgramDateTime,
initializationSegment, segments);
}
/**
@ -148,8 +152,9 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
if (this.hasEndTag) {
return this;
}
return new HlsMediaPlaylist(baseUri, startOffsetUs, startTimeUs, mediaSequence, version,
targetDurationUs, true, hasProgramDateTime, initializationSegment, segments);
return new HlsMediaPlaylist(baseUri, startOffsetUs, startTimeUs, hasDiscontinuitySequence,
discontinuitySequence, mediaSequence, version, targetDurationUs, true, hasProgramDateTime,
initializationSegment, segments);
}
}

View file

@ -266,7 +266,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
List<Segment> segments = new ArrayList<>();
long segmentDurationUs = 0;
int discontinuitySequenceNumber = 0;
boolean hasDiscontinuitySequence = false;
int playlistDiscontinuitySequence = 0;
int relativeDiscontinuitySequence = 0;
long playlistStartTimeUs = 0;
long segmentStartTimeUs = 0;
long segmentByteRangeOffset = 0;
@ -323,9 +325,10 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);
}
} else if (line.startsWith(TAG_DISCONTINUITY_SEQUENCE)) {
discontinuitySequenceNumber = Integer.parseInt(line.substring(line.indexOf(':') + 1));
hasDiscontinuitySequence = true;
playlistDiscontinuitySequence = Integer.parseInt(line.substring(line.indexOf(':') + 1));
} else if (line.equals(TAG_DISCONTINUITY)) {
discontinuitySequenceNumber++;
relativeDiscontinuitySequence++;
} else if (line.startsWith(TAG_PROGRAM_DATE_TIME)) {
if (playlistStartTimeUs == 0) {
long programDatetimeUs =
@ -345,7 +348,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
if (segmentByteRangeLength == C.LENGTH_UNSET) {
segmentByteRangeOffset = 0;
}
segments.add(new Segment(line, segmentDurationUs, discontinuitySequenceNumber,
segments.add(new Segment(line, segmentDurationUs, relativeDiscontinuitySequence,
segmentStartTimeUs, isEncrypted, encryptionKeyUri, segmentEncryptionIV,
segmentByteRangeOffset, segmentByteRangeLength));
segmentStartTimeUs += segmentDurationUs;
@ -358,7 +361,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
hasEndTag = true;
}
}
return new HlsMediaPlaylist(baseUri, startOffsetUs, playlistStartTimeUs, mediaSequence, version,
return new HlsMediaPlaylist(baseUri, startOffsetUs, playlistStartTimeUs,
hasDiscontinuitySequence, playlistDiscontinuitySequence, mediaSequence, version,
targetDurationUs, hasEndTag, playlistStartTimeUs != 0, initializationSegment, segments);
}

View file

@ -334,47 +334,71 @@ public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable
}
}
// TODO: Track discontinuities for media playlists that don't include the discontinuity number.
private HlsMediaPlaylist getLatestPlaylistSnapshot(HlsMediaPlaylist oldPlaylist,
HlsMediaPlaylist loadedPlaylist) {
if (loadedPlaylist.hasProgramDateTime) {
if (loadedPlaylist.isNewerThan(oldPlaylist)) {
return loadedPlaylist;
} else {
if (!loadedPlaylist.isNewerThan(oldPlaylist)) {
if (loadedPlaylist.hasEndTag) {
// If the loaded playlist has an end tag but is not newer than the old playlist then we have
// an inconsistent state. This is typically caused by the server incorrectly resetting the
// media sequence when appending the end tag. We resolve this case as best we can by
// returning the old playlist with the end tag appended.
return loadedPlaylist.hasEndTag ? oldPlaylist.copyWithEndTag() : oldPlaylist;
return oldPlaylist.copyWithEndTag();
} else {
return oldPlaylist;
}
}
// TODO: Once playlist type support is added, the snapshot's age can be added by using the
// target duration.
long startTimeUs = getLoadedPlaylistStartTimeUs(oldPlaylist, loadedPlaylist);
int discontinuitySequence = getLoadedPlaylistDiscontinuitySequence(oldPlaylist, loadedPlaylist);
return loadedPlaylist.copyWith(startTimeUs, discontinuitySequence);
}
private long getLoadedPlaylistStartTimeUs(HlsMediaPlaylist oldPlaylist,
HlsMediaPlaylist loadedPlaylist) {
if (loadedPlaylist.hasProgramDateTime) {
return loadedPlaylist.startTimeUs;
}
long primarySnapshotStartTimeUs = primaryUrlSnapshot != null
? primaryUrlSnapshot.startTimeUs : 0;
if (oldPlaylist == null) {
if (loadedPlaylist.startTimeUs == primarySnapshotStartTimeUs) {
// Playback has just started or is VOD so no adjustment is needed.
return loadedPlaylist;
} else {
return loadedPlaylist.copyWithStartTimeUs(primarySnapshotStartTimeUs);
}
return primarySnapshotStartTimeUs;
}
if (!loadedPlaylist.isNewerThan(oldPlaylist)) {
// See comment above.
return loadedPlaylist.hasEndTag ? oldPlaylist.copyWithEndTag() : oldPlaylist;
int oldPlaylistSize = oldPlaylist.segments.size();
Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist);
if (firstOldOverlappingSegment != null) {
return oldPlaylist.startTimeUs + firstOldOverlappingSegment.relativeStartTimeUs;
} else if (oldPlaylistSize == loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence) {
return oldPlaylist.getEndTimeUs();
} else {
// No segments overlap, we assume the new playlist start coincides with the primary playlist.
return primarySnapshotStartTimeUs;
}
List<Segment> oldSegments = oldPlaylist.segments;
int oldPlaylistSize = oldSegments.size();
}
private int getLoadedPlaylistDiscontinuitySequence(HlsMediaPlaylist oldPlaylist,
HlsMediaPlaylist loadedPlaylist) {
if (loadedPlaylist.hasDiscontinuitySequence) {
return loadedPlaylist.discontinuitySequence;
}
// TODO: Improve cross-playlist discontinuity adjustment.
int primaryUrlDiscontinuitySequence = primaryUrlSnapshot != null
? primaryUrlSnapshot.discontinuitySequence : 0;
if (oldPlaylist == null) {
return primaryUrlDiscontinuitySequence;
}
Segment firstOldOverlappingSegment = getFirstOldOverlappingSegment(oldPlaylist, loadedPlaylist);
if (firstOldOverlappingSegment != null) {
return oldPlaylist.discontinuitySequence
+ firstOldOverlappingSegment.relativeDiscontinuitySequence
- loadedPlaylist.segments.get(0).relativeDiscontinuitySequence;
}
return primaryUrlDiscontinuitySequence;
}
private static Segment getFirstOldOverlappingSegment(HlsMediaPlaylist oldPlaylist,
HlsMediaPlaylist loadedPlaylist) {
int mediaSequenceOffset = loadedPlaylist.mediaSequence - oldPlaylist.mediaSequence;
if (mediaSequenceOffset <= oldPlaylistSize) {
long adjustedNewPlaylistStartTimeUs = mediaSequenceOffset == oldPlaylistSize
? oldPlaylist.getEndTimeUs()
: oldPlaylist.startTimeUs + oldSegments.get(mediaSequenceOffset).relativeStartTimeUs;
return loadedPlaylist.copyWithStartTimeUs(adjustedNewPlaylistStartTimeUs);
}
// No segments overlap, we assume the new playlist start coincides with the primary playlist.
return loadedPlaylist.copyWithStartTimeUs(primarySnapshotStartTimeUs);
List<Segment> oldSegments = oldPlaylist.segments;
return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset) : null;
}
/**