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 {
/**
* 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);
}

View File

@ -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.
*
* <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. */
@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}.
*
* <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.
@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.

View File

@ -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<SeekMap> 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<Boolean> 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);
}
}
}

View File

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