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:
parent
e1c62df256
commit
50fefe698d
@ -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:
|
||||
|
@ -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;
|
||||
@ -111,7 +112,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@ -543,6 +548,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
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()) {
|
||||
|
@ -763,13 +763,10 @@ public final class DefaultHlsPlaylistTracker
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import androidx.media3.datasource.DataSource;
|
||||
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;
|
||||
@ -31,6 +32,7 @@ import java.util.ArrayList;
|
||||
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;
|
||||
@ -365,6 +367,85 @@ public class DefaultHlsPlaylistTrackerTest {
|
||||
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 {
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user