Avoid non-primary playlists continuously reloading for LL-HLS streams

For LL-HLS, the non-primary playlists originally keep reloading even after the primary playlist has been changed to another one. The reason being this is to check if the hinted(#EXT-X-PRELOAD-HINT) resource has been published or removed. If removed, the loading of it should be canceled, per the suggestion in the HLS spec:

"A Client SHOULD cancel a request for a hinted resource if it is not present in a subsequent Playlist update, such as in an EXT-X-PRELOAD-HINT tag or as part of another tag such as EXT-X-PART.  The client SHOULD ignore the results of such requests."

However, keeping the non-primary playlists reloading is not optimal. As a solution, we trigger the playlist reloading only when there is a preload chunk loading instead of every time after we have processed the playlist. Compared to the original implementation, this will save the requests of reloading non-primary playlist after we have taken action upon the preload chunk being published or removed.

Issue: androidx/media#1240
PiperOrigin-RevId: 626038032
This commit is contained in:
tianyifeng 2024-04-18 08:09:17 -07:00 committed by Copybara-Service
parent e1c62df256
commit 50fefe698d
5 changed files with 97 additions and 8 deletions

View File

@ -43,6 +43,8 @@
delegated in `HlsSampleStreamWrapper` with an incorrect offset causing delegated in `HlsSampleStreamWrapper` with an incorrect offset causing
an `IndexOutOfBoundsException` or an `IllegalArgumentException` an `IndexOutOfBoundsException` or an `IllegalArgumentException`
([#1002](https://github.com/androidx/media/issues/1002)). ([#1002](https://github.com/androidx/media/issues/1002)).
* Fix bug where non-primary playlists keep reloading for LL-HLS streams
([#1240](https://github.com/androidx/media/issues/1240)).
* DASH Extension: * DASH Extension:
* Smooth Streaming Extension: * Smooth Streaming Extension:
* RTSP Extension: * RTSP Extension:

View File

@ -15,6 +15,7 @@
*/ */
package androidx.media3.exoplayer.hls; package androidx.media3.exoplayer.hls;
import static androidx.media3.exoplayer.hls.HlsChunkSource.CHUNK_PUBLICATION_STATE_PRELOAD;
import static androidx.media3.exoplayer.hls.HlsChunkSource.CHUNK_PUBLICATION_STATE_PUBLISHED; import static androidx.media3.exoplayer.hls.HlsChunkSource.CHUNK_PUBLICATION_STATE_PUBLISHED;
import static androidx.media3.exoplayer.hls.HlsChunkSource.CHUNK_PUBLICATION_STATE_REMOVED; import static androidx.media3.exoplayer.hls.HlsChunkSource.CHUNK_PUBLICATION_STATE_REMOVED;
import static androidx.media3.exoplayer.trackselection.TrackSelectionUtil.createFallbackOptions; import static androidx.media3.exoplayer.trackselection.TrackSelectionUtil.createFallbackOptions;
@ -111,7 +112,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** /**
* Called to schedule a {@link #continueLoading(LoadingInfo)} call when the playlist referred by * Called to schedule a {@link #continueLoading(LoadingInfo)} call when the playlist referred by
* the given url changes. * the given url changes, or it requires a refresh to check whether the hinted resource has been
* published or removed.
*
* <p>Note: This method will be called on a later handler loop than the one on which {@link
* #onPlaylistUpdated()} is invoked.
*/ */
void onPlaylistRefreshRequired(Uri playlistUrl); void onPlaylistRefreshRequired(Uri playlistUrl);
} }
@ -543,6 +548,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
int chunkState = chunkSource.getChunkPublicationState(lastMediaChunk); int chunkState = chunkSource.getChunkPublicationState(lastMediaChunk);
if (chunkState == CHUNK_PUBLICATION_STATE_PUBLISHED) { if (chunkState == CHUNK_PUBLICATION_STATE_PUBLISHED) {
lastMediaChunk.publish(); lastMediaChunk.publish();
} else if (chunkState == CHUNK_PUBLICATION_STATE_PRELOAD) {
handler.post(() -> callback.onPlaylistRefreshRequired(lastMediaChunk.playlistUrl));
} else if (chunkState == CHUNK_PUBLICATION_STATE_REMOVED } else if (chunkState == CHUNK_PUBLICATION_STATE_REMOVED
&& !loadingFinished && !loadingFinished
&& loader.isLoading()) { && loader.isLoading()) {

View File

@ -763,13 +763,10 @@ public final class DefaultHlsPlaylistTracker
} }
earliestNextLoadTimeMs = earliestNextLoadTimeMs =
currentTimeMs + Util.usToMs(durationUntilNextLoadUs) - loadEventInfo.loadDurationMs; currentTimeMs + Util.usToMs(durationUntilNextLoadUs) - loadEventInfo.loadDurationMs;
// Schedule a load if this is the primary playlist or a playlist of a low-latency stream and // Schedule a load if this is the primary playlist and it doesn't have an end tag. Else the
// it doesn't have an end tag. Else the next load will be scheduled when refreshPlaylist is // next load will be scheduled when refreshPlaylist is called, or when this playlist becomes
// called, or when this playlist becomes the primary. // the primary.
boolean scheduleLoad = if (playlistUrl.equals(primaryMediaPlaylistUrl) && !playlistSnapshot.hasEndTag) {
playlistSnapshot.partTargetDurationUs != C.TIME_UNSET
|| playlistUrl.equals(primaryMediaPlaylistUrl);
if (scheduleLoad && !playlistSnapshot.hasEndTag) {
loadPlaylistInternal(getMediaPlaylistUriForReload()); loadPlaylistInternal(getMediaPlaylistUriForReload());
} }
} }

View File

@ -22,6 +22,7 @@ import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DefaultHttpDataSource; import androidx.media3.datasource.DefaultHttpDataSource;
import androidx.media3.exoplayer.source.MediaSourceEventListener; import androidx.media3.exoplayer.source.MediaSourceEventListener;
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy; import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.test.utils.TestUtil; import androidx.media3.test.utils.TestUtil;
import androidx.media3.test.utils.robolectric.RobolectricUtil; import androidx.media3.test.utils.robolectric.RobolectricUtil;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
@ -31,6 +32,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.MockWebServer;
@ -365,6 +367,85 @@ public class DefaultHlsPlaylistTrackerTest {
assertThat(mediaPlaylists.get(1).trailingParts).hasSize(2); assertThat(mediaPlaylists.get(1).trailingParts).hasSize(2);
} }
@Test
public void start_lowLatencyNotScheduleReloadForNonPrimaryPlaylist() throws Exception {
List<HttpUrl> httpUrls =
enqueueWebServerResponses(
new String[] {
"/multivariant.m3u8",
"/media0/playlist.m3u8",
"/media1/playlist.m3u8",
"/media1/playlist.m3u8",
"/media1/playlist.m3u8?_HLS_msn=14&_HLS_part=0",
},
getMockResponse(SAMPLE_M3U8_LIVE_MULTIVARIANT),
getMockResponse(
SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_PRELOAD),
getMockResponse(
SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_PRELOAD),
getMockResponse(
SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_PRELOAD),
getMockResponse(
SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_PRELOAD_NEXT));
DefaultHlsPlaylistTracker defaultHlsPlaylistTracker =
new DefaultHlsPlaylistTracker(
dataType -> new DefaultHttpDataSource.Factory().createDataSource(),
new DefaultLoadErrorHandlingPolicy(),
new DefaultHlsPlaylistParserFactory());
List<HlsMediaPlaylist> mediaPlaylists = new ArrayList<>();
AtomicInteger playlistCounter = new AtomicInteger();
AtomicReference<TimeoutException> primaryPlaylistChangeExceptionRef = new AtomicReference<>();
defaultHlsPlaylistTracker.addListener(
new HlsPlaylistTracker.PlaylistEventListener() {
@Override
public void onPlaylistChanged() {
// Upon the first call of onPlaylistChanged(), we simulate the situation that the
// primary playlist url changes.
Uri url = defaultHlsPlaylistTracker.getMultivariantPlaylist().mediaPlaylistUrls.get(1);
if (defaultHlsPlaylistTracker.isSnapshotValid(url)) {
return;
}
defaultHlsPlaylistTracker.refreshPlaylist(url);
try {
// Make sure that the playlist for the new url has been refreshed and set it as the
// current primary playlist, before this method returns.
RobolectricUtil.runMainLooperUntil(
() ->
defaultHlsPlaylistTracker.getPlaylistSnapshot(url, /* isForPlayback= */ true)
!= null);
} catch (TimeoutException e) {
primaryPlaylistChangeExceptionRef.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 -> {
mediaPlaylists.add(mediaPlaylist);
playlistCounter.addAndGet(1);
});
RobolectricUtil.runMainLooperUntil(() -> playlistCounter.get() >= 2);
defaultHlsPlaylistTracker.stop();
assertThat(primaryPlaylistChangeExceptionRef.get()).isNull();
assertRequestUrlsCalled(httpUrls);
assertThat(mediaPlaylists.get(0).mediaSequence).isEqualTo(10);
assertThat(mediaPlaylists.get(0).segments).hasSize(4);
assertThat(mediaPlaylists.get(0).trailingParts).hasSize(1);
assertThat(mediaPlaylists.get(1).mediaSequence).isEqualTo(10);
assertThat(mediaPlaylists.get(1).segments).hasSize(4);
assertThat(mediaPlaylists.get(1).trailingParts).hasSize(2);
}
@Test @Test
public void start_httpBadRequest_forcesFullNonBlockingPlaylistRequest() public void start_httpBadRequest_forcesFullNonBlockingPlaylistRequest()
throws IOException, TimeoutException, InterruptedException { throws IOException, TimeoutException, InterruptedException {

View File

@ -3,3 +3,5 @@
#EXT-X-STREAM-INF:BANDWIDTH=2000000,CODECS="avc1.640028,mp4a.40.2" #EXT-X-STREAM-INF:BANDWIDTH=2000000,CODECS="avc1.640028,mp4a.40.2"
media0/playlist.m3u8 media0/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="avc1.640028,mp4a.40.2"
media1/playlist.m3u8