Skip to content

Commit

Permalink
Fix bug where enabling CMCD for HLS live streams causes error
Browse files Browse the repository at this point in the history
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: #1395
PiperOrigin-RevId: 642961141
  • Loading branch information
rohitjoins authored and Copybara-Service committed Jun 13, 2024
1 parent 4f691a7 commit d4802a4
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 6 deletions.
3 changes: 3 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -521,13 +521,16 @@ public void getNextChunk(
? 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -305,6 +309,96 @@ public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCmcdHttpReq
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 =
Expand Down

0 comments on commit d4802a4

Please sign in to comment.