From 3e56d2a6fb31078f9ac0899ebe5a7b0a342a7336 Mon Sep 17 00:00:00 2001 From: tianyifeng Date: Tue, 4 Feb 2025 04:27:27 -0800 Subject: [PATCH] Add ProgressiveMediaSource.Listener interface and onSeekMap event This will allow the listeners who are interested in the `SeekMap` to get informed once the period has done the preparation. PiperOrigin-RevId: 723027718 --- .../source/ProgressiveMediaPeriod.java | 15 +++---- .../source/ProgressiveMediaSource.java | 45 ++++++++++++++++++- .../source/ProgressiveMediaSourceTest.java | 27 ++++++++++- .../test/utils/MediaSourceTestRunner.java | 2 +- 4 files changed, 78 insertions(+), 11 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java index 2d014eb761..cdffd107b5 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java @@ -87,14 +87,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; interface Listener { /** - * Called when the duration, the ability to seek within the period, or the categorization as - * live stream changes. + * Called when the duration, the {@link SeekMap} of the period, or the categorization as live + * stream changes. * * @param durationUs The duration of the period, or {@link C#TIME_UNSET}. - * @param isSeekable Whether the period is seekable. + * @param seekMap The {@link SeekMap}. * @param isLive Whether the period is live. */ - void onSourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive); + void onSourceInfoRefreshed(long durationUs, SeekMap seekMap, boolean isLive); } private static final String TAG = "ProgressiveMediaPeriod"; @@ -637,14 +637,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; public void onLoadCompleted( ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { if (durationUs == C.TIME_UNSET && seekMap != null) { - boolean isSeekable = seekMap.isSeekable(); long largestQueuedTimestampUs = getLargestQueuedTimestampUs(/* includeDisabledTracks= */ true); durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0 : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US; - listener.onSourceInfoRefreshed(durationUs, isSeekable, isLive); + listener.onSourceInfoRefreshed(durationUs, seekMap, isLive); } StatsDataSource dataSource = loadable.dataSource; LoadEventInfo loadEventInfo = @@ -829,7 +828,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; isLive = !isLengthKnown && seekMap.getDurationUs() == C.TIME_UNSET; dataType = isLive ? C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE : C.DATA_TYPE_MEDIA; if (prepared) { - listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable(), isLive); + listener.onSourceInfoRefreshed(durationUs, seekMap, isLive); } else { maybeFinishPrepare(); } @@ -892,7 +891,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } }; } - listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable(), isLive); + listener.onSourceInfoRefreshed(durationUs, seekMap, isLive); prepared = true; checkNotNull(callback).onPrepared(this); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaSource.java index 4ed305e187..1136f24eac 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaSource.java @@ -42,6 +42,7 @@ import androidx.media3.extractor.DefaultExtractorsFactory; import androidx.media3.extractor.Extractor; import androidx.media3.extractor.ExtractorOutput; import androidx.media3.extractor.ExtractorsFactory; +import androidx.media3.extractor.SeekMap; import androidx.media3.extractor.TrackOutput; import com.google.common.base.Supplier; import com.google.errorprone.annotations.CanIgnoreReturnValue; @@ -62,6 +63,24 @@ import java.util.concurrent.Executor; public final class ProgressiveMediaSource extends BaseMediaSource implements ProgressiveMediaPeriod.Listener { + /** + * A listener of {@linkplain ProgressiveMediaSource progressive media sources}, which will be + * notified of source events. + */ + public interface Listener { + + /** + * Called when the {@link SeekMap} of the source has been extracted from the stream. + * + *

Called on the playback thread. + * + * @param source The {@link MediaSource} whose {@link SeekMap} has been extracted from the + * stream. + * @param seekMap The source's {@link SeekMap}. + */ + void onSeekMap(MediaSource source, SeekMap seekMap); + } + /** Factory for {@link ProgressiveMediaSource}s. */ @SuppressWarnings("deprecation") // Implement deprecated type for backwards compatibility. public static final class Factory implements MediaSourceFactory { @@ -307,6 +326,8 @@ public final class ProgressiveMediaSource extends BaseMediaSource @GuardedBy("this") private MediaItem mediaItem; + @Nullable private Listener listener; + private ProgressiveMediaSource( MediaItem mediaItem, DataSource.Factory dataSourceFactory, @@ -399,12 +420,31 @@ public final class ProgressiveMediaSource extends BaseMediaSource drmSessionManager.release(); } + /** + * Sets the {@link Listener}. + * + *

This method must be called on the playback thread. + */ + public void setListener(Listener listener) { + this.listener = listener; + } + + /** + * Clears the {@link Listener}. + * + *

This method must be called on the playback thread. + */ + public void clearListener() { + this.listener = null; + } + // ProgressiveMediaPeriod.Listener implementation. @Override - public void onSourceInfoRefreshed(long durationUs, boolean isSeekable, boolean isLive) { + public void onSourceInfoRefreshed(long durationUs, SeekMap seekMap, boolean isLive) { // If we already have the duration from a previous source info refresh, use it. durationUs = durationUs == C.TIME_UNSET ? timelineDurationUs : durationUs; + boolean isSeekable = seekMap.isSeekable(); if (!timelineIsPlaceholder && timelineDurationUs == durationUs && timelineIsSeekable == isSeekable @@ -417,6 +457,9 @@ public final class ProgressiveMediaSource extends BaseMediaSource timelineIsLive = isLive; timelineIsPlaceholder = false; notifySourceInfoRefreshed(); + if (listener != null) { + listener.onSeekMap(this, seekMap); + } } // Internal methods. diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ProgressiveMediaSourceTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ProgressiveMediaSourceTest.java index f882d53b0d..d2ab4ff324 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ProgressiveMediaSourceTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/ProgressiveMediaSourceTest.java @@ -39,6 +39,7 @@ import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.FixedTrackSelection; import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy; +import androidx.media3.extractor.SeekMap; import androidx.media3.test.utils.MediaSourceTestRunner; import androidx.media3.test.utils.TestUtil; import androidx.media3.test.utils.robolectric.RobolectricUtil; @@ -151,7 +152,8 @@ public class ProgressiveMediaSourceTest { new ProgressiveMediaSource.Factory(dataSourceFactory) .enableLazyLoadingWithSingleTrack(/* trackId= */ 42, format) .createMediaSource(MediaItem.fromUri(mediaUri)); - MediaSourceTestRunner mediaSourceTestRunner = new MediaSourceTestRunner(mediaSource); + ProgressiveMediaSourceTestRunner mediaSourceTestRunner = + new ProgressiveMediaSourceTestRunner(mediaSource); ConditionVariable loadCompleted = new ConditionVariable(); mediaSourceTestRunner.runOnPlaybackThread( () -> @@ -168,6 +170,9 @@ public class ProgressiveMediaSourceTest { } })); + AtomicReference seekMapReference = new AtomicReference<>(); + ProgressiveMediaSource.Listener listener = (source, seekMap) -> seekMapReference.set(seekMap); + mediaSourceTestRunner.setListener(listener); Timeline timeline = mediaSourceTestRunner.prepareSource(); MediaPeriod mediaPeriod = mediaSourceTestRunner.createPeriod( @@ -178,6 +183,7 @@ public class ProgressiveMediaSourceTest { assertThat(preparedLatch.await(DEFAULT_TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(openedUris).isEmpty(); + assertThat(seekMapReference.get()).isNotNull(); ListenableFuture isLoading = mediaSourceTestRunner.asyncRunOnPlaybackThread( @@ -205,6 +211,7 @@ public class ProgressiveMediaSourceTest { assertThat(openedUris).containsExactly(mediaUri); mediaSourceTestRunner.releasePeriod(mediaPeriod); + mediaSourceTestRunner.clearListener(); mediaSourceTestRunner.releaseSource(); mediaSourceTestRunner.release(); } @@ -279,4 +286,22 @@ public class ProgressiveMediaSourceTest { /* streamResetFlags= */ new boolean[] {false}, /* positionUs= */ 0); } + + private static final class ProgressiveMediaSourceTestRunner extends MediaSourceTestRunner { + + private final ProgressiveMediaSource mediaSource; + + public ProgressiveMediaSourceTestRunner(ProgressiveMediaSource mediaSource) { + super(mediaSource); + this.mediaSource = mediaSource; + } + + public void setListener(ProgressiveMediaSource.Listener listener) { + runOnPlaybackThread(() -> mediaSource.setListener(listener)); + } + + public void clearListener() { + runOnPlaybackThread(mediaSource::clearListener); + } + } } diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/MediaSourceTestRunner.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/MediaSourceTestRunner.java index ee9fb97b6f..b4298b9357 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/MediaSourceTestRunner.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/MediaSourceTestRunner.java @@ -360,7 +360,7 @@ public class MediaSourceTestRunner { playbackThread.quit(); } - private class MediaSourceListener implements MediaSourceCaller, MediaSourceEventListener { + public class MediaSourceListener implements MediaSourceCaller, MediaSourceEventListener { // MediaSourceCaller methods.