From d4802a429b989463ee922794f7f6d01ab28d28fc Mon Sep 17 00:00:00 2001 From: rohks Date: Thu, 13 Jun 2024 06:09:41 -0700 Subject: [PATCH] Fix bug where enabling CMCD for HLS live streams causes error Determine `nextMediaSequence` and `nextPartIndex` based on the last `SegmentBaseHolder` instance, as it can update `mediaSequence` and `partIndex` depending on whether the HLS playlist has trailing parts or not. Issue: androidx/media#1395 PiperOrigin-RevId: 642961141 --- RELEASENOTES.md | 3 + .../media3/exoplayer/hls/HlsChunkSource.java | 15 +-- .../exoplayer/hls/HlsChunkSourceTest.java | 94 +++++++++++++++++++ 3 files changed, 106 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 772d311576..12530e32bf 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -133,6 +133,9 @@ This release includes the following changes since the schedule its work loop as renderers can make progress. * Use data class for `LoadControl` methods instead of individual parameters. + * Fix bug where enabling CMCD for HLS live streams causes + `ArrayIndexOutOfBoundsException` + ([#1395](https://github.com/androidx/media/issues/1395)). * Transformer: * Work around a decoder bug where the number of audio channels was capped at stereo when handling PCM input. diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java index 8722f3ffd0..6867a3291e 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java @@ -521,13 +521,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ? CmcdData.Factory.OBJECT_TYPE_MUXED_AUDIO_AND_VIDEO : CmcdData.Factory.getObjectType(trackSelection)); - long nextChunkMediaSequence = - partIndex == C.INDEX_UNSET - ? (chunkMediaSequence == C.INDEX_UNSET ? C.INDEX_UNSET : chunkMediaSequence + 1) - : chunkMediaSequence; - int nextPartIndex = partIndex == C.INDEX_UNSET ? C.INDEX_UNSET : partIndex + 1; + long nextMediaSequence = + segmentBaseHolder.partIndex == C.INDEX_UNSET + ? segmentBaseHolder.mediaSequence + 1 + : segmentBaseHolder.mediaSequence; + int nextPartIndex = + segmentBaseHolder.partIndex == C.INDEX_UNSET + ? C.INDEX_UNSET + : segmentBaseHolder.partIndex + 1; SegmentBaseHolder nextSegmentBaseHolder = - getNextSegmentHolder(playlist, nextChunkMediaSequence, nextPartIndex); + getNextSegmentHolder(playlist, nextMediaSequence, nextPartIndex); if (nextSegmentBaseHolder != null) { Uri uri = UriUtil.resolveToUri(playlist.baseUri, segmentBaseHolder.segmentBase.url); Uri nextUri = UriUtil.resolveToUri(playlist.baseUri, nextSegmentBaseHolder.segmentBase.url); diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java index d09b9b9c79..077ce0cb86 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java @@ -58,6 +58,10 @@ public class HlsChunkSourceTest { private static final String PLAYLIST = "media/m3u8/media_playlist"; private static final String PLAYLIST_INDEPENDENT_SEGMENTS = "media/m3u8/media_playlist_independent_segments"; + private static final String PLAYLIST_LIVE_LOW_LATENCY_SEGEMENTS_ONLY = + "media/m3u8/live_low_latency_segments_only"; + private static final String PLAYLIST_LIVE_LOW_LATENCY_SEGEMENTS_AND_PARTS = + "media/m3u8/live_low_latency_segments_and_parts"; private static final String PLAYLIST_EMPTY = "media/m3u8/media_playlist_empty"; private static final Uri PLAYLIST_URI = Uri.parse("http://example.com/"); private static final long PLAYLIST_START_PERIOD_OFFSET_US = 8_000_000L; @@ -305,6 +309,96 @@ public class HlsChunkSourceTest { assertThat(output.chunk.dataSpec.httpRequestHeaders).doesNotContainKey("CMCD-Status"); } + @Test + public void getNextChunk_forLivePlaylistWithSegmentsOnly_setsCorrectNextObjectRequest() + throws IOException { + // The live playlist contains 6 segments, each 4 seconds long. With a playlist start offset of 8 + // seconds, the total media time is 8 + 6*4 = 32 seconds. + InputStream inputStream = + TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), PLAYLIST_LIVE_LOW_LATENCY_SEGEMENTS_ONLY); + HlsMediaPlaylist playlist = + (HlsMediaPlaylist) new HlsPlaylistParser().parse(PLAYLIST_URI, inputStream); + when(mockPlaylistTracker.getPlaylistSnapshot(eq(PLAYLIST_URI), anyBoolean())) + .thenReturn(playlist); + CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT; + MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build(); + CmcdConfiguration cmcdConfiguration = + cmcdConfigurationFactory.createCmcdConfiguration(mediaItem); + HlsChunkSource testChunkSource = createHlsChunkSource(cmcdConfiguration); + HlsChunkSource.HlsChunkHolder output = new HlsChunkSource.HlsChunkHolder(); + + // A request to fetch the chunk at 27 seconds should retrieve the second-to-last segment. + testChunkSource.getNextChunk( + new LoadingInfo.Builder().setPlaybackPositionUs(27_000_000).setPlaybackSpeed(1.0f).build(), + /* loadPositionUs= */ 27_000_000, + /* queue= */ ImmutableList.of(), + /* allowEndOfStream= */ true, + output); + + // The `nor` key should point to the last segment, which is `FileSequence15.ts`. + assertThat(output.chunk.dataSpec.httpRequestHeaders) + .containsEntry("CMCD-Request", "bl=0,dl=0,nor=\"..%2FfileSequence15.ts\",nrr=\"0-\",su"); + + // A request to fetch the chunk at 31 seconds should retrieve the last segment. + testChunkSource.getNextChunk( + new LoadingInfo.Builder().setPlaybackPositionUs(31_000_000).setPlaybackSpeed(1.0f).build(), + /* loadPositionUs= */ 31_000_000, + /* queue= */ ImmutableList.of(), + /* allowEndOfStream= */ true, + output); + + // Since there are no next segments left, the `nor` key should be absent. + assertThat(output.chunk.dataSpec.httpRequestHeaders) + .containsEntry("CMCD-Request", "bl=0,dl=0,su"); + } + + @Test + public void getNextChunk_forLivePlaylistWithSegmentsAndParts_setsCorrectNextObjectRequest() + throws IOException { + // The live playlist contains 6 segments, each 4 seconds long, and two trailing parts of 1 + // second each. With a playlist start offset of 8 seconds, the total media time is 8 + 6*4 + 2*1 + // = 34 seconds. + InputStream inputStream = + TestUtil.getInputStream( + ApplicationProvider.getApplicationContext(), + PLAYLIST_LIVE_LOW_LATENCY_SEGEMENTS_AND_PARTS); + HlsMediaPlaylist playlist = + (HlsMediaPlaylist) new HlsPlaylistParser().parse(PLAYLIST_URI, inputStream); + when(mockPlaylistTracker.getPlaylistSnapshot(eq(PLAYLIST_URI), anyBoolean())) + .thenReturn(playlist); + CmcdConfiguration.Factory cmcdConfigurationFactory = CmcdConfiguration.Factory.DEFAULT; + MediaItem mediaItem = new MediaItem.Builder().setMediaId("mediaId").build(); + CmcdConfiguration cmcdConfiguration = + cmcdConfigurationFactory.createCmcdConfiguration(mediaItem); + HlsChunkSource testChunkSource = createHlsChunkSource(cmcdConfiguration); + HlsChunkSource.HlsChunkHolder output = new HlsChunkSource.HlsChunkHolder(); + + // A request to fetch the chunk at 31 seconds should retrieve the last segment. + testChunkSource.getNextChunk( + new LoadingInfo.Builder().setPlaybackPositionUs(31_000_000).setPlaybackSpeed(1.0f).build(), + /* loadPositionUs= */ 31_000_000, + /* queue= */ ImmutableList.of(), + /* allowEndOfStream= */ true, + output); + + // The `nor` key should point to the first trailing part, which is `FileSequence16.0.ts`. + assertThat(output.chunk.dataSpec.httpRequestHeaders) + .containsEntry("CMCD-Request", "bl=0,dl=0,nor=\"..%2FfileSequence16.0.ts\",nrr=\"0-\",su"); + + // A request to fetch the chunk at 34 seconds should retrieve the first trailing part. + testChunkSource.getNextChunk( + new LoadingInfo.Builder().setPlaybackPositionUs(34_000_000).setPlaybackSpeed(1.0f).build(), + /* loadPositionUs= */ 34_000_000, + /* queue= */ ImmutableList.of(), + /* allowEndOfStream= */ true, + output); + + // The `nor` key should point to the second trailing part, which is `FileSequence16.1.ts`. + assertThat(output.chunk.dataSpec.httpRequestHeaders) + .containsEntry("CMCD-Request", "bl=0,dl=0,nor=\"..%2FfileSequence16.1.ts\",nrr=\"0-\",su"); + } + @Test public void getNextChunk_chunkSourceWithCustomCmcdConfiguration_setsCmcdHttpRequestHeaders() { CmcdConfiguration.Factory cmcdConfigurationFactory =