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:
* Support changing ad break positions in the player logic
([#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)

View File

@ -503,7 +503,6 @@ public final class HlsMediaSource extends BaseMediaSource
@Override
public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) {
SinglePeriodTimeline timeline;
long windowStartTimeMs =
playlist.hasProgramDateTime ? C.usToMs(playlist.startTimeUs) : C.TIME_UNSET;
// For playlist types EVENT and VOD we know segments are never removed, so the presentation
@ -513,87 +512,127 @@ public final class HlsMediaSource extends BaseMediaSource
|| playlist.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD
? windowStartTimeMs
: C.TIME_UNSET;
long windowDefaultStartPositionUs = playlist.startOffsetUs;
// masterPlaylist is non-null because the first playlist has been fetched by now.
// The master playlist is non-null because the first playlist has been fetched by now.
HlsManifest manifest =
new HlsManifest(checkNotNull(playlistTracker.getMasterPlaylist()), playlist);
if (playlistTracker.isLive()) {
long liveEdgeOffsetUs = getLiveEdgeOffsetUs(playlist);
long targetLiveOffsetUs =
liveConfiguration.targetOffsetMs != C.TIME_UNSET
? C.msToUs(liveConfiguration.targetOffsetMs)
: getTargetLiveOffsetUs(playlist, liveEdgeOffsetUs);
// 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);
long offsetFromInitialStartTimeUs =
playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
long periodDurationUs =
playlist.hasEndTag ? offsetFromInitialStartTimeUs + playlist.durationUs : C.TIME_UNSET;
List<HlsMediaPlaylist.Segment> segments = playlist.segments;
if (!segments.isEmpty()) {
windowDefaultStartPositionUs = getWindowDefaultStartPosition(playlist, liveEdgeOffsetUs);
} else if (windowDefaultStartPositionUs == C.TIME_UNSET) {
windowDefaultStartPositionUs = 0;
}
timeline =
new SinglePeriodTimeline(
presentationStartTimeMs,
windowStartTimeMs,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
periodDurationUs,
/* windowDurationUs= */ playlist.durationUs,
/* windowPositionInPeriodUs= */ offsetFromInitialStartTimeUs,
windowDefaultStartPositionUs,
/* isSeekable= */ true,
/* isDynamic= */ !playlist.hasEndTag,
manifest,
mediaItem,
liveConfiguration);
} else /* not live */ {
if (windowDefaultStartPositionUs == C.TIME_UNSET) {
windowDefaultStartPositionUs = 0;
}
timeline =
new SinglePeriodTimeline(
presentationStartTimeMs,
windowStartTimeMs,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
/* periodDurationUs= */ playlist.durationUs,
/* windowDurationUs= */ playlist.durationUs,
/* windowPositionInPeriodUs= */ 0,
windowDefaultStartPositionUs,
/* isSeekable= */ true,
/* isDynamic= */ false,
manifest,
mediaItem,
/* liveConfiguration= */ null);
}
SinglePeriodTimeline timeline =
playlistTracker.isLive()
? createTimelineForLive(playlist, presentationStartTimeMs, windowStartTimeMs, manifest)
: createTimelineForOnDemand(
playlist, presentationStartTimeMs, windowStartTimeMs, manifest);
refreshSourceInfo(timeline);
}
private SinglePeriodTimeline createTimelineForLive(
HlsMediaPlaylist playlist,
long presentationStartTimeMs,
long windowStartTimeMs,
HlsManifest manifest) {
long offsetFromInitialStartTimeUs =
playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
long periodDurationUs =
playlist.hasEndTag ? offsetFromInitialStartTimeUs + playlist.durationUs : C.TIME_UNSET;
long liveEdgeOffsetUs = getLiveEdgeOffsetUs(playlist);
long targetLiveOffsetUs;
if (liveConfiguration.targetOffsetMs != C.TIME_UNSET) {
// Media item has a defined target offset.
targetLiveOffsetUs = C.msToUs(liveConfiguration.targetOffsetMs);
} else {
// Decide target offset from playlist.
targetLiveOffsetUs = getTargetLiveOffsetUs(playlist, liveEdgeOffsetUs);
}
// Ensure target live offset is within the live window and greater than the live edge offset.
targetLiveOffsetUs =
Util.constrainValue(
targetLiveOffsetUs, liveEdgeOffsetUs, playlist.durationUs + liveEdgeOffsetUs);
maybeUpdateLiveConfiguration(targetLiveOffsetUs);
long windowDefaultStartPositionUs =
getLiveWindowDefaultStartPositionUs(playlist, liveEdgeOffsetUs);
return new SinglePeriodTimeline(
presentationStartTimeMs,
windowStartTimeMs,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
periodDurationUs,
/* windowDurationUs= */ playlist.durationUs,
/* windowPositionInPeriodUs= */ offsetFromInitialStartTimeUs,
windowDefaultStartPositionUs,
/* isSeekable= */ true,
/* isDynamic= */ !playlist.hasEndTag,
manifest,
mediaItem,
liveConfiguration);
}
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,
windowStartTimeMs,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
/* periodDurationUs= */ playlist.durationUs,
/* windowDurationUs= */ playlist.durationUs,
/* windowPositionInPeriodUs= */ 0,
windowDefaultStartPositionUs,
/* isSeekable= */ true,
/* isDynamic= */ false,
manifest,
mediaItem,
/* liveConfiguration= */ null);
}
private long getLiveEdgeOffsetUs(HlsMediaPlaylist playlist) {
return playlist.hasProgramDateTime
? C.msToUs(Util.getNowUnixTimeMs(elapsedRealTimeOffsetMs)) - playlist.getEndTimeUs()
: 0;
}
private long getWindowDefaultStartPosition(HlsMediaPlaylist playlist, long liveEdgeOffsetUs) {
List<HlsMediaPlaylist.Segment> segments = playlist.segments;
int segmentIndex = segments.size() - 1;
long minStartPositionUs =
playlist.durationUs + liveEdgeOffsetUs - C.msToUs(liveConfiguration.targetOffsetMs);
while (segmentIndex > 0
&& segments.get(segmentIndex).relativeStartTimeUs > minStartPositionUs) {
segmentIndex--;
private long getLiveWindowDefaultStartPositionUs(
HlsMediaPlaylist playlist, long liveEdgeOffsetUs) {
if (playlist.startOffsetUs != C.TIME_UNSET && playlist.preciseStart) {
// 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.
return Util.constrainValue(playlist.startOffsetUs, 0, playlist.durationUs);
}
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);
if (targetLiveOffsetMs != liveConfiguration.targetOffsetMs) {
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) {
HlsMediaPlaylist.ServerControl serverControl = playlist.serverControl;
// Select part hold back only if the playlist has a part target duration.
long offsetToEndOfPlaylistUs;
long targetOffsetUs;
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
&& 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) {
offsetToEndOfPlaylistUs = serverControl.holdBackUs;
targetOffsetUs = serverControl.holdBackUs;
} else {
// 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}. */
@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;
/** 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.
* 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,
List<String> tags,
long startOffsetUs,
boolean preciseStart,
long startTimeUs,
boolean hasDiscontinuitySequence,
int discontinuitySequence,
@ -485,6 +491,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
super(baseUri, tags, hasIndependentSegments);
this.playlistType = playlistType;
this.startTimeUs = startTimeUs;
this.preciseStart = preciseStart;
this.hasDiscontinuitySequence = hasDiscontinuitySequence;
this.discontinuitySequence = discontinuitySequence;
this.mediaSequence = mediaSequence;
@ -562,6 +569,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
baseUri,
tags,
startOffsetUs,
preciseStart,
startTimeUs,
/* hasDiscontinuitySequence= */ true,
discontinuitySequence,
@ -592,6 +600,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
baseUri,
tags,
startOffsetUs,
preciseStart,
startTimeUs,
hasDiscontinuitySequence,
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_INDEPENDENT = compileBooleanAttrPattern("INDEPENDENT");
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_IMPORT = Pattern.compile("IMPORT=\"(.+?)\"");
private static final Pattern REGEX_VARIABLE_REFERENCE =
@ -652,6 +653,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
int relativeDiscontinuitySequence = 0;
long playlistStartTimeUs = 0;
long segmentStartTimeUs = 0;
boolean preciseStart = false;
long segmentByteRangeOffset = 0;
long segmentByteRangeLength = C.LENGTH_UNSET;
long partStartTimeUs = 0;
@ -694,6 +696,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
isIFrameOnly = true;
} else if (line.startsWith(TAG_START)) {
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)) {
serverControl = parseServerControl(line);
} else if (line.startsWith(TAG_PART_INF)) {
@ -1024,6 +1028,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
baseUri,
tags,
startOffsetUs,
preciseStart,
playlistStartTimeUs,
hasDiscontinuitySequence,
playlistDiscontinuitySequence,

View File

@ -122,7 +122,7 @@ public class HlsMediaSourceTest {
}
@Test
public void loadPlaylist_noTargetLiveOffsetDefined_fallbackToThreeTargetDuration()
public void loadLivePlaylist_noTargetLiveOffsetDefined_fallbackToThreeTargetDuration()
throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 16 seconds but not hold back or part hold back.
@ -158,7 +158,7 @@ public class HlsMediaSourceTest {
}
@Test
public void loadPlaylist_holdBackInPlaylist_targetLiveOffsetFromHoldBack()
public void loadLivePlaylist_holdBackInPlaylist_targetLiveOffsetFromHoldBack()
throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 16 seconds and a hold back of 12 seconds.
@ -195,7 +195,7 @@ public class HlsMediaSourceTest {
@Test
public void
loadPlaylist_partHoldBackWithoutPartInformationInPlaylist_targetLiveOffsetFromHoldBack()
loadLivePlaylist_partHoldBackWithoutPartInformationInPlaylist_targetLiveOffsetFromHoldBack()
throws TimeoutException, ParserException {
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.
@ -233,7 +233,7 @@ public class HlsMediaSourceTest {
@Test
public void
loadPlaylist_partHoldBackWithPartInformationInPlaylist_targetLiveOffsetFromPartHoldBack()
loadLivePlaylist_partHoldBackWithPartInformationInPlaylist_targetLiveOffsetFromPartHoldBack()
throws TimeoutException, ParserException {
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.
@ -263,7 +263,44 @@ public class HlsMediaSourceTest {
}
@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 {
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
@ -273,7 +310,45 @@ public class HlsMediaSourceTest {
+ "#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=-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"
+ "#EXTINF:4.00000,\n"
+ "fileSequence0.ts\n"
@ -283,7 +358,6 @@ public class HlsMediaSourceTest {
+ "fileSequence2.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence3.ts\n"
+ "#EXT-X-PART-INF:PART-TARGET=0.5\n"
+ "#EXT-X-SERVER-CONTROL:HOLD-BACK=12,PART-HOLD-BACK=3";
// The playlist finishes 1 second before the current time.
SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:17.0+00:00"));
@ -294,14 +368,15 @@ public class HlsMediaSourceTest {
Timeline timeline = prepareAndWaitForTimeline(mediaSource);
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
// edge (+1 seconds).
assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(16000);
assertThat(window.defaultPositionUs).isEqualTo(0);
// The target live offset is picked from start time (16 - 10 = 6) and then expressed in relation
// to the live edge (17 - 7 = 11 seconds).
assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(11000);
// The default position points to the start time.
assertThat(window.defaultPositionUs).isEqualTo(6000000);
}
@Test
public void loadPlaylist_targetLiveOffsetInMediaItem_targetLiveOffsetPickedFromMediaItem()
public void loadLivePlaylist_targetLiveOffsetInMediaItem_targetLiveOffsetPickedFromMediaItem()
throws TimeoutException, ParserException {
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.
@ -331,8 +406,9 @@ public class HlsMediaSourceTest {
}
@Test
public void loadPlaylist_targetLiveOffsetLargerThanLiveWindow_targetLiveOffsetIsWithinLiveWindow()
throws TimeoutException, ParserException {
public void
loadLivePlaylist_targetLiveOffsetLargerThanLiveWindow_targetLiveOffsetIsWithinLiveWindow()
throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 8 seconds and a hold back of 12 seconds.
String playlist =
@ -364,7 +440,7 @@ public class HlsMediaSourceTest {
@Test
public void
loadPlaylist_withoutProgramDateTime_targetLiveOffsetFromPlaylistNotAdjustedToLiveEdge()
loadLivePlaylist_withoutProgramDateTime_targetLiveOffsetFromPlaylistNotAdjustedToLiveEdge()
throws TimeoutException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// 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);
}
@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
public void refreshPlaylist_targetLiveOffsetRemainsInWindow()
throws TimeoutException, IOException {