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,