Skip to content

Commit c683a22

Browse files
tianyifcopybara-github
authored andcommitted
[HLS] Properly handle getNextChunk for a playlist with no segments
When calculating the `segmentIndexInPlaylist` with the binary search (`stayInBound=true`), a corner case of `mediaPlaylist.segments` being empty also lead to the adjusted result of `segmentIndexInPlaylist = 0`. Then in the logic of picking up a `Part`, fetching a `Segment` may end up in `IndexOutOfBoundsException`. Thus we fix the `Part` picking logic into: * When the playlist is VOD, there is no need to pick up a `Part`, then we bypass the `Part` picking logic; * When the playlist is LIVE, and we are inside the live window: * When the `mediaPlaylist.segments` is not empty, use the old part picking logic: Depending on the `targetPositionInPlaylistUs`, either picking among the `Part`s of an existing `Segment`, or picking among the trailing parts. If we pick among the trailing parts, we should increase the media sequence by one as if we will be loading a "new segment"; * When the `mediaPlaylist.segments` is empty, we can still try to pick from the trailing parts. If we pick a part, we don't need to increase the media sequence as we have hiddenly increased it by the `stayInBound` adjustment. As a result, for VOD playlist, an empty `mediaPlaylist.segments` will lead to end of stream, and for LIVE playlist, it will lead to either loading a trailing part or refreshing the playlist. Issue: #2821 PiperOrigin-RevId: 830887686
1 parent 6384e63 commit c683a22

File tree

6 files changed

+134
-7
lines changed

6 files changed

+134
-7
lines changed

RELEASENOTES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
* Cronet extension:
3333
* RTMP extension:
3434
* HLS extension:
35+
* Properly handle fetching the next chunk for an `HlsMediaPlaylist` with
36+
no segments ([#2821](https://github.com/androidx/media/issues/2821)).
3537
* DASH extension:
3638
* Smooth Streaming extension:
3739
* RTSP extension:

libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsChunkSource.java

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -905,20 +905,36 @@ private Pair<Long, Integer> getNextMediaSequenceAndPartIndex(
905905
/* stayInBounds= */ !playlistTracker.isLive() || previous == null);
906906
long mediaSequence = segmentIndexInPlaylist + mediaPlaylist.mediaSequence;
907907
int partIndex = C.INDEX_UNSET;
908+
if (!playlistTracker.isLive()) {
909+
// Early return as we don't need to pick a part for VOD.
910+
return new Pair<>(mediaSequence, partIndex);
911+
}
912+
908913
if (segmentIndexInPlaylist >= 0) {
909914
// In case we are inside the live window, we try to pick a part if available.
910-
Segment segment = mediaPlaylist.segments.get(segmentIndexInPlaylist);
911-
List<HlsMediaPlaylist.Part> parts =
912-
targetPositionInPlaylistUs < segment.relativeStartTimeUs + segment.durationUs
913-
? segment.parts
914-
: mediaPlaylist.trailingParts;
915+
List<HlsMediaPlaylist.Part> parts;
916+
if (!mediaPlaylist.segments.isEmpty()) {
917+
Segment segment = mediaPlaylist.segments.get(segmentIndexInPlaylist);
918+
parts =
919+
targetPositionInPlaylistUs < segment.relativeStartTimeUs + segment.durationUs
920+
? segment.parts
921+
: mediaPlaylist.trailingParts;
922+
} else {
923+
// There are no full segments in the playlist, but we can still pick a trailing part.
924+
parts = mediaPlaylist.trailingParts;
925+
}
915926
for (int i = 0; i < parts.size(); i++) {
916927
HlsMediaPlaylist.Part part = parts.get(i);
917928
if (targetPositionInPlaylistUs < part.relativeStartTimeUs + part.durationUs) {
918929
if (part.isIndependent) {
919930
partIndex = i;
920-
// Increase media sequence by one if the part is a trailing part.
921-
mediaSequence += parts == mediaPlaylist.trailingParts ? 1 : 0;
931+
// Increase media sequence by one if the part is a trailing part and
932+
// mediaPlaylist.segments is not empty. When mediaPlaylist.segments is empty, the
933+
// media sequence has already been increased by the stay-in-bound adjustment.
934+
mediaSequence +=
935+
(parts == mediaPlaylist.trailingParts && !mediaPlaylist.segments.isEmpty())
936+
? 1
937+
: 0;
922938
}
923939
break;
924940
}

libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsChunkSourceTest.java

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ public class HlsChunkSourceTest {
7878
"media/m3u8/live_low_latency_segments_and_parts";
7979
private static final String PLAYLIST_LIVE_LOW_LATENCY_SEGMENTS_AND_SINGLE_PRELOAD_PART =
8080
"media/m3u8/live_low_latency_segments_and_single_preload_part";
81+
private static final String PLAYLIST_VOD_EMPTY = "media/m3u8/media_playlist_vod_empty";
82+
private static final String PLAYLIST_LIVE_EMPTY = "media/m3u8/media_playlist_live_empty";
83+
private static final String PLAYLIST_LIVE_LOW_LATENCY_PARTS_ONLY =
84+
"media/m3u8/live_low_latency_parts_only";
8185
private static final Uri PLAYLIST_URI = Uri.parse("http://example.com/");
8286
private static final Uri PLAYLIST_URI_2 = Uri.parse("http://example2.com/");
8387
private static final long PLAYLIST_START_PERIOD_OFFSET_US = 8_000_000L;
@@ -337,6 +341,94 @@ public void getNextChunk_chunkSourceWithDefaultCmcdConfiguration_setsCorrectBuff
337341
assertThat(output.chunk.dataSpec.httpRequestHeaders).doesNotContainKey("CMCD-Status");
338342
}
339343

344+
@Test
345+
public void getNextChunk_forEmptyVodPlaylist_getsNullChunkAndInferEndOfStream()
346+
throws IOException {
347+
HlsChunkSource testChunkSource = createHlsChunkSource(PLAYLIST_VOD_EMPTY);
348+
HlsChunkSource.HlsChunkHolder output = new HlsChunkSource.HlsChunkHolder();
349+
350+
testChunkSource.getNextChunk(
351+
new LoadingInfo.Builder().setPlaybackPositionUs(9_000_000).setPlaybackSpeed(1.0f).build(),
352+
/* loadPositionUs= */ 9_000_000,
353+
/* largestReadPositionUs= */ 0,
354+
/* queue= */ ImmutableList.of(),
355+
/* allowEndOfStream= */ true,
356+
output);
357+
358+
assertThat(output.chunk).isNull();
359+
assertThat(output.endOfStream).isTrue();
360+
}
361+
362+
@Test
363+
public void getNextChunk_forEmptyLivePlaylist_getsNullChunkAndInferReloadingPlaylist()
364+
throws IOException {
365+
HlsChunkSource testChunkSource = createHlsChunkSource(PLAYLIST_LIVE_EMPTY);
366+
HlsChunkSource.HlsChunkHolder output = new HlsChunkSource.HlsChunkHolder();
367+
368+
testChunkSource.getNextChunk(
369+
new LoadingInfo.Builder().setPlaybackPositionUs(9_000_000).setPlaybackSpeed(1.0f).build(),
370+
/* loadPositionUs= */ 9_000_000,
371+
/* largestReadPositionUs= */ 0,
372+
/* queue= */ ImmutableList.of(),
373+
/* allowEndOfStream= */ true,
374+
output);
375+
376+
assertThat(output.chunk).isNull();
377+
assertThat(output.playlistUrl).isEqualTo(PLAYLIST_URI);
378+
assertThat(output.endOfStream).isFalse();
379+
}
380+
381+
@Test
382+
public void getNextChunk_forLivePlaylistWithSegmentsAndParts_getsCorrectChunk()
383+
throws IOException {
384+
HlsChunkSource testChunkSource =
385+
createHlsChunkSource(PLAYLIST_LIVE_LOW_LATENCY_SEGMENTS_AND_PARTS);
386+
HlsChunkSource.HlsChunkHolder output = new HlsChunkSource.HlsChunkHolder();
387+
388+
testChunkSource.getNextChunk(
389+
new LoadingInfo.Builder().setPlaybackPositionUs(28_000_000).setPlaybackSpeed(1.0f).build(),
390+
/* loadPositionUs= */ 28_000_000,
391+
/* largestReadPositionUs= */ 0,
392+
/* queue= */ ImmutableList.of(),
393+
/* allowEndOfStream= */ true,
394+
output);
395+
396+
// Gets a chunk from an independent segment part.
397+
Uri expectedDataSpecUri = Uri.parse("http://example.com/fileSequence15.0.ts");
398+
assertThat(output.chunk.dataSpec.uri).isEqualTo(expectedDataSpecUri);
399+
400+
testChunkSource.getNextChunk(
401+
new LoadingInfo.Builder().setPlaybackPositionUs(32_000_000).setPlaybackSpeed(1.0f).build(),
402+
/* loadPositionUs= */ 32_000_000,
403+
/* largestReadPositionUs= */ 0,
404+
/* queue= */ ImmutableList.of(),
405+
/* allowEndOfStream= */ true,
406+
output);
407+
408+
// Gets a chunk from an independent trailing part.
409+
expectedDataSpecUri = Uri.parse("http://example.com/fileSequence16.0.ts");
410+
assertThat(output.chunk.dataSpec.uri).isEqualTo(expectedDataSpecUri);
411+
}
412+
413+
@Test
414+
public void getNextChunk_forLivePlaylistWithPartsOnly_getsCorrectChunkFromParts()
415+
throws IOException {
416+
HlsChunkSource testChunkSource = createHlsChunkSource(PLAYLIST_LIVE_LOW_LATENCY_PARTS_ONLY);
417+
HlsChunkSource.HlsChunkHolder output = new HlsChunkSource.HlsChunkHolder();
418+
419+
// A request to fetch the chunk at 8 seconds should retrieve the first part.
420+
testChunkSource.getNextChunk(
421+
new LoadingInfo.Builder().setPlaybackPositionUs(8_000_000).setPlaybackSpeed(1.0f).build(),
422+
/* loadPositionUs= */ 8_000_000,
423+
/* largestReadPositionUs= */ 0,
424+
/* queue= */ ImmutableList.of(),
425+
/* allowEndOfStream= */ true,
426+
output);
427+
428+
Uri expectedDataSpecUri = Uri.parse("http://example.com/fileSequence16.0.ts");
429+
assertThat(output.chunk.dataSpec.uri).isEqualTo(expectedDataSpecUri);
430+
}
431+
340432
@Test
341433
public void getNextChunk_forLivePlaylistWithSegmentsOnly_setsCorrectNextObjectRequest()
342434
throws IOException {
@@ -949,6 +1041,7 @@ private static HlsChunkSource createHlsChunkSource(
9491041
when(mockPlaylistTracker.getPlaylistSnapshot(eq(playlistUri), anyBoolean()))
9501042
.thenReturn(playlist);
9511043
when(mockPlaylistTracker.isSnapshotValid(eq(playlistUri))).thenReturn(true);
1044+
when(mockPlaylistTracker.isLive()).thenAnswer(invocation -> !playlist.hasEndTag);
9521045
if (playlistLoadException != null) {
9531046
doThrow(playlistLoadException)
9541047
.when(mockPlaylistTracker)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#EXTM3U
2+
#EXT-X-TARGETDURATION:4
3+
#EXT-X-INDEPENDENT-SEGMENTS
4+
#EXT-X-PART-INF:PART-TARGET=1.000400
5+
#EXT-X-VERSION:3
6+
#EXT-X-MEDIA-SEQUENCE:10
7+
#EXT-X-PART:DURATION=1.00000,URI="fileSequence16.0.ts"
8+
#EXT-X-PART:DURATION=1.00000,URI="fileSequence16.1.ts"
9+
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="fileSequence16.2.ts"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#EXTM3U
2+
#EXT-X-MEDIA-SEQUENCE:2
3+
#EXT-X-MAP:URI="init.mp4"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#EXTM3U
2+
#EXT-X-MEDIA-SEQUENCE:2
3+
#EXT-X-MAP:URI="init.mp4"
4+
#EXT-X-ENDLIST

0 commit comments

Comments
 (0)