Loosen the condition for seeking to sync positions in a HLS stream

Previously we only enable `SeekParameter.*_SYNC` for HLS when `EXT-X-INDEPENDENT-SEGMENTS` is set in the playlist. However, this condition can actually be loosened. To seek in HLS, we need to download the segment in which the resolved seek position locates under any circumstance. If `SeekParameter.PREVIOUS_SYNC` or `SeekParameter.CLOSEST_SYNC` is passed, and that segment happens to start with sync samples, then the seek can be done quicker with that adjusted seek position. And if that segment doesn't start with sync samples, then the behaviour will be the same as we set the adjusted seek position to the exact original position. But we still cannot safely enable `SeekParameter.NEXT_SYNC` as it will potentially cause the seeking to miss more content than seeking to the exact position.

Issue: androidx/media#2209
PiperOrigin-RevId: 737580861
(cherry picked from commit 42b71c29e8bca0369381d100d5cec912e1c1e7ef)
This commit is contained in:
tianyifeng 2025-03-17 05:41:55 -07:00 committed by tonihei
parent 5957daadee
commit f357f0a966
3 changed files with 78 additions and 12 deletions

View File

@ -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

View File

@ -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)

View File

@ -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 =