Schedule refresh for all the playing playlists for HLS live stream

Issue: androidx/media#1240
PiperOrigin-RevId: 642927082
This commit is contained in:
tianyifeng 2024-06-13 03:49:31 -07:00 committed by Copybara-Service
parent ca51ed649b
commit 86a60e6ec2
10 changed files with 226 additions and 7 deletions

View File

@ -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.):

View File

@ -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 {

View File

@ -565,6 +565,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
sampleQueue.preRelease();
}
}
chunkSource.reset();
loader.release(this);
handler.removeCallbacksAndMessages(null);
released = true;

View File

@ -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());
}
}

View File

@ -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) {}
}

View File

@ -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[] {

View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -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