mirror of
https://github.com/samsonjs/media.git
synced 2026-04-16 13:05:46 +00:00
HLS: populate targetLiveOffset in MediaItem from server control
Issue: #5011 PiperOrigin-RevId: 340260636
This commit is contained in:
parent
e1211f9254
commit
42a2b9230a
2 changed files with 459 additions and 15 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue