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
|
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:
|
||||||
|
@ -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()) {
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user