diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a1794e8075..53703a0019 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -19,6 +19,9 @@ * Fix issue where adaptation sets marked with `adaptation-set-switching` but different languages or role flags are merged together ([#2222](https://github.com/androidx/media/issues/2222)). +* HLS extension: + * Loosen the condition for seeking to sync positions in a HLS stream + ([#2209](https://github.com/androidx/media/issues/2209)). ## 1.6 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 931ae99362..8ba3827899 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 @@ -296,19 +296,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /* isForPlayback= */ true) : null; - if (mediaPlaylist == null - || mediaPlaylist.segments.isEmpty() - || !mediaPlaylist.hasIndependentSegments) { + if (mediaPlaylist == null || mediaPlaylist.segments.isEmpty()) { return positionUs; } - // Segments start with sync samples (i.e., EXT-X-INDEPENDENT-SEGMENTS is set) and the playlist - // is non-empty, so we can use segment start times as sync points. Note that in the rare case - // that (a) an adaptive quality switch occurs between the adjustment and the seek being - // performed, and (b) segment start times are not aligned across variants, it's possible that - // the adjusted position may not be at a sync point when it was intended to be. However, this is - // very much an edge case, and getting it wrong is worth it for getting the vast majority of - // cases right whilst keeping the implementation relatively simple. + // The playlist is non-empty, so we can use segment start times as sync points. We can always + // safely assume that the segment contains the positionUs starts with sync samples (even if it + // actually doesn't) and set the below firstSyncUs as the start time of that segment, as it + // doesn't harm the seeking performance if it is resolved to be the seek position. However, we + // should set the secondSyncUs as the start time of the segment after the positionUs only when + // we're sure that the segments start with sync samples (i.e., EXT-X-INDEPENDENT-SEGMENTS is + // set). + // + // Note that in the rare case that (a) an adaptive quality switch occurs between the adjustment + // and the seek being performed, and (b) segment start times are not aligned across variants, + // it's possible that the adjusted position may not be at a sync point when it was intended to + // be. However, this is very much an edge case, and getting it wrong is worth it for getting + // the vast majority of cases right whilst keeping the implementation relatively simple. long startOfPlaylistInPeriodUs = mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); long relativePositionUs = positionUs - startOfPlaylistInPeriodUs; @@ -320,7 +324,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /* stayInBounds= */ true); long firstSyncUs = mediaPlaylist.segments.get(segmentIndex).relativeStartTimeUs; long secondSyncUs = firstSyncUs; - if (segmentIndex != mediaPlaylist.segments.size() - 1) { + if (mediaPlaylist.hasIndependentSegments && segmentIndex != mediaPlaylist.segments.size() - 1) { secondSyncUs = mediaPlaylist.segments.get(segmentIndex + 1).relativeStartTimeUs; } return seekParameters.resolveSeekPositionUs(relativePositionUs, firstSyncUs, secondSyncUs) 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 4094acf429..f413ec04ba 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 @@ -163,7 +163,66 @@ public class HlsChunkSourceTest { } @Test - public void getAdjustedSeekPositionUs_noIndependentSegments() throws IOException { + public void getAdjustedSeekPositionUsNoIndependentSegments_tryPreviousSync() throws IOException { + HlsChunkSource testChunkSource = createHlsChunkSource(/* cmcdConfiguration= */ null); + + InputStream inputStream = + TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), PLAYLIST); + HlsMediaPlaylist playlist = + (HlsMediaPlaylist) new HlsPlaylistParser().parse(PLAYLIST_URI, inputStream); + when(mockPlaylistTracker.getPlaylistSnapshot(eq(PLAYLIST_URI), anyBoolean())) + .thenReturn(playlist); + + long adjustedPositionUs = + testChunkSource.getAdjustedSeekPositionUs( + playlistTimeToPeriodTimeUs(17_000_000), SeekParameters.PREVIOUS_SYNC); + + assertThat(periodTimeToPlaylistTimeUs(adjustedPositionUs)).isEqualTo(16_000_000); + } + + @Test + public void getAdjustedSeekPositionUsNoIndependentSegments_notTryNextSync() throws IOException { + HlsChunkSource testChunkSource = createHlsChunkSource(/* cmcdConfiguration= */ null); + + InputStream inputStream = + TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), PLAYLIST); + HlsMediaPlaylist playlist = + (HlsMediaPlaylist) new HlsPlaylistParser().parse(PLAYLIST_URI, inputStream); + when(mockPlaylistTracker.getPlaylistSnapshot(eq(PLAYLIST_URI), anyBoolean())) + .thenReturn(playlist); + + long adjustedPositionUs = + testChunkSource.getAdjustedSeekPositionUs( + playlistTimeToPeriodTimeUs(17_000_000), SeekParameters.NEXT_SYNC); + + assertThat(periodTimeToPlaylistTimeUs(adjustedPositionUs)).isEqualTo(17_000_000); + } + + @Test + public void getAdjustedSeekPositionUsNoIndependentSegments_alwaysTryClosestSyncBefore() + throws IOException { + HlsChunkSource testChunkSource = createHlsChunkSource(/* cmcdConfiguration= */ null); + + InputStream inputStream = + TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), PLAYLIST); + HlsMediaPlaylist playlist = + (HlsMediaPlaylist) new HlsPlaylistParser().parse(PLAYLIST_URI, inputStream); + when(mockPlaylistTracker.getPlaylistSnapshot(eq(PLAYLIST_URI), anyBoolean())) + .thenReturn(playlist); + + long adjustedPositionUs1 = + testChunkSource.getAdjustedSeekPositionUs( + playlistTimeToPeriodTimeUs(17_000_000), SeekParameters.CLOSEST_SYNC); + long adjustedPositionUs2 = + testChunkSource.getAdjustedSeekPositionUs( + playlistTimeToPeriodTimeUs(19_000_000), SeekParameters.CLOSEST_SYNC); + + assertThat(periodTimeToPlaylistTimeUs(adjustedPositionUs1)).isEqualTo(16_000_000); + assertThat(periodTimeToPlaylistTimeUs(adjustedPositionUs2)).isEqualTo(16_000_000); + } + + @Test + public void getAdjustedSeekPositionUsNoIndependentSegments_exact() throws IOException { HlsChunkSource testChunkSource = createHlsChunkSource(/* cmcdConfiguration= */ null); InputStream inputStream =