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
This commit is contained in:
tianyifeng 2025-02-04 04:27:27 -08:00 committed by Copybara-Service
parent decfb9b0a9
commit 3e56d2a6fb
4 changed files with 78 additions and 11 deletions

View File

@ -87,14 +87,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
interface Listener { interface Listener {
/** /**
* Called when the duration, the ability to seek within the period, or the categorization as * Called when the duration, the {@link SeekMap} of the period, or the categorization as live
* live stream changes. * stream changes.
* *
* @param durationUs The duration of the period, or {@link C#TIME_UNSET}. * @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. * @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"; private static final String TAG = "ProgressiveMediaPeriod";
@ -637,14 +637,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public void onLoadCompleted( public void onLoadCompleted(
ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) {
if (durationUs == C.TIME_UNSET && seekMap != null) { if (durationUs == C.TIME_UNSET && seekMap != null) {
boolean isSeekable = seekMap.isSeekable();
long largestQueuedTimestampUs = long largestQueuedTimestampUs =
getLargestQueuedTimestampUs(/* includeDisabledTracks= */ true); getLargestQueuedTimestampUs(/* includeDisabledTracks= */ true);
durationUs = durationUs =
largestQueuedTimestampUs == Long.MIN_VALUE largestQueuedTimestampUs == Long.MIN_VALUE
? 0 ? 0
: largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US; : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US;
listener.onSourceInfoRefreshed(durationUs, isSeekable, isLive); listener.onSourceInfoRefreshed(durationUs, seekMap, isLive);
} }
StatsDataSource dataSource = loadable.dataSource; StatsDataSource dataSource = loadable.dataSource;
LoadEventInfo loadEventInfo = LoadEventInfo loadEventInfo =
@ -829,7 +828,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
isLive = !isLengthKnown && seekMap.getDurationUs() == C.TIME_UNSET; isLive = !isLengthKnown && seekMap.getDurationUs() == C.TIME_UNSET;
dataType = isLive ? C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE : C.DATA_TYPE_MEDIA; dataType = isLive ? C.DATA_TYPE_MEDIA_PROGRESSIVE_LIVE : C.DATA_TYPE_MEDIA;
if (prepared) { if (prepared) {
listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable(), isLive); listener.onSourceInfoRefreshed(durationUs, seekMap, isLive);
} else { } else {
maybeFinishPrepare(); 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; prepared = true;
checkNotNull(callback).onPrepared(this); checkNotNull(callback).onPrepared(this);
} }

View File

@ -42,6 +42,7 @@ import androidx.media3.extractor.DefaultExtractorsFactory;
import androidx.media3.extractor.Extractor; import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.ExtractorOutput; import androidx.media3.extractor.ExtractorOutput;
import androidx.media3.extractor.ExtractorsFactory; import androidx.media3.extractor.ExtractorsFactory;
import androidx.media3.extractor.SeekMap;
import androidx.media3.extractor.TrackOutput; import androidx.media3.extractor.TrackOutput;
import com.google.common.base.Supplier; import com.google.common.base.Supplier;
import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CanIgnoreReturnValue;
@ -62,6 +63,24 @@ import java.util.concurrent.Executor;
public final class ProgressiveMediaSource extends BaseMediaSource public final class ProgressiveMediaSource extends BaseMediaSource
implements ProgressiveMediaPeriod.Listener { 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.
*
* <p>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. */ /** Factory for {@link ProgressiveMediaSource}s. */
@SuppressWarnings("deprecation") // Implement deprecated type for backwards compatibility. @SuppressWarnings("deprecation") // Implement deprecated type for backwards compatibility.
public static final class Factory implements MediaSourceFactory { public static final class Factory implements MediaSourceFactory {
@ -307,6 +326,8 @@ public final class ProgressiveMediaSource extends BaseMediaSource
@GuardedBy("this") @GuardedBy("this")
private MediaItem mediaItem; private MediaItem mediaItem;
@Nullable private Listener listener;
private ProgressiveMediaSource( private ProgressiveMediaSource(
MediaItem mediaItem, MediaItem mediaItem,
DataSource.Factory dataSourceFactory, DataSource.Factory dataSourceFactory,
@ -399,12 +420,31 @@ public final class ProgressiveMediaSource extends BaseMediaSource
drmSessionManager.release(); drmSessionManager.release();
} }
/**
* Sets the {@link Listener}.
*
* <p>This method must be called on the playback thread.
*/
public void setListener(Listener listener) {
this.listener = listener;
}
/**
* Clears the {@link Listener}.
*
* <p>This method must be called on the playback thread.
*/
public void clearListener() {
this.listener = null;
}
// ProgressiveMediaPeriod.Listener implementation. // ProgressiveMediaPeriod.Listener implementation.
@Override @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. // If we already have the duration from a previous source info refresh, use it.
durationUs = durationUs == C.TIME_UNSET ? timelineDurationUs : durationUs; durationUs = durationUs == C.TIME_UNSET ? timelineDurationUs : durationUs;
boolean isSeekable = seekMap.isSeekable();
if (!timelineIsPlaceholder if (!timelineIsPlaceholder
&& timelineDurationUs == durationUs && timelineDurationUs == durationUs
&& timelineIsSeekable == isSeekable && timelineIsSeekable == isSeekable
@ -417,6 +457,9 @@ public final class ProgressiveMediaSource extends BaseMediaSource
timelineIsLive = isLive; timelineIsLive = isLive;
timelineIsPlaceholder = false; timelineIsPlaceholder = false;
notifySourceInfoRefreshed(); notifySourceInfoRefreshed();
if (listener != null) {
listener.onSeekMap(this, seekMap);
}
} }
// Internal methods. // Internal methods.

View File

@ -39,6 +39,7 @@ import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.trackselection.FixedTrackSelection; import androidx.media3.exoplayer.trackselection.FixedTrackSelection;
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy; import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy;
import androidx.media3.extractor.SeekMap;
import androidx.media3.test.utils.MediaSourceTestRunner; import androidx.media3.test.utils.MediaSourceTestRunner;
import androidx.media3.test.utils.TestUtil; import androidx.media3.test.utils.TestUtil;
import androidx.media3.test.utils.robolectric.RobolectricUtil; import androidx.media3.test.utils.robolectric.RobolectricUtil;
@ -151,7 +152,8 @@ public class ProgressiveMediaSourceTest {
new ProgressiveMediaSource.Factory(dataSourceFactory) new ProgressiveMediaSource.Factory(dataSourceFactory)
.enableLazyLoadingWithSingleTrack(/* trackId= */ 42, format) .enableLazyLoadingWithSingleTrack(/* trackId= */ 42, format)
.createMediaSource(MediaItem.fromUri(mediaUri)); .createMediaSource(MediaItem.fromUri(mediaUri));
MediaSourceTestRunner mediaSourceTestRunner = new MediaSourceTestRunner(mediaSource); ProgressiveMediaSourceTestRunner mediaSourceTestRunner =
new ProgressiveMediaSourceTestRunner(mediaSource);
ConditionVariable loadCompleted = new ConditionVariable(); ConditionVariable loadCompleted = new ConditionVariable();
mediaSourceTestRunner.runOnPlaybackThread( mediaSourceTestRunner.runOnPlaybackThread(
() -> () ->
@ -168,6 +170,9 @@ public class ProgressiveMediaSourceTest {
} }
})); }));
AtomicReference<SeekMap> seekMapReference = new AtomicReference<>();
ProgressiveMediaSource.Listener listener = (source, seekMap) -> seekMapReference.set(seekMap);
mediaSourceTestRunner.setListener(listener);
Timeline timeline = mediaSourceTestRunner.prepareSource(); Timeline timeline = mediaSourceTestRunner.prepareSource();
MediaPeriod mediaPeriod = MediaPeriod mediaPeriod =
mediaSourceTestRunner.createPeriod( mediaSourceTestRunner.createPeriod(
@ -178,6 +183,7 @@ public class ProgressiveMediaSourceTest {
assertThat(preparedLatch.await(DEFAULT_TIMEOUT_MS, MILLISECONDS)).isTrue(); assertThat(preparedLatch.await(DEFAULT_TIMEOUT_MS, MILLISECONDS)).isTrue();
assertThat(openedUris).isEmpty(); assertThat(openedUris).isEmpty();
assertThat(seekMapReference.get()).isNotNull();
ListenableFuture<Boolean> isLoading = ListenableFuture<Boolean> isLoading =
mediaSourceTestRunner.asyncRunOnPlaybackThread( mediaSourceTestRunner.asyncRunOnPlaybackThread(
@ -205,6 +211,7 @@ public class ProgressiveMediaSourceTest {
assertThat(openedUris).containsExactly(mediaUri); assertThat(openedUris).containsExactly(mediaUri);
mediaSourceTestRunner.releasePeriod(mediaPeriod); mediaSourceTestRunner.releasePeriod(mediaPeriod);
mediaSourceTestRunner.clearListener();
mediaSourceTestRunner.releaseSource(); mediaSourceTestRunner.releaseSource();
mediaSourceTestRunner.release(); mediaSourceTestRunner.release();
} }
@ -279,4 +286,22 @@ public class ProgressiveMediaSourceTest {
/* streamResetFlags= */ new boolean[] {false}, /* streamResetFlags= */ new boolean[] {false},
/* positionUs= */ 0); /* 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);
}
}
} }

View File

@ -360,7 +360,7 @@ public class MediaSourceTestRunner {
playbackThread.quit(); playbackThread.quit();
} }
private class MediaSourceListener implements MediaSourceCaller, MediaSourceEventListener { public class MediaSourceListener implements MediaSourceCaller, MediaSourceEventListener {
// MediaSourceCaller methods. // MediaSourceCaller methods.