HLS: populate targetLiveOffset in MediaItem from server control

Issue: #5011
PiperOrigin-RevId: 340260636
This commit is contained in:
christosts 2020-11-02 18:09:59 +00:00 committed by Oliver Woodman
parent e1211f9254
commit 42a2b9230a
2 changed files with 459 additions and 15 deletions

View file

@ -16,12 +16,13 @@
package com.google.android.exoplayer2.source.hls;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static java.lang.Math.max;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.net.Uri;
import android.os.SystemClock;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.MediaItem;
@ -52,6 +53,7 @@ import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
@ -87,7 +89,6 @@ public final class HlsMediaSource extends BaseMediaSource
public static final int METADATA_TYPE_ID3 = 1;
/** Type for ESMG metadata in HLS streams. */
public static final int METADATA_TYPE_EMSG = 3;
/** Factory for {@link HlsMediaSource}s. */
public static final class Factory implements MediaSourceFactory {
@ -105,6 +106,7 @@ public final class HlsMediaSource extends BaseMediaSource
private boolean useSessionKeys;
private List<StreamKey> streamKeys;
@Nullable private Object tag;
private long elapsedRealTimeOffsetMs;
/**
* Creates a new factory for {@link HlsMediaSource}s.
@ -133,6 +135,7 @@ public final class HlsMediaSource extends BaseMediaSource
compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();
metadataType = METADATA_TYPE_ID3;
streamKeys = Collections.emptyList();
elapsedRealTimeOffsetMs = C.TIME_UNSET;
}
/**
@ -316,6 +319,20 @@ public final class HlsMediaSource extends BaseMediaSource
return this;
}
/**
* Sets the offset between {@link SystemClock#elapsedRealtime()} and the time since the Unix
* epoch. By default, is it set to {@link C#TIME_UNSET}.
*
* @param elapsedRealTimeOffsetMs The offset between {@link SystemClock#elapsedRealtime()} and
* the time since the Unix epoch, in milliseconds.
* @return This factory, for convenience.
*/
@VisibleForTesting
/* package */ Factory setElapsedRealTimeOffsetMs(long elapsedRealTimeOffsetMs) {
this.elapsedRealTimeOffsetMs = elapsedRealTimeOffsetMs;
return this;
}
/** @deprecated Use {@link #createMediaSource(MediaItem)} instead. */
@SuppressWarnings("deprecation")
@Deprecated
@ -364,6 +381,7 @@ public final class HlsMediaSource extends BaseMediaSource
loadErrorHandlingPolicy,
playlistTrackerFactory.createTracker(
hlsDataSourceFactory, loadErrorHandlingPolicy, playlistParserFactory),
elapsedRealTimeOffsetMs,
allowChunklessPreparation,
metadataType,
useSessionKeys);
@ -376,7 +394,6 @@ public final class HlsMediaSource extends BaseMediaSource
}
private final HlsExtractorFactory extractorFactory;
private final MediaItem mediaItem;
private final MediaItem.PlaybackProperties playbackProperties;
private final HlsDataSourceFactory dataSourceFactory;
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
@ -386,7 +403,9 @@ public final class HlsMediaSource extends BaseMediaSource
private final @MetadataType int metadataType;
private final boolean useSessionKeys;
private final HlsPlaylistTracker playlistTracker;
private final long elapsedRealTimeOffsetMs;
private MediaItem mediaItem;
@Nullable private TransferListener mediaTransferListener;
private HlsMediaSource(
@ -397,6 +416,7 @@ public final class HlsMediaSource extends BaseMediaSource
DrmSessionManager drmSessionManager,
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
HlsPlaylistTracker playlistTracker,
long elapsedRealTimeOffsetMs,
boolean allowChunklessPreparation,
@MetadataType int metadataType,
boolean useSessionKeys) {
@ -408,6 +428,7 @@ public final class HlsMediaSource extends BaseMediaSource
this.drmSessionManager = drmSessionManager;
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
this.playlistTracker = playlistTracker;
this.elapsedRealTimeOffsetMs = elapsedRealTimeOffsetMs;
this.allowChunklessPreparation = allowChunklessPreparation;
this.metadataType = metadataType;
this.useSessionKeys = useSessionKeys;
@ -491,25 +512,28 @@ public final class HlsMediaSource extends BaseMediaSource
HlsManifest manifest =
new HlsManifest(checkNotNull(playlistTracker.getMasterPlaylist()), playlist);
if (playlistTracker.isLive()) {
long liveEdgeOffsetUs = getLiveEdgeOffsetUs(playlist);
long targetLiveOffsetUs =
mediaItem.liveConfiguration.targetLiveOffsetMs != C.TIME_UNSET
? C.msToUs(mediaItem.liveConfiguration.targetLiveOffsetMs)
: 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 (windowDefaultStartPositionUs == C.TIME_UNSET) {
if (!segments.isEmpty()) {
windowDefaultStartPositionUs = getWindowDefaultStartPosition(playlist, liveEdgeOffsetUs);
} else if (windowDefaultStartPositionUs == C.TIME_UNSET) {
windowDefaultStartPositionUs = 0;
if (!segments.isEmpty()) {
int defaultStartSegmentIndex = max(0, segments.size() - 3);
// We attempt to set the default start position to be at least twice the target duration
// behind the live edge.
long minStartPositionUs = playlist.durationUs - playlist.targetDurationUs * 2;
while (defaultStartSegmentIndex > 0
&& segments.get(defaultStartSegmentIndex).relativeStartTimeUs > minStartPositionUs) {
defaultStartSegmentIndex--;
}
windowDefaultStartPositionUs = segments.get(defaultStartSegmentIndex).relativeStartTimeUs;
}
}
timeline =
new SinglePeriodTimeline(
presentationStartTimeMs,
@ -545,4 +569,47 @@ public final class HlsMediaSource extends BaseMediaSource
}
refreshSourceInfo(timeline);
}
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(mediaItem.liveConfiguration.targetLiveOffsetMs);
while (segmentIndex > 0
&& segments.get(segmentIndex).relativeStartTimeUs > minStartPositionUs) {
segmentIndex--;
}
return segments.get(segmentIndex).relativeStartTimeUs;
}
private void maybeUpdateMediaItem(long targetLiveOffsetUs) {
long targetLiveOffsetMs = C.usToMs(targetLiveOffsetUs);
if (targetLiveOffsetMs != mediaItem.liveConfiguration.targetLiveOffsetMs) {
mediaItem = mediaItem.buildUpon().setLiveTargetOffsetMs(targetLiveOffsetMs).build();
}
}
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;
if (serverControl.partHoldBackUs != C.TIME_UNSET
&& playlist.partTargetDurationUs != C.TIME_UNSET) {
offsetToEndOfPlaylistUs = serverControl.partHoldBackUs;
} else if (serverControl.holdBackUs != C.TIME_UNSET) {
offsetToEndOfPlaylistUs = serverControl.holdBackUs;
} else {
// Fallback, see RFC 8216, Section 4.4.3.8.
offsetToEndOfPlaylistUs = 3 * playlist.targetDurationUs;
}
return offsetToEndOfPlaylistUs + liveEdgeOffsetUs;
}
}

View file

@ -15,15 +15,33 @@
*/
package com.google.android.exoplayer2.source.hls;
import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import android.net.Uri;
import android.os.SystemClock;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
import com.google.android.exoplayer2.testutil.FakeDataSet;
import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Util;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -132,4 +150,363 @@ public class HlsMediaSourceTest {
assertThat(hlsMediaItem.playbackProperties.uri).isEqualTo(mediaItem.playbackProperties.uri);
assertThat(hlsMediaItem.playbackProperties.streamKeys).containsExactly(mediaItemStreamKey);
}
@Test
public void loadPlaylist_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.
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"
+ "#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:CAN-SKIP-UNTIL=24";
// The playlist finishes 1 second before the the current time, therefore there's a live edge
// offset of 1 second.
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 target duration (3 * 4 = 12 seconds) and then expressed
// in relation to the live edge (12 + 1 seconds).
assertThat(window.mediaItem.liveConfiguration.targetLiveOffsetMs).isEqualTo(13000);
assertThat(window.defaultPositionUs).isEqualTo(4000000);
}
@Test
public void loadPlaylist_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.
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"
+ "#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";
// The playlist finishes 1 second before the the current time, therefore there's a live edge
// offset of 1 second.
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 hold back and then expressed in relation to the live
// edge (+1 seconds).
assertThat(window.mediaItem.liveConfiguration.targetLiveOffsetMs).isEqualTo(13000);
assertThat(window.defaultPositionUs).isEqualTo(4000000);
}
@Test
public void
loadPlaylist_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.
// The duration of the playlist is 16 seconds so that the defined hold back is within the live
// window.
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"
+ "#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";
// The playlist finishes 1 second before the 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 hold back and then expressed in relation to the live
// edge (+1 seconds).
assertThat(window.mediaItem.liveConfiguration.targetLiveOffsetMs).isEqualTo(13000);
assertThat(window.defaultPositionUs).isEqualTo(4000000);
}
@Test
public void
loadPlaylist_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.
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-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 the current time.
SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:05.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.mediaItem.liveConfiguration.targetLiveOffsetMs).isEqualTo(4000);
assertThat(window.defaultPositionUs).isEqualTo(0);
}
@Test
public void loadPlaylist_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.
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=3";
// The playlist finishes 1 second before the the current time. This should not affect the target
// live offset set in the media item.
SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:05.0+00:00"));
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
MediaItem mediaItem =
new MediaItem.Builder().setUri(playlistUri).setLiveTargetOffsetMs(1000).build();
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 the media item and not adjusted.
assertThat(window.mediaItem.liveConfiguration.targetLiveOffsetMs).isEqualTo(1000);
assertThat(window.defaultPositionUs).isEqualTo(0);
}
@Test
public void loadPlaylist_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 =
"#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"
+ "#EXTINF:4.00000,\n"
+ "fileSequence1.ts\n"
+ "#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24";
// The playlist finishes 1 second before the live edge, therefore the live window duration is
// 9 seconds (8 + 1).
SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:09.0+00:00"));
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
MediaItem mediaItem =
new MediaItem.Builder().setUri(playlistUri).setLiveTargetOffsetMs(20_000).build();
HlsMediaSource mediaSource = factory.createMediaSource(mediaItem);
Timeline timeline = prepareAndWaitForTimeline(mediaSource);
Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
assertThat(mediaItem.liveConfiguration.targetLiveOffsetMs)
.isGreaterThan(C.usToMs(window.durationUs));
assertThat(window.mediaItem.liveConfiguration.targetLiveOffsetMs).isEqualTo(9000);
}
@Test
public void
loadPlaylist_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.
String playlist =
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-VERSION:3\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";
// The playlist finishes 8 seconds before the current time.
SystemClock.setCurrentTimeMillis(20000);
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.mediaItem.liveConfiguration.targetLiveOffsetMs).isEqualTo(12000);
assertThat(window.defaultPositionUs).isEqualTo(4000000);
}
@Test
public void refreshPlaylist_targetLiveOffsetRemainsInWindow()
throws TimeoutException, IOException {
String playlistUri1 = "fake://foo.bar/media0/playlist1.m3u8";
// The playlist has a duration of 16 seconds and a hold back of 12 seconds.
String playlist1 =
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-VERSION:3\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";
// The second playlist defines a different hold back.
String playlistUri2 = "fake://foo.bar/media0/playlist2.m3u8";
String playlist2 =
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-VERSION:3\n"
+ "#EXT-X-MEDIA-SEQUENCE:4\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence4.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence5.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence6.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence7.ts\n"
+ "#EXT-X-SERVER-CONTROL:HOLD-BACK:14";
// The third playlist has a duration of 8 seconds.
String playlistUri3 = "fake://foo.bar/media0/playlist3.m3u8";
String playlist3 =
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-VERSION:3\n"
+ "#EXT-X-MEDIA-SEQUENCE:4\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence8.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence9.ts\n"
+ "#EXTINF:4.00000,\n"
+ "#EXT-X-SERVER-CONTROL:HOLD-BACK:12";
// The third playlist has a duration of 16 seconds but the target live offset should remain at
// 8 seconds.
String playlistUri4 = "fake://foo.bar/media0/playlist4.m3u8";
String playlist4 =
"#EXTM3U\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-VERSION:3\n"
+ "#EXT-X-MEDIA-SEQUENCE:4\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence10.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence11.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence12.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence13.ts\n"
+ "#EXTINF:4.00000,\n"
+ "#EXT-X-SERVER-CONTROL:HOLD-BACK:12";
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri1, playlist1);
MediaItem mediaItem = new MediaItem.Builder().setUri(playlistUri1).build();
HlsMediaSource mediaSource = factory.createMediaSource(mediaItem);
HlsMediaPlaylist secondPlaylist = parseHlsMediaPlaylist(playlistUri2, playlist2);
HlsMediaPlaylist thirdPlaylist = parseHlsMediaPlaylist(playlistUri3, playlist3);
HlsMediaPlaylist fourthPlaylist = parseHlsMediaPlaylist(playlistUri4, playlist4);
List<Timeline> timelines = new ArrayList<>();
MediaSource.MediaSourceCaller mediaSourceCaller = (source, timeline) -> timelines.add(timeline);
mediaSource.prepareSource(mediaSourceCaller, null);
runMainLooperUntil(() -> timelines.size() == 1);
mediaSource.onPrimaryPlaylistRefreshed(secondPlaylist);
runMainLooperUntil(() -> timelines.size() == 2);
mediaSource.onPrimaryPlaylistRefreshed(thirdPlaylist);
runMainLooperUntil(() -> timelines.size() == 3);
mediaSource.onPrimaryPlaylistRefreshed(fourthPlaylist);
runMainLooperUntil(() -> timelines.size() == 4);
Timeline.Window window = new Timeline.Window();
assertThat(timelines.get(0).getWindow(0, window).mediaItem.liveConfiguration.targetLiveOffsetMs)
.isEqualTo(12000);
assertThat(timelines.get(1).getWindow(0, window).mediaItem.liveConfiguration.targetLiveOffsetMs)
.isEqualTo(12000);
assertThat(timelines.get(2).getWindow(0, window).mediaItem.liveConfiguration.targetLiveOffsetMs)
.isEqualTo(8000);
assertThat(timelines.get(3).getWindow(0, window).mediaItem.liveConfiguration.targetLiveOffsetMs)
.isEqualTo(8000);
}
private static HlsMediaSource.Factory createHlsMediaSourceFactory(
String playlistUri, String playlist) {
FakeDataSet fakeDataSet = new FakeDataSet().setData(playlistUri, Util.getUtf8Bytes(playlist));
return new HlsMediaSource.Factory(
dataType -> new FakeDataSource.Factory().setFakeDataSet(fakeDataSet).createDataSource())
.setElapsedRealTimeOffsetMs(0);
}
/** Prepares the media source and waits until the timeline is updated. */
private static Timeline prepareAndWaitForTimeline(HlsMediaSource mediaSource)
throws TimeoutException {
AtomicReference<Timeline> receivedTimeline = new AtomicReference<>();
mediaSource.prepareSource(
(source, timeline) -> receivedTimeline.set(timeline), /* mediaTransferListener= */ null);
runMainLooperUntil(() -> receivedTimeline.get() != null);
return receivedTimeline.get();
}
private static HlsMediaPlaylist parseHlsMediaPlaylist(String playlistUri, String playlist)
throws IOException {
return (HlsMediaPlaylist)
new HlsPlaylistParser()
.parse(Uri.parse(playlistUri), new ByteArrayInputStream(Util.getUtf8Bytes(playlist)));
}
}