diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 5d5d398370a..80a4aad902e 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -270,29 +270,34 @@ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParame return positionUs; } + long startOfPlaylistInPeriodUs = + mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs(); + long targetPositionInPlaylistUs = positionUs - startOfPlaylistInPeriodUs; + + if (targetPositionInPlaylistUs > mediaPlaylist.durationUs) { + 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 + // is non-empty, and the playlist is fresh enough that the period position falls in side of + // the playlist bound, so we can use segment start times as sync points. + // + // If (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; - int segmentIndex = - Util.binarySearchFloor( - mediaPlaylist.segments, - relativePositionUs, - /* inclusive= */ true, - /* stayInBounds= */ true); - long firstSyncUs = mediaPlaylist.segments.get(segmentIndex).relativeStartTimeUs; + int segIndex = Util.binarySearchFloor(mediaPlaylist.segments, targetPositionInPlaylistUs, + true, true); + long firstSyncUs = + mediaPlaylist.segments.get(segIndex).relativeStartTimeUs + startOfPlaylistInPeriodUs; long secondSyncUs = firstSyncUs; - if (segmentIndex != mediaPlaylist.segments.size() - 1) { - secondSyncUs = mediaPlaylist.segments.get(segmentIndex + 1).relativeStartTimeUs; + if (segIndex != mediaPlaylist.segments.size() - 1) { + secondSyncUs = mediaPlaylist.segments.get(segIndex + 1).relativeStartTimeUs + + startOfPlaylistInPeriodUs; } - return seekParameters.resolveSeekPositionUs(relativePositionUs, firstSyncUs, secondSyncUs) - + startOfPlaylistInPeriodUs; + + return seekParameters.resolveSeekPositionUs(positionUs, firstSyncUs, secondSyncUs); } /** diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsChunkSourceTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsChunkSourceTest.java index ac718f747b9..099e441bd4c 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsChunkSourceTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsChunkSourceTest.java @@ -47,8 +47,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_FIRST = + "media/m3u8/media_playlist_live_first"; + private static final String PLAYLIST_LIVE_SECOND = + "media/m3u8/media_playlist_live_second"; 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; @@ -71,7 +73,7 @@ public void setup() throws IOException { InputStream inputStream = TestUtil.getInputStream( - ApplicationProvider.getApplicationContext(), PLAYLIST_INDEPENDENT_SEGMENTS); + ApplicationProvider.getApplicationContext(), PLAYLIST_LIVE_FIRST); HlsMediaPlaylist playlist = (HlsMediaPlaylist) new HlsPlaylistParser().parse(PLAYLIST_URI, inputStream); when(mockPlaylistTracker.getPlaylistSnapshot(eq(PLAYLIST_URI), anyBoolean())) @@ -161,7 +163,22 @@ public void getAdjustedSeekPositionUs_noIndependentSegments() throws IOException long adjustedPositionUs = testChunkSource.getAdjustedSeekPositionUs( - playlistTimeToPeriodTimeUs(100_000_000), SeekParameters.EXACT); + playlistTimeToPeriodTimeUs(100_000_000), SeekParameters.NEXT_SYNC); + + assertThat(periodTimeToPlaylistTimeUs(adjustedPositionUs)).isEqualTo(100_000_000); + } + + @Test + public void getAdjustedSeekPositionUs_stalePlaylist() throws IOException { + InputStream inputStream = + TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), PLAYLIST_LIVE_SECOND); + HlsMediaPlaylist playlist = + (HlsMediaPlaylist) new HlsPlaylistParser().parse(PLAYLIST_URI, inputStream); + when(mockPlaylistTracker.getPlaylistSnapshot(eq(PLAYLIST_URI), anyBoolean())) + .thenReturn(playlist); + long adjustedPositionUs = + testChunkSource.getAdjustedSeekPositionUs( + playlistTimeToPeriodTimeUs(100_000_000), SeekParameters.NEXT_SYNC); assertThat(periodTimeToPlaylistTimeUs(adjustedPositionUs)).isEqualTo(100_000_000); } @@ -177,7 +194,7 @@ public void getAdjustedSeekPositionUs_emptyPlaylist() throws IOException { long adjustedPositionUs = testChunkSource.getAdjustedSeekPositionUs( - playlistTimeToPeriodTimeUs(100_000_000), SeekParameters.EXACT); + playlistTimeToPeriodTimeUs(100_000_000), SeekParameters.NEXT_SYNC); assertThat(periodTimeToPlaylistTimeUs(adjustedPositionUs)).isEqualTo(100_000_000); } diff --git a/testdata/src/test/assets/media/m3u8/media_playlist_live_first b/testdata/src/test/assets/media/m3u8/media_playlist_live_first new file mode 100644 index 00000000000..9fb6840b917 --- /dev/null +++ b/testdata/src/test/assets/media/m3u8/media_playlist_live_first @@ -0,0 +1,17 @@ +#EXTM3U +#EXT-X-MEDIA-SEQUENCE:2 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:00:00.106Z +#EXT-X-MAP:URI="init.mp4" +#EXTINF:4, +2.mp4 +#EXTINF:4, +3.mp4 +#EXTINF:4, +4.mp4 +#EXTINF:4, +5.mp4 +#EXTINF:4, +6.mp4 +#EXTINF:4, +7.mp4 diff --git a/testdata/src/test/assets/media/m3u8/media_playlist_live_second b/testdata/src/test/assets/media/m3u8/media_playlist_live_second new file mode 100644 index 00000000000..ce931829581 --- /dev/null +++ b/testdata/src/test/assets/media/m3u8/media_playlist_live_second @@ -0,0 +1,15 @@ +#EXTM3U +#EXT-X-MEDIA-SEQUENCE:4 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:00:08.106Z +#EXT-X-MAP:URI="init.mp4" +#EXTINF:4, +4.mp4 +#EXTINF:4, +5.mp4 +#EXTINF:4, +6.mp4 +#EXTINF:4, +7.mp4 +#EXTINF:4, +8.mp4 \ No newline at end of file