Skip to content

Commit

Permalink
Avoid non-primary playlists continuously reloading for LL-HLS streams
Browse files Browse the repository at this point in the history
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: #1240
PiperOrigin-RevId: 626038032
  • Loading branch information
tianyif authored and Copybara-Service committed Apr 18, 2024
1 parent e1c62df commit 50fefe6
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 8 deletions.
2 changes: 2 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
delegated in `HlsSampleStreamWrapper` with an incorrect offset causing
an `IndexOutOfBoundsException` or an `IllegalArgumentException`
([#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:
* Smooth Streaming Extension:
* RTSP Extension:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
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_REMOVED;
import static androidx.media3.exoplayer.trackselection.TrackSelectionUtil.createFallbackOptions;
Expand Down Expand Up @@ -111,7 +112,11 @@ public interface Callback extends SequenceableLoader.Callback<HlsSampleStreamWra

/**
* 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);
}
Expand Down Expand Up @@ -543,6 +548,8 @@ public void onPlaylistUpdated() {
int chunkState = chunkSource.getChunkPublicationState(lastMediaChunk);
if (chunkState == CHUNK_PUBLICATION_STATE_PUBLISHED) {
lastMediaChunk.publish();
} else if (chunkState == CHUNK_PUBLICATION_STATE_PRELOAD) {
handler.post(() -> callback.onPlaylistRefreshRequired(lastMediaChunk.playlistUrl));
} else if (chunkState == CHUNK_PUBLICATION_STATE_REMOVED
&& !loadingFinished
&& loader.isLoading()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -763,13 +763,10 @@ private void processLoadedPlaylist(
}
earliestNextLoadTimeMs =
currentTimeMs + Util.usToMs(durationUntilNextLoadUs) - loadEventInfo.loadDurationMs;
// Schedule a load if this is the primary playlist or a playlist of a low-latency stream 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.
boolean scheduleLoad =
playlistSnapshot.partTargetDurationUs != C.TIME_UNSET
|| playlistUrl.equals(primaryMediaPlaylistUrl);
if (scheduleLoad && !playlistSnapshot.hasEndTag) {
// 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) {
loadPlaylistInternal(getMediaPlaylistUriForReload());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import androidx.media3.datasource.DefaultHttpDataSource;
import androidx.media3.exoplayer.source.MediaSourceEventListener;
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import androidx.media3.test.utils.TestUtil;
import androidx.media3.test.utils.robolectric.RobolectricUtil;
import androidx.test.core.app.ApplicationProvider;
Expand All @@ -31,6 +32,7 @@
import java.util.List;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import okhttp3.HttpUrl;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
Expand Down Expand Up @@ -365,6 +367,85 @@ public void start_playlistCanBlockReloadLowLatencyFullSegmentWithPreloadPart_ign
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
public void start_httpBadRequest_forcesFullNonBlockingPlaylistRequest()
throws IOException, TimeoutException, InterruptedException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@

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

0 comments on commit 50fefe6

Please sign in to comment.