From c9b6590dac9ff2c004f5aac1bd80f0b1a1d71e36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Varga?= Date: Tue, 7 Oct 2025 14:09:15 +0200 Subject: [PATCH 1/3] strict matching of segment boundary --- .../hls/HlsInterstitialsAdsLoader.java | 37 +++++++++++++++---- .../hls/HlsInterstitialsAdsLoaderTest.java | 37 +++++++++++++++++++ 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoader.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoader.java index 0a592176735..f6fcc96ccd2 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoader.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoader.java @@ -38,13 +38,13 @@ import static com.google.common.base.Preconditions.checkState; import static java.lang.Math.abs; import static java.lang.Math.max; -import static java.lang.Math.min; import android.content.Context; import android.net.Uri; import android.os.Bundle; import android.os.Looper; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.media3.common.AdPlaybackState; import androidx.media3.common.AdPlaybackState.AdGroup; import androidx.media3.common.AdPlaybackState.SkipInfo; @@ -1514,18 +1514,41 @@ private static long resolveInterstitialStartTimeUs( } } - private static long getClosestSegmentBoundaryUs(long unixTimeUs, HlsMediaPlaylist mediaPlaylist) { + @VisibleForTesting + /* package */ static long getClosestSegmentBoundaryUs( + long unixTimeUs, HlsMediaPlaylist mediaPlaylist) { long positionInPlaylistUs = unixTimeUs - mediaPlaylist.startTimeUs; if (positionInPlaylistUs <= 0 || mediaPlaylist.segments.isEmpty()) { return mediaPlaylist.startTimeUs; } else if (positionInPlaylistUs >= mediaPlaylist.durationUs) { return mediaPlaylist.startTimeUs + mediaPlaylist.durationUs; } - long segmentIndex = - min( - positionInPlaylistUs / mediaPlaylist.targetDurationUs, - mediaPlaylist.segments.size() - 1); - HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get((int) segmentIndex); + + // Binary search to find the segment containing or closest to the position + int left = 0; + int right = mediaPlaylist.segments.size() - 1; + int closestIndex = 0; + + while (left <= right) { + int mid = left + (right - left) / 2; + HlsMediaPlaylist.Segment midSegment = mediaPlaylist.segments.get(mid); + long segmentStart = midSegment.relativeStartTimeUs; + long segmentEnd = segmentStart + midSegment.durationUs; + + if (positionInPlaylistUs >= segmentStart && positionInPlaylistUs <= segmentEnd) { + // Position is within this segment + closestIndex = mid; + break; + } else if (positionInPlaylistUs < segmentStart) { + closestIndex = mid; + right = mid - 1; + } else { + closestIndex = mid; + left = mid + 1; + } + } + + HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(closestIndex); return positionInPlaylistUs - segment.relativeStartTimeUs < abs(positionInPlaylistUs - (segment.relativeStartTimeUs + segment.durationUs)) ? mediaPlaylist.startTimeUs + segment.relativeStartTimeUs diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoaderTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoaderTest.java index c5518931321..92bd8f4dcc5 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoaderTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoaderTest.java @@ -17,6 +17,8 @@ import static androidx.media3.common.Player.DISCONTINUITY_REASON_AUTO_TRANSITION; import static androidx.media3.common.Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED; +import static androidx.media3.common.util.Util.msToUs; +import static androidx.media3.exoplayer.hls.HlsInterstitialsAdsLoader.getClosestSegmentBoundaryUs; import static androidx.media3.test.utils.robolectric.RobolectricUtil.runMainLooperUntil; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.truth.Truth.assertThat; @@ -5741,6 +5743,41 @@ public void state_constructorWithAdsIdsThatDoNotMatch_throwsIllegalArgumentExcep IllegalArgumentException.class, () -> new AdsResumptionState("5678", adPlaybackState)); } + @Test + public void getClosestSegmentBoundaryUs_vastlyVariedSegmentSize() throws IOException { + String interstitialStartDate = "2025-10-07T01:00:14.000Z"; + long interstitialStartDateUs = msToUs(Util.parseXsDateTime(interstitialStartDate)); + String playlistString = + "#EXTM3U\n" + + "#EXT-X-TARGETDURATION:10\n" + + "#EXT-X-PROGRAM-DATE-TIME:2025-10-07T01:00:00.000Z\n" + + "#EXTINF:2,\n" + + "1.aac\n" + + "#EXTINF:2,\n" + + "2.aac\n" + + "#EXTINF:10,\n" + + "3.aac\n" + + "#EXT-X-DATERANGE:ID=\"1\"," + + "CLASS=\"com.apple.hls.interstitial\"," + + "START-DATE=\"" + + interstitialStartDate + + "\"," + + "X-ASSET-LIST=\"http://example.com/assetlist-0-0.json\"," + + "X-SNAP=\"OUT,IN\"\n" + + "#EXTINF:10,\n" + + "4.aac"; + + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); + HlsMediaPlaylist contentMediaPlaylist = + (HlsMediaPlaylist) new HlsPlaylistParser().parse(Uri.EMPTY, inputStream); + + long closestSegmentBoundaryUs = + getClosestSegmentBoundaryUs(interstitialStartDateUs, contentMediaPlaylist); + + assertThat(closestSegmentBoundaryUs) + .isEqualTo(contentMediaPlaylist.startTimeUs + 2_000_000 + 2_000_000 + 10_000_000); + } + private List callHandleContentTimelineChangedForLiveAndCaptureAdPlaybackStates( HlsInterstitialsAdsLoader adsLoader, boolean startAdsLoader, From 4613ca5632a1eb64e9d0c7f9112f742f4d7a1912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20B=C3=A4chinger?= Date: Tue, 14 Oct 2025 19:59:09 +0000 Subject: [PATCH 2/3] Add release notes --- RELEASENOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 73cb34643fc..a0504743191 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -303,6 +303,8 @@ * Fix bug where the start time of the playlist was dropped when the EXT-X-PROGRAM-START-DATE tag defining the start time was removed from a playlist ([#2760](https://github.com/androidx/media/issues/2760)). + * Use binary search to find the segment index of a given position in the + playlist ([#2826](https://github.com/androidx/media/pull/2826). * DASH extension: * Fix `UnsupportedOperationException` when playing DASH streams with a non-hierarchical `data:` URI manifest From 954e966bf87e9886fd413c7d8285f6ce5589bdd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20B=C3=A4chinger?= Date: Tue, 14 Oct 2025 20:47:42 +0000 Subject: [PATCH 3/3] Add unit test --- .../hls/HlsInterstitialsAdsLoaderTest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoaderTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoaderTest.java index 92bd8f4dcc5..e604a3cb932 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoaderTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/HlsInterstitialsAdsLoaderTest.java @@ -5778,6 +5778,45 @@ public void getClosestSegmentBoundaryUs_vastlyVariedSegmentSize() throws IOExcep .isEqualTo(contentMediaPlaylist.startTimeUs + 2_000_000 + 2_000_000 + 10_000_000); } + @Test + public void getClosestSegmentBoundaryUs_positionBeforeAndAfterPlaylist_getsSanitizedValues() + throws IOException { + String timestampBeforePlaylist = "2025-10-07T00:00:00.000Z"; + long timeBeforePlaylistUs = msToUs(Util.parseXsDateTime(timestampBeforePlaylist)); + String timestampLastNonPostRollSnap = "2025-10-07T01:00:18.999Z"; + long timestampLastNonPostRollSnapUs = msToUs( + Util.parseXsDateTime(timestampLastNonPostRollSnap)); + String timestampPostRollSnap = "2025-10-07T01:00:19.000Z"; + long timestampPostRollSnapUs = msToUs(Util.parseXsDateTime(timestampPostRollSnap)); + String timestampAfterPlaylist = "2025-10-07T23:59:59.000Z"; + long timestampAfterPlaylistUs = msToUs(Util.parseXsDateTime(timestampAfterPlaylist)); + String playlistString = + "#EXTM3U\n" + + "#EXT-X-TARGETDURATION:10\n" + + "#EXT-X-PROGRAM-DATE-TIME:2025-10-07T01:00:00.000Z\n" + + "#EXTINF:2,\n" + + "1.aac\n" + + "#EXTINF:2,\n" + + "2.aac\n" + + "#EXTINF:10,\n" + + "3.aac\n" + + "#EXTINF:10,\n" + + "4.aac"; + + InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString)); + HlsMediaPlaylist contentMediaPlaylist = + (HlsMediaPlaylist) new HlsPlaylistParser().parse(Uri.EMPTY, inputStream); + + assertThat(getClosestSegmentBoundaryUs(timeBeforePlaylistUs, contentMediaPlaylist)) + .isEqualTo(contentMediaPlaylist.startTimeUs); + assertThat(getClosestSegmentBoundaryUs(timestampLastNonPostRollSnapUs, contentMediaPlaylist)) + .isEqualTo(contentMediaPlaylist.startTimeUs + 14_000_000L); + assertThat(getClosestSegmentBoundaryUs(timestampPostRollSnapUs, contentMediaPlaylist)) + .isEqualTo(contentMediaPlaylist.startTimeUs + 24_000_000L); + assertThat(getClosestSegmentBoundaryUs(timestampAfterPlaylistUs, contentMediaPlaylist)) + .isEqualTo(contentMediaPlaylist.startTimeUs + 24_000_000L); + } + private List callHandleContentTimelineChangedForLiveAndCaptureAdPlaybackStates( HlsInterstitialsAdsLoader adsLoader, boolean startAdsLoader,