Schedule refresh for all the playing playlists for HLS live stream
Issue: androidx/media#1240 PiperOrigin-RevId: 642927082
This commit is contained in:
parent
ca51ed649b
commit
86a60e6ec2
@ -55,6 +55,8 @@
|
||||
* Cronet Extension:
|
||||
* RTMP Extension:
|
||||
* HLS Extension:
|
||||
* Fix a bug where non-primary playing playlists are not refreshed during
|
||||
live playback ([#1240](https://github.com/androidx/media/issues/1240)).
|
||||
* Smooth Streaming Extension:
|
||||
* RTSP Extension:
|
||||
* Decoder Extensions (FFmpeg, VP9, AV1, etc.):
|
||||
|
@ -253,6 +253,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
* @param trackSelection The {@link ExoTrackSelection}.
|
||||
*/
|
||||
public void setTrackSelection(ExoTrackSelection trackSelection) {
|
||||
// Deactivate the selected playlist from the old track selection for playback.
|
||||
deactivatePlaylistForSelectedTrack();
|
||||
this.trackSelection = trackSelection;
|
||||
}
|
||||
|
||||
@ -263,6 +265,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/** Resets the source. */
|
||||
public void reset() {
|
||||
deactivatePlaylistForSelectedTrack();
|
||||
fatalError = null;
|
||||
}
|
||||
|
||||
@ -463,6 +466,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
partIndex = nextMediaSequenceAndPartIndexWithoutAdapting.second;
|
||||
}
|
||||
|
||||
// If the selected track index changes from another one, we should deactivate the old playlist
|
||||
// for playback.
|
||||
if (selectedTrackIndex != oldTrackIndex && oldTrackIndex != C.INDEX_UNSET) {
|
||||
Uri oldPlaylistUrl = playlistUrls[oldTrackIndex];
|
||||
playlistTracker.deactivatePlaylistForPlayback(oldPlaylistUrl);
|
||||
}
|
||||
|
||||
if (chunkMediaSequence < playlist.mediaSequence) {
|
||||
fatalError = new BehindLiveWindowException();
|
||||
return;
|
||||
@ -944,6 +954,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
return UriUtil.resolveToUri(playlist.baseUri, segmentBase.fullSegmentEncryptionKeyUri);
|
||||
}
|
||||
|
||||
private void deactivatePlaylistForSelectedTrack() {
|
||||
int selectedTrackIndex = this.trackSelection.getSelectedIndexInTrackGroup();
|
||||
playlistTracker.deactivatePlaylistForPlayback(playlistUrls[selectedTrackIndex]);
|
||||
}
|
||||
|
||||
// Package classes.
|
||||
|
||||
/* package */ static final class SegmentBaseHolder {
|
||||
|
@ -565,6 +565,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
sampleQueue.preRelease();
|
||||
}
|
||||
}
|
||||
chunkSource.reset();
|
||||
loader.release(this);
|
||||
handler.removeCallbacksAndMessages(null);
|
||||
released = true;
|
||||
|
@ -191,9 +191,11 @@ public final class DefaultHlsPlaylistTracker
|
||||
@Override
|
||||
@Nullable
|
||||
public HlsMediaPlaylist getPlaylistSnapshot(Uri url, boolean isForPlayback) {
|
||||
@Nullable HlsMediaPlaylist snapshot = playlistBundles.get(url).getPlaylistSnapshot();
|
||||
MediaPlaylistBundle bundle = playlistBundles.get(url);
|
||||
@Nullable HlsMediaPlaylist snapshot = bundle.getPlaylistSnapshot();
|
||||
if (snapshot != null && isForPlayback) {
|
||||
maybeSetPrimaryUrl(url);
|
||||
maybeActivateForPlayback(url);
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
@ -242,6 +244,14 @@ public final class DefaultHlsPlaylistTracker
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivatePlaylistForPlayback(Uri url) {
|
||||
@Nullable MediaPlaylistBundle bundle = playlistBundles.get(url);
|
||||
if (bundle != null) {
|
||||
bundle.setActiveForPlayback(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Loader.Callback implementation.
|
||||
|
||||
@Override
|
||||
@ -352,7 +362,7 @@ public final class DefaultHlsPlaylistTracker
|
||||
|| !isVariantUrl(url)
|
||||
|| (primaryMediaPlaylistSnapshot != null && primaryMediaPlaylistSnapshot.hasEndTag)) {
|
||||
// Ignore if the primary media playlist URL is unchanged, if the media playlist is not
|
||||
// referenced directly by a variant, or it the last primary snapshot contains an end tag.
|
||||
// referenced directly by a variant, or if the last primary snapshot contains an end tag.
|
||||
return;
|
||||
}
|
||||
primaryMediaPlaylistUrl = url;
|
||||
@ -368,6 +378,20 @@ public final class DefaultHlsPlaylistTracker
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeActivateForPlayback(Uri url) {
|
||||
MediaPlaylistBundle playlistBundle = playlistBundles.get(url);
|
||||
@Nullable HlsMediaPlaylist playlistSnapshot = playlistBundle.getPlaylistSnapshot();
|
||||
if (playlistBundle.isActiveForPlayback()) {
|
||||
return;
|
||||
}
|
||||
playlistBundle.setActiveForPlayback(true);
|
||||
if (playlistSnapshot != null && !playlistSnapshot.hasEndTag) {
|
||||
// For playlist that doesn't contain an end tag, we should trigger another load for it, as
|
||||
// the snapshot for it may be stale and it can keep refreshing as an active playlist.
|
||||
playlistBundle.loadPlaylist(true);
|
||||
}
|
||||
}
|
||||
|
||||
private Uri getRequestUriForPrimaryChange(Uri newPrimaryPlaylistUri) {
|
||||
if (primaryMediaPlaylistSnapshot != null
|
||||
&& primaryMediaPlaylistSnapshot.serverControl.canBlockReload) {
|
||||
@ -528,6 +552,7 @@ public final class DefaultHlsPlaylistTracker
|
||||
private long excludeUntilMs;
|
||||
private boolean loadPending;
|
||||
@Nullable private IOException playlistError;
|
||||
private boolean activeForPlayback;
|
||||
|
||||
public MediaPlaylistBundle(Uri playlistUrl) {
|
||||
this.playlistUrl = playlistUrl;
|
||||
@ -563,6 +588,14 @@ public final class DefaultHlsPlaylistTracker
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isActiveForPlayback() {
|
||||
return activeForPlayback;
|
||||
}
|
||||
|
||||
public void setActiveForPlayback(boolean activeForPlayback) {
|
||||
this.activeForPlayback = activeForPlayback;
|
||||
}
|
||||
|
||||
public void release() {
|
||||
mediaPlaylistLoader.release();
|
||||
}
|
||||
@ -763,10 +796,11 @@ public final class DefaultHlsPlaylistTracker
|
||||
}
|
||||
earliestNextLoadTimeMs =
|
||||
currentTimeMs + Util.usToMs(durationUntilNextLoadUs) - loadEventInfo.loadDurationMs;
|
||||
// Schedule a load if this is the primary playlist and it doesn't have an end tag. Else the
|
||||
// next load will be scheduled when refreshPlaylist is called, or when this playlist becomes
|
||||
// the primary.
|
||||
if (playlistUrl.equals(primaryMediaPlaylistUrl) && !playlistSnapshot.hasEndTag) {
|
||||
// Schedule a load if this is the primary or playback playlist and it doesn't have an end tag.
|
||||
// Else the next load will be scheduled when refreshPlaylist is called, or when this playlist
|
||||
// becomes the primary or active for playback.
|
||||
if (!playlistSnapshot.hasEndTag
|
||||
&& (playlistUrl.equals(primaryMediaPlaylistUrl) || activeForPlayback)) {
|
||||
loadPlaylistInternal(getMediaPlaylistUriForReload());
|
||||
}
|
||||
}
|
||||
|
@ -235,4 +235,13 @@ public interface HlsPlaylistTracker {
|
||||
* @return True if the content is live. False otherwise.
|
||||
*/
|
||||
boolean isLive();
|
||||
|
||||
/**
|
||||
* Deactivate the playlist for playback.
|
||||
*
|
||||
* <p>The default implementation is a no-op.
|
||||
*
|
||||
* @param url The {@link Uri} of the playlist to deactivate for playback.
|
||||
*/
|
||||
default void deactivatePlaylistForPlayback(Uri url) {}
|
||||
}
|
||||
|
@ -50,6 +50,8 @@ public class DefaultHlsPlaylistTrackerTest {
|
||||
"media/m3u8/live_low_latency_multivariant";
|
||||
private static final String SAMPLE_M3U8_LIVE_MULTIVARIANT_MEDIA_URI_WITH_PARAM =
|
||||
"media/m3u8/live_low_latency_multivariant_media_uri_with_param";
|
||||
private static final String SAMPLE_M3U8_LIVE_MULTIVARIANT_WITH_AUDIO_RENDITIONS =
|
||||
"media/m3u8/live_low_latency_multivariant_with_audio_renditions";
|
||||
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL =
|
||||
"media/m3u8/live_low_latency_media_can_skip_until";
|
||||
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_FULL_RELOAD_AFTER_ERROR =
|
||||
@ -75,12 +77,21 @@ public class DefaultHlsPlaylistTrackerTest {
|
||||
"media/m3u8/live_low_latency_media_can_block_reload_low_latency_next";
|
||||
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT =
|
||||
"media/m3u8/live_low_latency_media_can_block_reload_low_latency_full_segment";
|
||||
private static final String
|
||||
SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_AUDIO =
|
||||
"media/m3u8/live_low_latency_media_can_block_reload_low_latency_full_segment_audio";
|
||||
private static final String
|
||||
SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_NEXT =
|
||||
"media/m3u8/live_low_latency_media_can_block_reload_low_latency_full_segment_next";
|
||||
private static final String
|
||||
SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_NEXT2 =
|
||||
"media/m3u8/live_low_latency_media_can_block_reload_low_latency_full_segment_next2";
|
||||
private static final String
|
||||
SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_AUDIO_NEXT =
|
||||
"media/m3u8/live_low_latency_media_can_block_reload_low_latency_full_segment_audio_next";
|
||||
private static final String
|
||||
SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_AUDIO_NEXT2 =
|
||||
"media/m3u8/live_low_latency_media_can_block_reload_low_latency_full_segment_audio_next2";
|
||||
private static final String
|
||||
SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_PRELOAD =
|
||||
"media/m3u8/live_low_latency_media_can_block_reload_low_latency_full_segment_preload";
|
||||
@ -371,7 +382,87 @@ public class DefaultHlsPlaylistTrackerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void start_lowLatencyNotScheduleReloadForNonPrimaryPlaylist() throws Exception {
|
||||
public void start_lowLatencyScheduleReloadForPlayingButNonPrimaryPlaylist() throws Exception {
|
||||
List<HttpUrl> httpUrls =
|
||||
enqueueWebServerResponses(
|
||||
new String[] {
|
||||
"/multivariant.m3u8",
|
||||
"/media0/playlist.m3u8",
|
||||
"/english/audio-playlist.m3u8",
|
||||
"/english/audio-playlist.m3u8?_HLS_msn=14&_HLS_part=0",
|
||||
"/english/audio-playlist.m3u8?_HLS_msn=14&_HLS_part=1",
|
||||
},
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT_WITH_AUDIO_RENDITIONS),
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT),
|
||||
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_AUDIO),
|
||||
getMockResponse(
|
||||
SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_AUDIO_NEXT),
|
||||
getMockResponse(
|
||||
SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_AUDIO_NEXT2));
|
||||
|
||||
DefaultHlsPlaylistTracker defaultHlsPlaylistTracker =
|
||||
new DefaultHlsPlaylistTracker(
|
||||
dataType -> new DefaultHttpDataSource.Factory().createDataSource(),
|
||||
new DefaultLoadErrorHandlingPolicy(),
|
||||
new DefaultHlsPlaylistParserFactory());
|
||||
AtomicInteger playlistChangedCounter = new AtomicInteger();
|
||||
AtomicReference<TimeoutException> audioPlaylistRefreshExceptionRef = new AtomicReference<>();
|
||||
defaultHlsPlaylistTracker.addListener(
|
||||
new HlsPlaylistTracker.PlaylistEventListener() {
|
||||
@Override
|
||||
public void onPlaylistChanged() {
|
||||
playlistChangedCounter.addAndGet(1);
|
||||
// Upon the first call of onPlaylistChanged(), we simulate the situation that the first
|
||||
// audio rendition is chosen for playback.
|
||||
Uri url = defaultHlsPlaylistTracker.getMultivariantPlaylist().audios.get(0).url;
|
||||
if (!defaultHlsPlaylistTracker.isSnapshotValid(url)) {
|
||||
defaultHlsPlaylistTracker.refreshPlaylist(url);
|
||||
try {
|
||||
// Make sure that the audio playlist has been refreshed and we've got a playlist
|
||||
// snapshot of it.
|
||||
RobolectricUtil.runMainLooperUntil(
|
||||
() -> defaultHlsPlaylistTracker.isSnapshotValid(url));
|
||||
} catch (TimeoutException e) {
|
||||
audioPlaylistRefreshExceptionRef.set(e);
|
||||
}
|
||||
// Simulate the operations in HlsChunkSource where we keep loading and get a playlist
|
||||
// snapshot of the given url when there is a valid snapshot available.
|
||||
defaultHlsPlaylistTracker.getPlaylistSnapshot(url, /* isForPlayback= */ true);
|
||||
// We have to force the expected audio playlists to load in the first call of
|
||||
// onPlaylistChanged(), as once this method returned for "/media0/playlist.m3u8", the
|
||||
// DefaultHlsPlaylistTracker will continue reloading the primary playlist, and it's
|
||||
// hard to make the order of loading primary and audio playlists deterministic. Thus,
|
||||
// we will verify if audio playlists are reloaded as expected first, and ignore the
|
||||
// reloading of primary playlists, whose behaviour was already verified in the other
|
||||
// tests.
|
||||
try {
|
||||
RobolectricUtil.runMainLooperUntil(() -> playlistChangedCounter.get() >= 4);
|
||||
} catch (TimeoutException e) {
|
||||
audioPlaylistRefreshExceptionRef.set(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPlaylistError(
|
||||
Uri url, LoadErrorHandlingPolicy.LoadErrorInfo loadErrorInfo, boolean forceRetry) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
defaultHlsPlaylistTracker.start(
|
||||
Uri.parse(mockWebServer.url("/multivariant.m3u8").toString()),
|
||||
new MediaSourceEventListener.EventDispatcher(),
|
||||
mediaPlaylist -> {});
|
||||
RobolectricUtil.runMainLooperUntil(() -> playlistChangedCounter.get() >= 4);
|
||||
defaultHlsPlaylistTracker.stop();
|
||||
|
||||
assertThat(audioPlaylistRefreshExceptionRef.get()).isNull();
|
||||
assertRequestUrlsCalled(httpUrls);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void start_lowLatencyNotScheduleReloadForNonPlayingPlaylist() throws Exception {
|
||||
List<HttpUrl> httpUrls =
|
||||
enqueueWebServerResponses(
|
||||
new String[] {
|
||||
|
@ -0,0 +1,18 @@
|
||||
#EXTM3U
|
||||
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES
|
||||
#EXT-X-TARGETDURATION:4
|
||||
#EXT-X-PART-INF:PART-TARGET=1.000000
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-MEDIA-SEQUENCE:10
|
||||
#EXTINF:4.00000,
|
||||
fileSequence10.aac
|
||||
#EXTINF:4.00000,
|
||||
fileSequence11.aac
|
||||
#EXTINF:4.00000,
|
||||
fileSequence12.aac
|
||||
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.0.aac"
|
||||
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.1.aac"
|
||||
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.2.aac"
|
||||
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.3.aac"
|
||||
#EXTINF:4.00000,
|
||||
fileSequence13.aac
|
@ -0,0 +1,19 @@
|
||||
#EXTM3U
|
||||
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES
|
||||
#EXT-X-TARGETDURATION:4
|
||||
#EXT-X-PART-INF:PART-TARGET=1.000000
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-MEDIA-SEQUENCE:10
|
||||
#EXTINF:4.00000,
|
||||
fileSequence10.aac
|
||||
#EXTINF:4.00000,
|
||||
fileSequence11.aac
|
||||
#EXTINF:4.00000,
|
||||
fileSequence12.aac
|
||||
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.0.aac"
|
||||
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.1.aac"
|
||||
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.2.aac"
|
||||
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.3.aac"
|
||||
#EXTINF:4.00000,
|
||||
fileSequence13.aac
|
||||
#EXT-X-PART:DURATION=1.00000,URI="fileSequence14.0.aac"
|
@ -0,0 +1,20 @@
|
||||
#EXTM3U
|
||||
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES
|
||||
#EXT-X-TARGETDURATION:4
|
||||
#EXT-X-PART-INF:PART-TARGET=1.000000
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-MEDIA-SEQUENCE:10
|
||||
#EXTINF:4.00000,
|
||||
fileSequence10.aac
|
||||
#EXTINF:4.00000,
|
||||
fileSequence11.aac
|
||||
#EXTINF:4.00000,
|
||||
fileSequence12.aac
|
||||
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.0.aac"
|
||||
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.1.aac"
|
||||
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.2.aac"
|
||||
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.3.aac"
|
||||
#EXTINF:4.00000,
|
||||
fileSequence13.aac
|
||||
#EXT-X-PART:DURATION=1.00000,URI="fileSequence14.0.aac"
|
||||
#EXT-X-PART:DURATION=1.00000,URI="fileSequence14.1.aac"
|
@ -0,0 +1,10 @@
|
||||
#EXTM3U
|
||||
#EXT-X-INDEPENDENT-SEGMENTS
|
||||
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="English", DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en", URI="english/audio-playlist.m3u8"
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Deutsch", DEFAULT=NO,AUTOSELECT=YES,LANGUAGE="de", URI="german/audio-playlist.m3u8"
|
||||
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=2000000,CODECS="avc1.640028,mp4a.40.2",AUDIO="aac"
|
||||
media0/playlist.m3u8
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="avc1.640028,mp4a.40.2",AUDIO="aac"
|
||||
media1/playlist.m3u8
|
Loading…
x
Reference in New Issue
Block a user