From 6e8af81ddce0d0c38d9a12a02d1fc1dd5f7dddd1 Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 7 Jan 2021 14:48:39 +0000 Subject: [PATCH] Reload HLS media playlist when merging delta update fails Issue: #5011 PiperOrigin-RevId: 350550204 --- .../playlist/DefaultHlsPlaylistTracker.java | 15 +++-- .../hls/playlist/HlsPlaylistParser.java | 62 ++++++++++--------- .../DefaultHlsPlaylistTrackerTest.java | 18 +++--- ...n_skip_until_and_block_reload_next_skipped | 2 +- ...dia_can_skip_until_full_reload_after_error | 17 +++++ 5 files changed, 72 insertions(+), 42 deletions(-) create mode 100644 testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_until_full_reload_after_error diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java index afc55a2bfa..2a69f5c6af 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTracker.java @@ -603,11 +603,16 @@ public final class DefaultHlsPlaylistTracker 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) { + 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) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 1b4fd34565..62357ecdea 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -69,6 +69,9 @@ import org.checkerframework.checker.nullness.qual.PolyNull; */ public final class HlsPlaylistParser implements ParsingLoadable.Parser { + /** 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"; @@ -744,36 +747,37 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser= 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); diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTrackerTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTrackerTest.java index 7fd12abb4f..80060b15f8 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTrackerTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/DefaultHlsPlaylistTrackerTest.java @@ -37,7 +37,6 @@ import okhttp3.mockwebserver.MockWebServer; import okio.Buffer; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -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 = @@ -168,18 +169,21 @@ public class DefaultHlsPlaylistTrackerTest { 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 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 mediaPlaylists = runPlaylistTrackerAndCollectMediaPlaylists( @@ -192,8 +196,8 @@ public class DefaultHlsPlaylistTrackerTest { 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 diff --git a/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_until_and_block_reload_next_skipped b/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_until_and_block_reload_next_skipped index 4c2b636862..a3aa44fb4e 100644 --- a/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_until_and_block_reload_next_skipped +++ b/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_until_and_block_reload_next_skipped @@ -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, diff --git a/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_until_full_reload_after_error b/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_until_full_reload_after_error new file mode 100644 index 0000000000..ae83021ae9 --- /dev/null +++ b/testdata/src/test/assets/media/m3u8/live_low_latency_media_can_skip_until_full_reload_after_error @@ -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