Skip to content

Commit

Permalink
Reload HLS media playlist when merging delta update fails
Browse files Browse the repository at this point in the history
Issue: #5011
PiperOrigin-RevId: 350550204
  • Loading branch information
marcbaechinger authored and icbaker committed Jan 8, 2021
1 parent 6dec832 commit 6e8af81
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 42 deletions.
Expand Up @@ -603,11 +603,16 @@ public LoadErrorAction onLoadError(
loadDurationMs,
loadable.bytesLoaded());
boolean isBlockingRequest = loadable.getUri().getQueryParameter(BLOCK_MSN_PARAM) != null;
if (isBlockingRequest && error instanceof HttpDataSource.InvalidResponseCodeException) {
int responseCode = ((HttpDataSource.InvalidResponseCodeException) error).responseCode;
if (responseCode == 400 || responseCode == 503) {
// Intercept bad request and service unavailable to force a full, non-blocking request
// (see RFC 8216, section 6.2.5.2).
boolean deltaUpdateFailed = error instanceof HlsPlaylistParser.DeltaUpdateException;
if (isBlockingRequest || deltaUpdateFailed) {
int responseCode = Integer.MAX_VALUE;
if (error instanceof HttpDataSource.InvalidResponseCodeException) {

This comment has been minimized.

Copy link
@stevemayhew

stevemayhew Jan 10, 2023

Contributor

I'm working on an issue with stale playlists snapshots, the code below:

    private void loadPlaylistImmediately(Uri playlistRequestUri) {
      ParsingLoadable.Parser<HlsPlaylist> mediaPlaylistParser =
          playlistParserFactory.createPlaylistParser(masterPlaylist, playlistSnapshot);

Potentially uses a very stale playlistSnapshot (for example, if you change tracks the previous MediaPlaylistBundle can stay around for longer the window size, so all of the segments the snapshot contains age out. In this case I think you will repeatedly throw the HlsPlaylistParser.DeltaUpdateException until the stale snapshot is discarded.

What I did is:

  1. at this line, simply null the old playlistSnapshot before the reload.
  2. The code below can avoid the exception completely
    private void loadPlaylistImmediately(Uri playlistRequestUri) {

      // Try to avoid using stale playlist
      if (playlistSnapshot != null && ! isSnapshotValid()) {
        playlistSnapshot = null;
      }

Suspect you also need to add to the request URL to avoid having the origin send a partial playlist, I think this was added in later versions? @marcbaechinger

responseCode = ((HttpDataSource.InvalidResponseCodeException) error).responseCode;
}
if (deltaUpdateFailed || responseCode == 400 || responseCode == 503) {
// Intercept failed delta updates and blocking requests producing a Bad Request (400) and
// Service Unavailable (503). In such cases, force a full, non-blocking request (see RFC
// 8216, section 6.2.5.2 and 6.3.7).
earliestNextLoadTimeMs = SystemClock.elapsedRealtime();
loadPlaylist();
castNonNull(eventDispatcher)
Expand Down
Expand Up @@ -69,6 +69,9 @@
*/
public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlaylist> {

/** Exception thrown when merging a delta update fails. */
public static final class DeltaUpdateException extends IOException {}

private static final String LOG_TAG = "HlsPlaylistParser";

private static final String PLAYLIST_HEADER = "#EXTM3U";
Expand Down Expand Up @@ -744,36 +747,37 @@ private static HlsMediaPlaylist parseMediaPlaylist(
checkState(previousMediaPlaylist != null && segments.isEmpty());
int startIndex = (int) (mediaSequence - castNonNull(previousMediaPlaylist).mediaSequence);
int endIndex = startIndex + skippedSegmentCount;
if (startIndex >= 0 && endIndex <= previousMediaPlaylist.segments.size()) {
// Merge only if all skipped segments are available in the previous playlist.
for (int i = startIndex; i < endIndex; i++) {
Segment segment = previousMediaPlaylist.segments.get(i);
if (mediaSequence != previousMediaPlaylist.mediaSequence) {
// If the media sequences of the playlists are not the same, we need to recreate the
// object with the updated relative start time and the relative discontinuity
// sequence. With identical playlist media sequences these values do not change.
int newRelativeDiscontinuitySequence =
previousMediaPlaylist.discontinuitySequence
- playlistDiscontinuitySequence
+ segment.relativeDiscontinuitySequence;
segment = segment.copyWith(segmentStartTimeUs, newRelativeDiscontinuitySequence);
}
segments.add(segment);
segmentStartTimeUs += segment.durationUs;
partStartTimeUs = segmentStartTimeUs;
if (segment.byteRangeLength != C.LENGTH_UNSET) {
segmentByteRangeOffset = segment.byteRangeOffset + segment.byteRangeLength;
}
relativeDiscontinuitySequence = segment.relativeDiscontinuitySequence;
initializationSegment = segment.initializationSegment;
cachedDrmInitData = segment.drmInitData;
fullSegmentEncryptionKeyUri = segment.fullSegmentEncryptionKeyUri;
if (segment.encryptionIV == null
|| !segment.encryptionIV.equals(Long.toHexString(segmentMediaSequence))) {
fullSegmentEncryptionIV = segment.encryptionIV;
}
segmentMediaSequence++;
if (startIndex < 0 || endIndex > previousMediaPlaylist.segments.size()) {
// Throw to force a reload if not all segments are available in the previous playlist.
throw new DeltaUpdateException();
}
for (int i = startIndex; i < endIndex; i++) {
Segment segment = previousMediaPlaylist.segments.get(i);
if (mediaSequence != previousMediaPlaylist.mediaSequence) {
// If the media sequences of the playlists are not the same, we need to recreate the
// object with the updated relative start time and the relative discontinuity
// sequence. With identical playlist media sequences these values do not change.
int newRelativeDiscontinuitySequence =
previousMediaPlaylist.discontinuitySequence
- playlistDiscontinuitySequence
+ segment.relativeDiscontinuitySequence;
segment = segment.copyWith(segmentStartTimeUs, newRelativeDiscontinuitySequence);
}
segments.add(segment);
segmentStartTimeUs += segment.durationUs;
partStartTimeUs = segmentStartTimeUs;
if (segment.byteRangeLength != C.LENGTH_UNSET) {
segmentByteRangeOffset = segment.byteRangeOffset + segment.byteRangeLength;
}
relativeDiscontinuitySequence = segment.relativeDiscontinuitySequence;
initializationSegment = segment.initializationSegment;
cachedDrmInitData = segment.drmInitData;
fullSegmentEncryptionKeyUri = segment.fullSegmentEncryptionKeyUri;
if (segment.encryptionIV == null
|| !segment.encryptionIV.equals(Long.toHexString(segmentMediaSequence))) {
fullSegmentEncryptionIV = segment.encryptionIV;
}
segmentMediaSequence++;
}
} else if (line.startsWith(TAG_KEY)) {
String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions);
Expand Down
Expand Up @@ -37,7 +37,6 @@
import okio.Buffer;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;

Expand All @@ -50,6 +49,8 @@ public class DefaultHlsPlaylistTrackerTest {
"media/m3u8/live_low_latency_master_media_uri_with_param";
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 =
"media/m3u8/live_low_latency_media_can_skip_until_full_reload_after_error";
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_DATERANGES =
"media/m3u8/live_low_latency_media_can_skip_dateranges";
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED =
Expand Down Expand Up @@ -168,18 +169,21 @@ public void start_playlistCanSkip_requestsDeltaUpdateAndExpandsSkippedSegments()
assertThat(mergedPlaylist.segments.get(1).relativeStartTimeUs).isEqualTo(4000000);
}

@Ignore // Test disabled because playlist delta updates are temporarily disabled.
@Test
public void start_playlistCanSkip_missingSegments_correctedMediaSequence()
public void start_playlistCanSkip_missingSegments_reloadsWithoutSkipping()
throws IOException, TimeoutException, InterruptedException {
List<HttpUrl> httpUrls =
enqueueWebServerResponses(
new String[] {
"/master.m3u8", "/media0/playlist.m3u8", "/media0/playlist.m3u8?_HLS_skip=YES"
"/master.m3u8",
"/media0/playlist.m3u8",
"/media0/playlist.m3u8?_HLS_skip=YES",
"/media0/playlist.m3u8"
},
getMockResponse(SAMPLE_M3U8_LIVE_MASTER),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED_MEDIA_SEQUENCE_NO_OVERLAPPING));
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_SKIPPED_MEDIA_SEQUENCE_NO_OVERLAPPING),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_FULL_RELOAD_AFTER_ERROR));

List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists(
Expand All @@ -192,8 +196,8 @@ public void start_playlistCanSkip_missingSegments_correctedMediaSequence()
assertThat(initialPlaylistWithAllSegments.mediaSequence).isEqualTo(10);
assertThat(initialPlaylistWithAllSegments.segments).hasSize(6);
HlsMediaPlaylist mergedPlaylist = mediaPlaylists.get(1);
assertThat(mergedPlaylist.mediaSequence).isEqualTo(22);
assertThat(mergedPlaylist.segments).hasSize(4);
assertThat(mergedPlaylist.mediaSequence).isEqualTo(20);
assertThat(mergedPlaylist.segments).hasSize(6);
}

@Test
Expand Down
Expand Up @@ -2,8 +2,8 @@
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24,CAN-BLOCK-RELOAD=YES
#EXT-X-TARGETDURATION:4
#EXT-X-VERSION:3
#EXT-X-SKIP:SKIPPED-SEGMENTS=2
#EXT-X-MEDIA-SEQUENCE:12
#EXT-X-SKIP:SKIPPED-SEGMENTS=2
#EXTINF:4.00000,
fileSequence14.ts
#EXTINF:4.00000,
Expand Down
@@ -0,0 +1,17 @@
#EXTM3U
#EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=24
#EXT-X-TARGETDURATION:4
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:20
#EXTINF:4.00000,
fileSequence20.ts
#EXTINF:4.00000,
fileSequence21.ts
#EXTINF:4.00000,
fileSequence22.ts
#EXTINF:4.00000,
fileSequence23.ts
#EXTINF:4.00000,
fileSequence24.ts
#EXTINF:4.00000,
fileSequence25.ts

0 comments on commit 6e8af81

Please sign in to comment.