diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bfbbe540f6..772d311576 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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.): diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java index 4e19ae8eba..8722f3ffd0 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java @@ -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 { diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java index 80c2212cb2..8c6bcd09e2 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsSampleStreamWrapper.java @@ -565,6 +565,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; sampleQueue.preRelease(); } } + chunkSource.reset(); loader.release(this); handler.removeCallbacksAndMessages(null); released = true; diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java index 32ec51e4be..9e2d786d18 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java @@ -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()); } } diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistTracker.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistTracker.java index d7eff3ca05..d08874232a 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistTracker.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/HlsPlaylistTracker.java @@ -235,4 +235,13 @@ public interface HlsPlaylistTracker { * @return True if the content is live. False otherwise. */ boolean isLive(); + + /** + * Deactivate the playlist for playback. + * + *
The default implementation is a no-op.
+ *
+ * @param url The {@link Uri} of the playlist to deactivate for playback.
+ */
+ default void deactivatePlaylistForPlayback(Uri url) {}
}
diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTrackerTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTrackerTest.java
index 1a23152d88..23000b89c7 100644
--- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTrackerTest.java
+++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTrackerTest.java
@@ -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