Skip to content

Commit

Permalink
Track HLS discontinuities when playlist does not declare sequence
Browse files Browse the repository at this point in the history
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
  • Loading branch information
AquilesCanta authored and ojw28 committed Jan 17, 2017
1 parent 7d7a159 commit 6e48117
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 96 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

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;
Expand Down Expand Up @@ -73,59 +74,64 @@ public void testParseMediaPlaylist() {

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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,18 +270,19 @@ public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChu

// 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);
DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength,
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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public static final class Segment implements Comparable<Long> {

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;
Expand All @@ -43,12 +43,12 @@ public Segment(String uri, long byterangeOffset, long byterangeLength) {
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;
Expand All @@ -67,6 +67,8 @@ public int compareTo(Long relativeStartTimeUs) {

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;
Expand All @@ -76,11 +78,14 @@ public int compareTo(Long relativeStartTimeUs) {
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;
Expand Down Expand Up @@ -123,19 +128,18 @@ public long getEndTimeUs() {
}

/**
* 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);
}

/**
Expand All @@ -148,8 +152,9 @@ public HlsMediaPlaylist copyWithEndTag() {
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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,9 @@ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String
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;
Expand Down Expand Up @@ -323,9 +325,10 @@ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String
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 =
Expand All @@ -345,7 +348,7 @@ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String
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;
Expand All @@ -358,7 +361,8 @@ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String
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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,47 +334,71 @@ private void notifyPlaylistBlacklisting(HlsUrl url, long blacklistMs) {
}
}

// 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();
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);
}

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;
}
// No segments overlap, we assume the new playlist start coincides with the primary playlist.
return loadedPlaylist.copyWithStartTimeUs(primarySnapshotStartTimeUs);
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;
List<Segment> oldSegments = oldPlaylist.segments;
return mediaSequenceOffset < oldSegments.size() ? oldSegments.get(mediaSequenceOffset) : null;
}

/**
Expand Down

0 comments on commit 6e48117

Please sign in to comment.