diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpDataChannel.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpDataChannel.java index 1e64330b3c..012f7bfebd 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpDataChannel.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpDataChannel.java @@ -53,6 +53,9 @@ import java.io.IOException; */ int getLocalPort(); + /** Returns whether the {@code RtpDataChannel} needs to be closed when loading completes. */ + boolean needsClosingOnLoadCompletion(); + /** * Returns a {@link InterleavedBinaryDataListener} if the implementation supports receiving RTP * packets on a side-band protocol, for example RTP-over-RTSP; otherwise {@code null}. diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpDataLoadable.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpDataLoadable.java index c5e227ac1a..debd66c472 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpDataLoadable.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpDataLoadable.java @@ -27,7 +27,6 @@ import androidx.media3.datasource.DataSourceUtil; import androidx.media3.exoplayer.upstream.Loader; import androidx.media3.extractor.DefaultExtractorInput; import androidx.media3.extractor.Extractor; -import androidx.media3.extractor.ExtractorInput; import androidx.media3.extractor.ExtractorOutput; import androidx.media3.extractor.PositionHolder; import java.io.IOException; @@ -77,7 +76,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final Handler playbackThreadHandler; private final RtpDataChannel.Factory rtpDataChannelFactory; + @Nullable private RtpDataChannel dataChannel; private @MonotonicNonNull RtpExtractor extractor; + private @MonotonicNonNull DefaultExtractorInput extractorInput; private volatile boolean loadCancelled; private volatile long pendingSeekPositionUs; @@ -142,36 +143,49 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public void load() throws IOException { - @Nullable RtpDataChannel dataChannel = null; + // Allows to resume loading after canceling load. + if (loadCancelled) { + loadCancelled = false; + } + try { - dataChannel = rtpDataChannelFactory.createAndOpenDataChannel(trackId); - String transport = dataChannel.getTransport(); + if (dataChannel == null) { + dataChannel = rtpDataChannelFactory.createAndOpenDataChannel(trackId); + String transport = dataChannel.getTransport(); - RtpDataChannel finalDataChannel = dataChannel; - playbackThreadHandler.post(() -> eventListener.onTransportReady(transport, finalDataChannel)); + RtpDataChannel finalDataChannel = dataChannel; + playbackThreadHandler.post( + () -> eventListener.onTransportReady(transport, finalDataChannel)); - // Sets up the extractor. - ExtractorInput extractorInput = - new DefaultExtractorInput( - checkNotNull(dataChannel), /* position= */ 0, /* length= */ C.LENGTH_UNSET); - extractor = new RtpExtractor(rtspMediaTrack.payloadFormat, trackId); - extractor.init(output); + extractorInput = + new DefaultExtractorInput( + checkNotNull(dataChannel), /* position= */ 0, /* length= */ C.LENGTH_UNSET); + extractor = new RtpExtractor(rtspMediaTrack.payloadFormat, trackId); + extractor.init(output); + } while (!loadCancelled) { if (pendingSeekPositionUs != C.TIME_UNSET) { - extractor.seek(nextRtpTimestamp, pendingSeekPositionUs); + checkNotNull(extractor).seek(nextRtpTimestamp, pendingSeekPositionUs); pendingSeekPositionUs = C.TIME_UNSET; } @Extractor.ReadResult - int readResult = extractor.read(extractorInput, /* seekPosition= */ new PositionHolder()); + int readResult = + checkNotNull(extractor) + .read(checkNotNull(extractorInput), /* seekPosition= */ new PositionHolder()); if (readResult == Extractor.RESULT_END_OF_INPUT) { // Loading is finished. break; } } + // Resets the flag if user cancels loading. + loadCancelled = false; } finally { - DataSourceUtil.closeQuietly(dataChannel); + if (checkNotNull(dataChannel).needsClosingOnLoadCompletion()) { + DataSourceUtil.closeQuietly(dataChannel); + dataChannel = null; + } } } diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspClient.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspClient.java index 751731fce6..8056d5687e 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspClient.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspClient.java @@ -230,6 +230,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; messageSender.sendPlayRequest(uri, offsetMs, checkNotNull(sessionId)); } + public void signalPlaybackEnded() { + rtspState = RTSP_STATE_READY; + } + /** * Seeks to a specific time using RTSP. * @@ -723,7 +727,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } private void onPlayResponseReceived(RtspPlayResponse response) { - checkState(rtspState == RTSP_STATE_READY); + checkState(rtspState == RTSP_STATE_READY || rtspState == RTSP_STATE_PLAYING); rtspState = RTSP_STATE_PLAYING; if (keepAliveMonitor == null) { 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 1af0f2b415..e766780a77 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 @@ -19,6 +19,7 @@ package androidx.media3.exoplayer.rtsp; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkStateNotNull; +import static androidx.media3.common.util.Util.usToMs; import static java.lang.Math.min; import android.net.Uri; @@ -314,7 +315,22 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } pendingSeekPositionUs = positionUs; - rtspClient.seekToUs(positionUs); + + if (loadingFinished) { + for (int i = 0; i < rtspLoaderWrappers.size(); i++) { + rtspLoaderWrappers.get(i).resumeLoad(); + } + + if (isUsingRtpTcp) { + rtspClient.startPlayback(/* offsetMs= */ usToMs(positionUs)); + } else { + rtspClient.seekToUs(positionUs); + } + + } else { + rtspClient.seekToUs(positionUs); + } + for (int i = 0; i < rtspLoaderWrappers.size(); i++) { rtspLoaderWrappers.get(i).seekTo(positionUs); } @@ -530,6 +546,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; break; } } + + rtspClient.signalPlaybackEnded(); } @Override @@ -817,6 +835,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } + /** Resumes loading after {@linkplain #cancelLoad() loading is canceled}. */ + public void resumeLoad() { + checkState(canceled); + canceled = false; + updateLoadingFinished(); + startLoading(); + } + /** Resets the {@link Loadable} and {@link SampleQueue} to prepare for an RTSP seek. */ public void seekTo(long positionUs) { if (!canceled) { diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/TransferRtpDataChannel.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/TransferRtpDataChannel.java index 286ca8858e..11de686944 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/TransferRtpDataChannel.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/TransferRtpDataChannel.java @@ -69,6 +69,12 @@ import java.util.concurrent.LinkedBlockingQueue; return channelNumber; } + @Override + public boolean needsClosingOnLoadCompletion() { + // TCP channel is managed by the RTSP mesasge channel and does not need closing from here. + return false; + } + @Override public InterleavedBinaryDataListener getInterleavedBinaryDataListener() { return this; diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/UdpDataSourceRtpDataChannel.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/UdpDataSourceRtpDataChannel.java index 16817828fc..23e0159cac 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/UdpDataSourceRtpDataChannel.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/UdpDataSourceRtpDataChannel.java @@ -64,6 +64,11 @@ import java.io.IOException; return port == UdpDataSource.UDP_PORT_UNSET ? C.INDEX_UNSET : port; } + @Override + public boolean needsClosingOnLoadCompletion() { + return true; + } + @Nullable @Override public RtspMessageChannel.InterleavedBinaryDataListener getInterleavedBinaryDataListener() { diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspPlaybackTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspPlaybackTest.java index 37650c297b..e351420846 100644 --- a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspPlaybackTest.java +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspPlaybackTest.java @@ -441,6 +441,11 @@ public final class RtspPlaybackTest { return Util.formatInvariant("RTP/AVP;unicast;client_port=%d-%d", LOCAL_PORT, LOCAL_PORT + 1); } + @Override + public boolean needsClosingOnLoadCompletion() { + return false; + } + @Override public RtspMessageChannel.InterleavedBinaryDataListener getInterleavedBinaryDataListener() { return null; @@ -454,6 +459,11 @@ public final class RtspPlaybackTest { return Util.formatInvariant( "RTP/AVP/TCP;unicast;interleaved=%d-%d", LOCAL_PORT + 2, LOCAL_PORT + 3); } + + @Override + public boolean needsClosingOnLoadCompletion() { + return false; + } } private static class ForwardingRtpDataChannelFactory implements RtpDataChannel.Factory {