Merge pull request #8767 from uvjustin:hls-start-from-independent-part
PiperOrigin-RevId: 373343326
This commit is contained in:
commit
e20ea797ef
@ -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)
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user