From 914cf423bd308489455df173b50333864ee44a33 Mon Sep 17 00:00:00 2001 From: claincly Date: Fri, 21 Jan 2022 11:22:53 +0000 Subject: [PATCH] Handle when RTSP track timing is not available. Issue: google/ExoPlayer#9775 We got a few issues for this on GH already. Some RTSP servers do not provide track timing in PLAY responses, or the timings are invalid. Missing timing means the RTSP stream is not seekable. Added method to 1. Update the timeline that seek is not possible 2. Report read discontinuity so that playback can start from the beginning. PiperOrigin-RevId: 423281439 --- .../exoplayer/rtsp/RtspMediaPeriod.java | 51 ++++++++++++++++--- .../exoplayer/rtsp/RtspMediaSource.java | 21 +++++--- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaPeriod.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaPeriod.java index 51bcdf404e..b0ba5c4c5e 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaPeriod.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaPeriod.java @@ -69,6 +69,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** Called when the {@link RtspSessionTiming} is available. */ void onSourceInfoRefreshed(RtspSessionTiming timing); + + /** Called when the RTSP server does not support seeking. */ + default void onSeekingUnsupported() {} } /** The maximum times to retry if the underlying data channel failed to bind. */ @@ -92,6 +95,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private long pendingSeekPositionUs; private long pendingSeekPositionUsForTcpRetry; private boolean loadingFinished; + private boolean notifyDiscontinuity; private boolean released; private boolean prepared; private boolean trackSelected; @@ -245,6 +249,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public long readDiscontinuity() { + // Discontinuity only happens in RTSP when seeking an unexpectedly un-seekable RTSP server (a + // server that doesn't include the required RTP-Info header in its PLAY responses). This only + // applies to seeks made before receiving the first RTSP PLAY response. The playback can only + // start from time zero in this case. + if (notifyDiscontinuity) { + notifyDiscontinuity = false; + return 0; + } return C.TIME_UNSET; } @@ -357,7 +369,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // SampleStream methods. /* package */ boolean isReady(int trackGroupIndex) { - return rtspLoaderWrappers.get(trackGroupIndex).isSampleQueueReady(); + return !suppressRead() && rtspLoaderWrappers.get(trackGroupIndex).isSampleQueueReady(); } @ReadDataResult @@ -366,9 +378,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; FormatHolder formatHolder, DecoderInputBuffer buffer, @ReadFlags int readFlags) { + if (suppressRead()) { + return C.RESULT_NOTHING_READ; + } return rtspLoaderWrappers.get(sampleQueueIndex).read(formatHolder, buffer, readFlags); } + /* package */ int skipData(int sampleQueueIndex, long positionUs) { + if (suppressRead()) { + return C.RESULT_NOTHING_READ; + } + return rtspLoaderWrappers.get(sampleQueueIndex).skipData(positionUs); + } + + private boolean suppressRead() { + return notifyDiscontinuity; + } + // Internal methods. @Nullable @@ -551,7 +577,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public void onPlaybackStarted( long startPositionUs, ImmutableList trackTimingList) { - // Validate that the trackTimingList contains timings for the selected tracks. + + // Validate that the trackTimingList contains timings for the selected tracks, and notify the + // listener. ArrayList trackUrisWithTiming = new ArrayList<>(trackTimingList.size()); for (int i = 0; i < trackTimingList.size(); i++) { trackUrisWithTiming.add(checkNotNull(trackTimingList.get(i).uri.getPath())); @@ -559,10 +587,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; for (int i = 0; i < selectedLoadInfos.size(); i++) { RtpLoadInfo loadInfo = selectedLoadInfos.get(i); if (!trackUrisWithTiming.contains(loadInfo.getTrackUri().getPath())) { - playbackException = - new RtspPlaybackException( - "Server did not provide timing for track " + loadInfo.getTrackUri()); - return; + listener.onSeekingUnsupported(); + if (isSeekPending()) { + notifyDiscontinuity = true; + pendingSeekPositionUs = C.TIME_UNSET; + requestedSeekPositionUs = C.TIME_UNSET; + pendingSeekPositionUsForTcpRetry = C.TIME_UNSET; + } } } @@ -699,7 +730,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public int skipData(long positionUs) { - return 0; + return RtspMediaPeriod.this.skipData(track, positionUs); } } @@ -750,6 +781,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return sampleQueue.read(formatHolder, buffer, readFlags, /* loadingFinished= */ canceled); } + public int skipData(long positionUs) { + int skipCount = sampleQueue.getSkipCount(positionUs, /* allowEndOfQueue= */ canceled); + sampleQueue.skip(skipCount); + return skipCount; + } + /** Cancels loading. */ public void cancelLoad() { if (!canceled) { diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaSource.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaSource.java index bc91dd8b93..031c8505e5 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaSource.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaSource.java @@ -257,12 +257,21 @@ public final class RtspMediaSource extends BaseMediaSource { allocator, rtpDataChannelFactory, uri, - /* listener= */ timing -> { - timelineDurationUs = Util.msToUs(timing.getDurationMs()); - timelineIsSeekable = !timing.isLive(); - timelineIsLive = timing.isLive(); - timelineIsPlaceholder = false; - notifySourceInfoRefreshed(); + new RtspMediaPeriod.Listener() { + @Override + public void onSourceInfoRefreshed(RtspSessionTiming timing) { + timelineDurationUs = Util.msToUs(timing.getDurationMs()); + timelineIsSeekable = !timing.isLive(); + timelineIsLive = timing.isLive(); + timelineIsPlaceholder = false; + notifySourceInfoRefreshed(); + } + + @Override + public void onSeekingUnsupported() { + timelineIsSeekable = false; + notifySourceInfoRefreshed(); + } }, userAgent, socketFactory,