Merge pull request #8767 from uvjustin:hls-start-from-independent-part

PiperOrigin-RevId: 373343326
This commit is contained in:
Oliver Woodman 2021-05-12 13:54:44 +01:00
commit e20ea797ef
5 changed files with 383 additions and 91 deletions

View File

@ -41,6 +41,9 @@
* Ad playback: * Ad playback:
* Support changing ad break positions in the player logic * Support changing ad break positions in the player logic
([#5067](https://github.com/google/ExoPlayer/issues/5067). ([#5067](https://github.com/google/ExoPlayer/issues/5067).
* HLS
* Use the PRECISE attribute in EXT-X-START to select the default start
position.
### 2.14.0 (2021-05-13) ### 2.14.0 (2021-05-13)

View File

@ -503,7 +503,6 @@ public final class HlsMediaSource extends BaseMediaSource
@Override @Override
public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) { public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) {
SinglePeriodTimeline timeline;
long windowStartTimeMs = long windowStartTimeMs =
playlist.hasProgramDateTime ? C.usToMs(playlist.startTimeUs) : C.TIME_UNSET; playlist.hasProgramDateTime ? C.usToMs(playlist.startTimeUs) : C.TIME_UNSET;
// For playlist types EVENT and VOD we know segments are never removed, so the presentation // For playlist types EVENT and VOD we know segments are never removed, so the presentation
@ -513,34 +512,43 @@ public final class HlsMediaSource extends BaseMediaSource
|| playlist.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD || playlist.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD
? windowStartTimeMs ? windowStartTimeMs
: C.TIME_UNSET; : C.TIME_UNSET;
long windowDefaultStartPositionUs = playlist.startOffsetUs; // The master playlist is non-null because the first playlist has been fetched by now.
// masterPlaylist is non-null because the first playlist has been fetched by now.
HlsManifest manifest = HlsManifest manifest =
new HlsManifest(checkNotNull(playlistTracker.getMasterPlaylist()), playlist); new HlsManifest(checkNotNull(playlistTracker.getMasterPlaylist()), playlist);
if (playlistTracker.isLive()) { SinglePeriodTimeline timeline =
long liveEdgeOffsetUs = getLiveEdgeOffsetUs(playlist); playlistTracker.isLive()
long targetLiveOffsetUs = ? createTimelineForLive(playlist, presentationStartTimeMs, windowStartTimeMs, manifest)
liveConfiguration.targetOffsetMs != C.TIME_UNSET : createTimelineForOnDemand(
? C.msToUs(liveConfiguration.targetOffsetMs) playlist, presentationStartTimeMs, windowStartTimeMs, manifest);
: getTargetLiveOffsetUs(playlist, liveEdgeOffsetUs); refreshSourceInfo(timeline);
// Ensure target live offset is within the live window and greater than the live edge offset. }
targetLiveOffsetUs =
Util.constrainValue(
targetLiveOffsetUs, liveEdgeOffsetUs, playlist.durationUs + liveEdgeOffsetUs);
maybeUpdateMediaItem(targetLiveOffsetUs);
private SinglePeriodTimeline createTimelineForLive(
HlsMediaPlaylist playlist,
long presentationStartTimeMs,
long windowStartTimeMs,
HlsManifest manifest) {
long offsetFromInitialStartTimeUs = long offsetFromInitialStartTimeUs =
playlist.startTimeUs - playlistTracker.getInitialStartTimeUs(); playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
long periodDurationUs = long periodDurationUs =
playlist.hasEndTag ? offsetFromInitialStartTimeUs + playlist.durationUs : C.TIME_UNSET; playlist.hasEndTag ? offsetFromInitialStartTimeUs + playlist.durationUs : C.TIME_UNSET;
List<HlsMediaPlaylist.Segment> segments = playlist.segments; long liveEdgeOffsetUs = getLiveEdgeOffsetUs(playlist);
if (!segments.isEmpty()) { long targetLiveOffsetUs;
windowDefaultStartPositionUs = getWindowDefaultStartPosition(playlist, liveEdgeOffsetUs); if (liveConfiguration.targetOffsetMs != C.TIME_UNSET) {
} else if (windowDefaultStartPositionUs == C.TIME_UNSET) { // Media item has a defined target offset.
windowDefaultStartPositionUs = 0; targetLiveOffsetUs = C.msToUs(liveConfiguration.targetOffsetMs);
} else {
// Decide target offset from playlist.
targetLiveOffsetUs = getTargetLiveOffsetUs(playlist, liveEdgeOffsetUs);
} }
timeline = // Ensure target live offset is within the live window and greater than the live edge offset.
new SinglePeriodTimeline( targetLiveOffsetUs =
Util.constrainValue(
targetLiveOffsetUs, liveEdgeOffsetUs, playlist.durationUs + liveEdgeOffsetUs);
maybeUpdateLiveConfiguration(targetLiveOffsetUs);
long windowDefaultStartPositionUs =
getLiveWindowDefaultStartPositionUs(playlist, liveEdgeOffsetUs);
return new SinglePeriodTimeline(
presentationStartTimeMs, presentationStartTimeMs,
windowStartTimeMs, windowStartTimeMs,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
@ -553,12 +561,29 @@ public final class HlsMediaSource extends BaseMediaSource
manifest, manifest,
mediaItem, mediaItem,
liveConfiguration); liveConfiguration);
} else /* not live */ {
if (windowDefaultStartPositionUs == C.TIME_UNSET) {
windowDefaultStartPositionUs = 0;
} }
timeline =
new SinglePeriodTimeline( private SinglePeriodTimeline createTimelineForOnDemand(
HlsMediaPlaylist playlist,
long presentationStartTimeMs,
long windowStartTimeMs,
HlsManifest manifest) {
long windowDefaultStartPositionUs;
if (playlist.startOffsetUs == C.TIME_UNSET || playlist.segments.isEmpty()) {
windowDefaultStartPositionUs = 0;
} else {
// From RFC 8216, section 4.4.2.2: if playlist.startOffsetUs is negative, it indicates the
// beginning of the Playlist, whereas if it is beyond the playlist duration it indicates the
// end of the playlist.
long startOffsetUs = Util.constrainValue(playlist.startOffsetUs, 0, playlist.durationUs);
if (playlist.preciseStart || startOffsetUs == playlist.durationUs) {
windowDefaultStartPositionUs = startOffsetUs;
} else {
windowDefaultStartPositionUs =
findClosestPrecedingSegment(playlist.segments, startOffsetUs).relativeStartTimeUs;
}
}
return new SinglePeriodTimeline(
presentationStartTimeMs, presentationStartTimeMs,
windowStartTimeMs, windowStartTimeMs,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
@ -572,8 +597,6 @@ public final class HlsMediaSource extends BaseMediaSource
mediaItem, mediaItem,
/* liveConfiguration= */ null); /* liveConfiguration= */ null);
} }
refreshSourceInfo(timeline);
}
private long getLiveEdgeOffsetUs(HlsMediaPlaylist playlist) { private long getLiveEdgeOffsetUs(HlsMediaPlaylist playlist) {
return playlist.hasProgramDateTime return playlist.hasProgramDateTime
@ -581,19 +604,35 @@ public final class HlsMediaSource extends BaseMediaSource
: 0; : 0;
} }
private long getWindowDefaultStartPosition(HlsMediaPlaylist playlist, long liveEdgeOffsetUs) { private long getLiveWindowDefaultStartPositionUs(
List<HlsMediaPlaylist.Segment> segments = playlist.segments; HlsMediaPlaylist playlist, long liveEdgeOffsetUs) {
int segmentIndex = segments.size() - 1; if (playlist.startOffsetUs != C.TIME_UNSET && playlist.preciseStart) {
long minStartPositionUs = // From RFC 8216, section 4.4.2.2: if playlist.startOffsetUs is negative, it indicates the
playlist.durationUs + liveEdgeOffsetUs - C.msToUs(liveConfiguration.targetOffsetMs); // beginning of the Playlist, whereas if it is beyond the playlist duration it indicates the
while (segmentIndex > 0 // end of the playlist.
&& segments.get(segmentIndex).relativeStartTimeUs > minStartPositionUs) { return Util.constrainValue(playlist.startOffsetUs, 0, playlist.durationUs);
segmentIndex--;
} }
return segments.get(segmentIndex).relativeStartTimeUs; long maxStartPositionUs =
playlist.durationUs + liveEdgeOffsetUs - C.msToUs(liveConfiguration.targetOffsetMs);
@Nullable
HlsMediaPlaylist.Part part =
findClosestPrecedingIndependentPart(playlist.trailingParts, maxStartPositionUs);
if (part != null) {
return part.relativeStartTimeUs;
}
if (playlist.segments.isEmpty()) {
return 0;
}
HlsMediaPlaylist.Segment segment =
findClosestPrecedingSegment(playlist.segments, maxStartPositionUs);
part = findClosestPrecedingIndependentPart(segment.parts, maxStartPositionUs);
if (part != null) {
return part.relativeStartTimeUs;
}
return segment.relativeStartTimeUs;
} }
private void maybeUpdateMediaItem(long targetLiveOffsetUs) { private void maybeUpdateLiveConfiguration(long targetLiveOffsetUs) {
long targetLiveOffsetMs = C.usToMs(targetLiveOffsetUs); long targetLiveOffsetMs = C.usToMs(targetLiveOffsetUs);
if (targetLiveOffsetMs != liveConfiguration.targetOffsetMs) { if (targetLiveOffsetMs != liveConfiguration.targetOffsetMs) {
liveConfiguration = liveConfiguration =
@ -601,21 +640,68 @@ public final class HlsMediaSource extends BaseMediaSource
} }
} }
/**
* Gets the target live offset, in microseconds, for a live playlist.
*
* <p>The target offset is derived by checking the following in this order:
*
* <ol>
* <li>The playlist defines a start offset.
* <li>The playlist defines a part hold back in server control and has part duration.
* <li>The playlist defines a hold back in server control.
* <li>Fallback to {@code 3 x target duration}.
* </ol>
*
* @param playlist The playlist.
* @param liveEdgeOffsetUs The current live edge offset.
* @return The selected target live offset, in microseconds.
*/
private static long getTargetLiveOffsetUs(HlsMediaPlaylist playlist, long liveEdgeOffsetUs) { private static long getTargetLiveOffsetUs(HlsMediaPlaylist playlist, long liveEdgeOffsetUs) {
HlsMediaPlaylist.ServerControl serverControl = playlist.serverControl; HlsMediaPlaylist.ServerControl serverControl = playlist.serverControl;
// Select part hold back only if the playlist has a part target duration. long targetOffsetUs;
long offsetToEndOfPlaylistUs;
if (playlist.startOffsetUs != C.TIME_UNSET) { if (playlist.startOffsetUs != C.TIME_UNSET) {
offsetToEndOfPlaylistUs = playlist.durationUs - playlist.startOffsetUs; // From RFC 8216, section 4.4.2.2: if playlist.startOffsetUs is negative, it indicates the
// beginning of the Playlist, whereas if it is beyond the playlist duration it indicates the
// end of the playlist.
long startOffsetUs = Util.constrainValue(playlist.startOffsetUs, 0, playlist.durationUs);
targetOffsetUs = playlist.durationUs - startOffsetUs;
} else if (serverControl.partHoldBackUs != C.TIME_UNSET } else if (serverControl.partHoldBackUs != C.TIME_UNSET
&& playlist.partTargetDurationUs != C.TIME_UNSET) { && playlist.partTargetDurationUs != C.TIME_UNSET) {
offsetToEndOfPlaylistUs = serverControl.partHoldBackUs; // Select part hold back only if the playlist has a part target duration.
targetOffsetUs = serverControl.partHoldBackUs;
} else if (serverControl.holdBackUs != C.TIME_UNSET) { } else if (serverControl.holdBackUs != C.TIME_UNSET) {
offsetToEndOfPlaylistUs = serverControl.holdBackUs; targetOffsetUs = serverControl.holdBackUs;
} else { } else {
// Fallback, see RFC 8216, Section 4.4.3.8. // Fallback, see RFC 8216, Section 4.4.3.8.
offsetToEndOfPlaylistUs = 3 * playlist.targetDurationUs; targetOffsetUs = 3 * playlist.targetDurationUs;
} }
return offsetToEndOfPlaylistUs + liveEdgeOffsetUs; return targetOffsetUs + liveEdgeOffsetUs;
}
@Nullable
private static HlsMediaPlaylist.Part findClosestPrecedingIndependentPart(
List<HlsMediaPlaylist.Part> parts, long positionUs) {
@Nullable HlsMediaPlaylist.Part closestPart = null;
for (int i = 0; i < parts.size(); i++) {
HlsMediaPlaylist.Part part = parts.get(i);
if (part.relativeStartTimeUs <= positionUs && part.isIndependent) {
closestPart = part;
} else if (part.relativeStartTimeUs > positionUs) {
break;
}
}
return closestPart;
}
/**
* Gets the segment that contains {@code positionUs}, or the last sent if the position is beyond
* the segments list.
*/
private static HlsMediaPlaylist.Segment findClosestPrecedingSegment(
List<HlsMediaPlaylist.Segment> segments, long positionUs) {
int segmentIndex =
Util.binarySearchFloor(
segments, positionUs, /* inclusive= */ true, /* stayInBounds= */ true);
return segments.get(segmentIndex);
} }
} }

View File

@ -391,8 +391,13 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
/** The type of the playlist. See {@link PlaylistType}. */ /** The type of the playlist. See {@link PlaylistType}. */
@PlaylistType public final int playlistType; @PlaylistType public final int playlistType;
/** The start offset in microseconds, as defined by #EXT-X-START. */ /**
* The start offset in microseconds, as defined by #EXT-X-START, or {@link C#TIME_UNSET} if
* undefined.
*/
public final long startOffsetUs; public final long startOffsetUs;
/** Whether the start position should be precise, as defined by #EXT-X-START. */
public final boolean preciseStart;
/** /**
* If {@link #hasProgramDateTime} is true, contains the datetime as microseconds since epoch. * If {@link #hasProgramDateTime} is true, contains the datetime as microseconds since epoch.
* Otherwise, contains the aggregated duration of removed segments up to this snapshot of the * Otherwise, contains the aggregated duration of removed segments up to this snapshot of the
@ -467,6 +472,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
String baseUri, String baseUri,
List<String> tags, List<String> tags,
long startOffsetUs, long startOffsetUs,
boolean preciseStart,
long startTimeUs, long startTimeUs,
boolean hasDiscontinuitySequence, boolean hasDiscontinuitySequence,
int discontinuitySequence, int discontinuitySequence,
@ -485,6 +491,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
super(baseUri, tags, hasIndependentSegments); super(baseUri, tags, hasIndependentSegments);
this.playlistType = playlistType; this.playlistType = playlistType;
this.startTimeUs = startTimeUs; this.startTimeUs = startTimeUs;
this.preciseStart = preciseStart;
this.hasDiscontinuitySequence = hasDiscontinuitySequence; this.hasDiscontinuitySequence = hasDiscontinuitySequence;
this.discontinuitySequence = discontinuitySequence; this.discontinuitySequence = discontinuitySequence;
this.mediaSequence = mediaSequence; this.mediaSequence = mediaSequence;
@ -562,6 +569,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
baseUri, baseUri,
tags, tags,
startOffsetUs, startOffsetUs,
preciseStart,
startTimeUs, startTimeUs,
/* hasDiscontinuitySequence= */ true, /* hasDiscontinuitySequence= */ true,
discontinuitySequence, discontinuitySequence,
@ -592,6 +600,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
baseUri, baseUri,
tags, tags,
startOffsetUs, startOffsetUs,
preciseStart,
startTimeUs, startTimeUs,
hasDiscontinuitySequence, hasDiscontinuitySequence,
discontinuitySequence, discontinuitySequence,

View File

@ -217,6 +217,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final Pattern REGEX_FORCED = compileBooleanAttrPattern("FORCED"); private static final Pattern REGEX_FORCED = compileBooleanAttrPattern("FORCED");
private static final Pattern REGEX_INDEPENDENT = compileBooleanAttrPattern("INDEPENDENT"); private static final Pattern REGEX_INDEPENDENT = compileBooleanAttrPattern("INDEPENDENT");
private static final Pattern REGEX_GAP = compileBooleanAttrPattern("GAP"); private static final Pattern REGEX_GAP = compileBooleanAttrPattern("GAP");
private static final Pattern REGEX_PRECISE = compileBooleanAttrPattern("PRECISE");
private static final Pattern REGEX_VALUE = Pattern.compile("VALUE=\"(.+?)\""); private static final Pattern REGEX_VALUE = Pattern.compile("VALUE=\"(.+?)\"");
private static final Pattern REGEX_IMPORT = Pattern.compile("IMPORT=\"(.+?)\""); private static final Pattern REGEX_IMPORT = Pattern.compile("IMPORT=\"(.+?)\"");
private static final Pattern REGEX_VARIABLE_REFERENCE = private static final Pattern REGEX_VARIABLE_REFERENCE =
@ -652,6 +653,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
int relativeDiscontinuitySequence = 0; int relativeDiscontinuitySequence = 0;
long playlistStartTimeUs = 0; long playlistStartTimeUs = 0;
long segmentStartTimeUs = 0; long segmentStartTimeUs = 0;
boolean preciseStart = false;
long segmentByteRangeOffset = 0; long segmentByteRangeOffset = 0;
long segmentByteRangeLength = C.LENGTH_UNSET; long segmentByteRangeLength = C.LENGTH_UNSET;
long partStartTimeUs = 0; long partStartTimeUs = 0;
@ -694,6 +696,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
isIFrameOnly = true; isIFrameOnly = true;
} else if (line.startsWith(TAG_START)) { } else if (line.startsWith(TAG_START)) {
startOffsetUs = (long) (parseDoubleAttr(line, REGEX_TIME_OFFSET) * C.MICROS_PER_SECOND); startOffsetUs = (long) (parseDoubleAttr(line, REGEX_TIME_OFFSET) * C.MICROS_PER_SECOND);
preciseStart =
parseOptionalBooleanAttribute(line, REGEX_PRECISE, /* defaultValue= */ false);
} else if (line.startsWith(TAG_SERVER_CONTROL)) { } else if (line.startsWith(TAG_SERVER_CONTROL)) {
serverControl = parseServerControl(line); serverControl = parseServerControl(line);
} else if (line.startsWith(TAG_PART_INF)) { } else if (line.startsWith(TAG_PART_INF)) {
@ -1024,6 +1028,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
baseUri, baseUri,
tags, tags,
startOffsetUs, startOffsetUs,
preciseStart,
playlistStartTimeUs, playlistStartTimeUs,
hasDiscontinuitySequence, hasDiscontinuitySequence,
playlistDiscontinuitySequence, playlistDiscontinuitySequence,

View File

@ -122,7 +122,7 @@ public class HlsMediaSourceTest {
} }
@Test @Test
public void loadPlaylist_noTargetLiveOffsetDefined_fallbackToThreeTargetDuration() public void loadLivePlaylist_noTargetLiveOffsetDefined_fallbackToThreeTargetDuration()
throws TimeoutException, ParserException { throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8"; String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 16 seconds but not hold back or part hold back. // The playlist has a duration of 16 seconds but not hold back or part hold back.
@ -158,7 +158,7 @@ public class HlsMediaSourceTest {
} }
@Test @Test
public void loadPlaylist_holdBackInPlaylist_targetLiveOffsetFromHoldBack() public void loadLivePlaylist_holdBackInPlaylist_targetLiveOffsetFromHoldBack()
throws TimeoutException, ParserException { throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8"; String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 16 seconds and a hold back of 12 seconds. // The playlist has a duration of 16 seconds and a hold back of 12 seconds.
@ -195,7 +195,7 @@ public class HlsMediaSourceTest {
@Test @Test
public void public void
loadPlaylist_partHoldBackWithoutPartInformationInPlaylist_targetLiveOffsetFromHoldBack() loadLivePlaylist_partHoldBackWithoutPartInformationInPlaylist_targetLiveOffsetFromHoldBack()
throws TimeoutException, ParserException { throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8"; String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a part hold back but not EXT-X-PART-INF. We should pick up the hold back. // The playlist has a part hold back but not EXT-X-PART-INF. We should pick up the hold back.
@ -233,7 +233,7 @@ public class HlsMediaSourceTest {
@Test @Test
public void public void
loadPlaylist_partHoldBackWithPartInformationInPlaylist_targetLiveOffsetFromPartHoldBack() loadLivePlaylist_partHoldBackWithPartInformationInPlaylist_targetLiveOffsetFromPartHoldBack()
throws TimeoutException, ParserException { throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8"; String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 4 seconds, part hold back and EXT-X-PART-INF defined. // The playlist has a duration of 4 seconds, part hold back and EXT-X-PART-INF defined.
@ -263,7 +263,44 @@ public class HlsMediaSourceTest {
} }
@Test @Test
public void loadPlaylist_withPlaylistStartTime_targetLiveOffsetFromStartTime() public void loadLivePlaylist_withParts_defaultPositionPointsAtClosestIndependentPart()
throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 7 seconds, part hold back and EXT-X-PART-INF defined.
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-01T00:00:00.0+00:00\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-VERSION:3\n"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence0.ts\n"
+ "#EXT-X-SERVER-CONTROL:HOLD-BACK=12,PART-HOLD-BACK=2\n"
+ "#EXT-X-PART-INF:PART-TARGET=0.5\n"
+ "#EXT-X-PART:DURATION=0.5000,URI=\"fileSequence1.0.ts\",INDEPENDENT=YES\n"
+ "#EXT-X-PART:DURATION=0.5000,URI=\"fileSequence1.1.ts\"\n"
+ "#EXT-X-PART:DURATION=0.5000,URI=\"fileSequence1.2.ts\",INDEPENDENT=YES\n"
+ "#EXT-X-PART:DURATION=0.5000,URI=\"fileSequence1.3.ts\"\n"
+ "#EXT-X-PART:DURATION=0.5000,URI=\"fileSequence1.4.ts\",INDEPENDENT=YES\n"
+ "#EXT-X-PART:DURATION=0.5000,URI=\"fileSequence1.5.ts\"";
// The playlist finishes 1 second before the current time.
SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:08.0+00:00"));
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
MediaItem mediaItem = MediaItem.fromUri(playlistUri);
HlsMediaSource mediaSource = factory.createMediaSource(mediaItem);
Timeline timeline = prepareAndWaitForTimeline(mediaSource);
Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
// The target live offset is picked from part hold back and then expressed in relation to the
// live edge (+1 seconds).
assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(3000);
// The default position points the closest preceding independent part.
assertThat(window.defaultPositionUs).isEqualTo(5000000);
}
@Test
public void loadLivePlaylist_withNonPreciseStartTime_targetLiveOffsetFromStartTime()
throws TimeoutException, ParserException { throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8"; String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 16 seconds, and part hold back, hold back and start time // The playlist has a duration of 16 seconds, and part hold back, hold back and start time
@ -273,7 +310,45 @@ public class HlsMediaSourceTest {
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-01T00:00:00.0+00:00\n" + "#EXT-X-PROGRAM-DATE-TIME:2020-01-01T00:00:00.0+00:00\n"
+ "#EXT-X-TARGETDURATION:4\n" + "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-VERSION:3\n" + "#EXT-X-VERSION:3\n"
+ "#EXT-X-START:TIME-OFFSET=-15" + "#EXT-X-START:TIME-OFFSET=-10\n"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence0.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence1.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence2.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence3.ts\n"
+ "#EXT-X-SERVER-CONTROL:HOLD-BACK=12,PART-HOLD-BACK=3\n";
// The playlist finishes 1 second before the current time.
SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:17.0+00:00"));
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
MediaItem mediaItem = MediaItem.fromUri(playlistUri);
HlsMediaSource mediaSource = factory.createMediaSource(mediaItem);
Timeline timeline = prepareAndWaitForTimeline(mediaSource);
Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
// The target live offset is picked from start time (16 - 10 = 6) and then expressed in relation
// to the live edge (17 - 6 = 11 seconds).
assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(11000);
// The default position points to the segment containing the start time.
assertThat(window.defaultPositionUs).isEqualTo(4000000);
}
@Test
public void loadLivePlaylist_withPreciseStartTime_targetLiveOffsetFromStartTime()
throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 16 seconds, and part hold back, hold back and start time
// defined.
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-01T00:00:00.0+00:00\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-VERSION:3\n"
+ "#EXT-X-START:TIME-OFFSET=-10,PRECISE=YES\n"
+ "#EXT-X-MEDIA-SEQUENCE:0\n" + "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXTINF:4.00000,\n" + "#EXTINF:4.00000,\n"
+ "fileSequence0.ts\n" + "fileSequence0.ts\n"
@ -283,7 +358,6 @@ public class HlsMediaSourceTest {
+ "fileSequence2.ts\n" + "fileSequence2.ts\n"
+ "#EXTINF:4.00000,\n" + "#EXTINF:4.00000,\n"
+ "fileSequence3.ts\n" + "fileSequence3.ts\n"
+ "#EXT-X-PART-INF:PART-TARGET=0.5\n"
+ "#EXT-X-SERVER-CONTROL:HOLD-BACK=12,PART-HOLD-BACK=3"; + "#EXT-X-SERVER-CONTROL:HOLD-BACK=12,PART-HOLD-BACK=3";
// The playlist finishes 1 second before the current time. // The playlist finishes 1 second before the current time.
SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:17.0+00:00")); SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:17.0+00:00"));
@ -294,14 +368,15 @@ public class HlsMediaSourceTest {
Timeline timeline = prepareAndWaitForTimeline(mediaSource); Timeline timeline = prepareAndWaitForTimeline(mediaSource);
Timeline.Window window = timeline.getWindow(0, new Timeline.Window()); Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
// The target live offset is picked from start time and then expressed in relation to the live // The target live offset is picked from start time (16 - 10 = 6) and then expressed in relation
// edge (+1 seconds). // to the live edge (17 - 7 = 11 seconds).
assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(16000); assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(11000);
assertThat(window.defaultPositionUs).isEqualTo(0); // The default position points to the start time.
assertThat(window.defaultPositionUs).isEqualTo(6000000);
} }
@Test @Test
public void loadPlaylist_targetLiveOffsetInMediaItem_targetLiveOffsetPickedFromMediaItem() public void loadLivePlaylist_targetLiveOffsetInMediaItem_targetLiveOffsetPickedFromMediaItem()
throws TimeoutException, ParserException { throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8"; String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a hold back of 12 seconds and a part hold back of 3 seconds. // The playlist has a hold back of 12 seconds and a part hold back of 3 seconds.
@ -331,7 +406,8 @@ public class HlsMediaSourceTest {
} }
@Test @Test
public void loadPlaylist_targetLiveOffsetLargerThanLiveWindow_targetLiveOffsetIsWithinLiveWindow() public void
loadLivePlaylist_targetLiveOffsetLargerThanLiveWindow_targetLiveOffsetIsWithinLiveWindow()
throws TimeoutException, ParserException { throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8"; String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 8 seconds and a hold back of 12 seconds. // The playlist has a duration of 8 seconds and a hold back of 12 seconds.
@ -364,7 +440,7 @@ public class HlsMediaSourceTest {
@Test @Test
public void public void
loadPlaylist_withoutProgramDateTime_targetLiveOffsetFromPlaylistNotAdjustedToLiveEdge() loadLivePlaylist_withoutProgramDateTime_targetLiveOffsetFromPlaylistNotAdjustedToLiveEdge()
throws TimeoutException { throws TimeoutException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8"; String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 16 seconds and a hold back of 12 seconds. // The playlist has a duration of 16 seconds and a hold back of 12 seconds.
@ -397,6 +473,119 @@ public class HlsMediaSourceTest {
assertThat(window.defaultPositionUs).isEqualTo(4000000); assertThat(window.defaultPositionUs).isEqualTo(4000000);
} }
@Test
public void loadOnDemandPlaylist_withPreciseStartTime_setsDefaultPosition()
throws TimeoutException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-TARGETDURATION:10\n"
+ "#EXT-X-VERSION:4\n"
+ "#EXT-X-START:TIME-OFFSET=15.000,PRECISE=YES"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXTINF:10.0,\n"
+ "fileSequence1.ts\n"
+ "#EXTINF:10.0,\n"
+ "fileSequence2.ts\n"
+ "#EXT-X-ENDLIST";
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
MediaItem mediaItem = new MediaItem.Builder().setUri(playlistUri).build();
HlsMediaSource mediaSource = factory.createMediaSource(mediaItem);
Timeline timeline = prepareAndWaitForTimeline(mediaSource);
Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
// The target live offset is not adjusted to the live edge because the list does not have
// program date time.
assertThat(window.liveConfiguration).isNull();
assertThat(window.defaultPositionUs).isEqualTo(15000000);
}
@Test
public void loadOnDemandPlaylist_withNonPreciseStartTime_setsDefaultPosition()
throws TimeoutException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-TARGETDURATION:10\n"
+ "#EXT-X-VERSION:4\n"
+ "#EXT-X-START:TIME-OFFSET=15.000"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXTINF:10.0,\n"
+ "fileSequence1.ts\n"
+ "#EXTINF:10.0,\n"
+ "fileSequence2.ts\n"
+ "#EXT-X-ENDLIST";
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
MediaItem mediaItem = new MediaItem.Builder().setUri(playlistUri).build();
HlsMediaSource mediaSource = factory.createMediaSource(mediaItem);
Timeline timeline = prepareAndWaitForTimeline(mediaSource);
Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
// The target live offset is not adjusted to the live edge because the list does not have
// program date time.
assertThat(window.liveConfiguration).isNull();
assertThat(window.defaultPositionUs).isEqualTo(10000000);
}
@Test
public void
loadOnDemandPlaylist_withStartTimeBeforeTheBeginning_setsDefaultPositionToTheBeginning()
throws TimeoutException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-TARGETDURATION:10\n"
+ "#EXT-X-VERSION:4\n"
+ "#EXT-X-START:TIME-OFFSET=-35.000"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXTINF:10.0,\n"
+ "fileSequence1.ts\n"
+ "#EXTINF:10.0,\n"
+ "fileSequence2.ts\n"
+ "#EXT-X-ENDLIST";
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
MediaItem mediaItem = new MediaItem.Builder().setUri(playlistUri).build();
HlsMediaSource mediaSource = factory.createMediaSource(mediaItem);
Timeline timeline = prepareAndWaitForTimeline(mediaSource);
Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
assertThat(window.liveConfiguration).isNull();
assertThat(window.defaultPositionUs).isEqualTo(0);
}
@Test
public void loadOnDemandPlaylist_withStartTimeAfterTheNed_setsDefaultPositionToTheEnd()
throws TimeoutException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-TARGETDURATION:10\n"
+ "#EXT-X-VERSION:4\n"
+ "#EXT-X-START:TIME-OFFSET=35.000"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXTINF:10.0,\n"
+ "fileSequence1.ts\n"
+ "#EXTINF:10.0,\n"
+ "fileSequence2.ts\n"
+ "#EXT-X-ENDLIST";
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
MediaItem mediaItem = new MediaItem.Builder().setUri(playlistUri).build();
HlsMediaSource mediaSource = factory.createMediaSource(mediaItem);
Timeline timeline = prepareAndWaitForTimeline(mediaSource);
Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
assertThat(window.liveConfiguration).isNull();
assertThat(window.defaultPositionUs).isEqualTo(20000000);
}
@Test @Test
public void refreshPlaylist_targetLiveOffsetRemainsInWindow() public void refreshPlaylist_targetLiveOffsetRemainsInWindow()
throws TimeoutException, IOException { throws TimeoutException, IOException {