Commit 29fc1648 by claincly Committed by Rohit Singh

Handle seek after playback ends

Reusing the loader wrappers allows us to use the current RTSP connection
without having to set up a new RTSP connection. Consequently, the Extractors,
RTP readers are also preserved.

PiperOrigin-RevId: 524663012
parent 011fc9d5
...@@ -53,6 +53,9 @@ import java.io.IOException; ...@@ -53,6 +53,9 @@ import java.io.IOException;
*/ */
int getLocalPort(); 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 * Returns a {@link InterleavedBinaryDataListener} if the implementation supports receiving RTP
* packets on a side-band protocol, for example RTP-over-RTSP; otherwise {@code null}. * packets on a side-band protocol, for example RTP-over-RTSP; otherwise {@code null}.
......
...@@ -27,7 +27,6 @@ import androidx.media3.datasource.DataSourceUtil; ...@@ -27,7 +27,6 @@ import androidx.media3.datasource.DataSourceUtil;
import androidx.media3.exoplayer.upstream.Loader; import androidx.media3.exoplayer.upstream.Loader;
import androidx.media3.extractor.DefaultExtractorInput; import androidx.media3.extractor.DefaultExtractorInput;
import androidx.media3.extractor.Extractor; import androidx.media3.extractor.Extractor;
import androidx.media3.extractor.ExtractorInput;
import androidx.media3.extractor.ExtractorOutput; import androidx.media3.extractor.ExtractorOutput;
import androidx.media3.extractor.PositionHolder; import androidx.media3.extractor.PositionHolder;
import java.io.IOException; import java.io.IOException;
...@@ -77,7 +76,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -77,7 +76,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final Handler playbackThreadHandler; private final Handler playbackThreadHandler;
private final RtpDataChannel.Factory rtpDataChannelFactory; private final RtpDataChannel.Factory rtpDataChannelFactory;
@Nullable private RtpDataChannel dataChannel;
private @MonotonicNonNull RtpExtractor extractor; private @MonotonicNonNull RtpExtractor extractor;
private @MonotonicNonNull DefaultExtractorInput extractorInput;
private volatile boolean loadCancelled; private volatile boolean loadCancelled;
private volatile long pendingSeekPositionUs; private volatile long pendingSeekPositionUs;
...@@ -142,36 +143,49 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -142,36 +143,49 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override @Override
public void load() throws IOException { public void load() throws IOException {
@Nullable RtpDataChannel dataChannel = null; // Allows to resume loading after canceling load.
try { if (loadCancelled) {
dataChannel = rtpDataChannelFactory.createAndOpenDataChannel(trackId); loadCancelled = false;
String transport = dataChannel.getTransport(); }
RtpDataChannel finalDataChannel = dataChannel;
playbackThreadHandler.post(() -> eventListener.onTransportReady(transport, finalDataChannel));
// Sets up the extractor. try {
ExtractorInput extractorInput = if (dataChannel == null) {
new DefaultExtractorInput( dataChannel = rtpDataChannelFactory.createAndOpenDataChannel(trackId);
checkNotNull(dataChannel), /* position= */ 0, /* length= */ C.LENGTH_UNSET); String transport = dataChannel.getTransport();
extractor = new RtpExtractor(rtspMediaTrack.payloadFormat, trackId);
extractor.init(output); RtpDataChannel finalDataChannel = dataChannel;
playbackThreadHandler.post(
() -> eventListener.onTransportReady(transport, finalDataChannel));
extractorInput =
new DefaultExtractorInput(
checkNotNull(dataChannel), /* position= */ 0, /* length= */ C.LENGTH_UNSET);
extractor = new RtpExtractor(rtspMediaTrack.payloadFormat, trackId);
extractor.init(output);
}
while (!loadCancelled) { while (!loadCancelled) {
if (pendingSeekPositionUs != C.TIME_UNSET) { if (pendingSeekPositionUs != C.TIME_UNSET) {
extractor.seek(nextRtpTimestamp, pendingSeekPositionUs); checkNotNull(extractor).seek(nextRtpTimestamp, pendingSeekPositionUs);
pendingSeekPositionUs = C.TIME_UNSET; pendingSeekPositionUs = C.TIME_UNSET;
} }
@Extractor.ReadResult @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) { if (readResult == Extractor.RESULT_END_OF_INPUT) {
// Loading is finished. // Loading is finished.
break; break;
} }
} }
// Resets the flag if user cancels loading.
loadCancelled = false;
} finally { } finally {
DataSourceUtil.closeQuietly(dataChannel); if (checkNotNull(dataChannel).needsClosingOnLoadCompletion()) {
DataSourceUtil.closeQuietly(dataChannel);
dataChannel = null;
}
} }
} }
......
...@@ -230,6 +230,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -230,6 +230,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
messageSender.sendPlayRequest(uri, offsetMs, checkNotNull(sessionId)); messageSender.sendPlayRequest(uri, offsetMs, checkNotNull(sessionId));
} }
public void signalPlaybackEnded() {
rtspState = RTSP_STATE_READY;
}
/** /**
* Seeks to a specific time using RTSP. * Seeks to a specific time using RTSP.
* *
...@@ -723,7 +727,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -723,7 +727,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
private void onPlayResponseReceived(RtspPlayResponse response) { private void onPlayResponseReceived(RtspPlayResponse response) {
checkState(rtspState == RTSP_STATE_READY); checkState(rtspState == RTSP_STATE_READY || rtspState == RTSP_STATE_PLAYING);
rtspState = RTSP_STATE_PLAYING; rtspState = RTSP_STATE_PLAYING;
if (keepAliveMonitor == null) { if (keepAliveMonitor == null) {
......
...@@ -19,6 +19,7 @@ package androidx.media3.exoplayer.rtsp; ...@@ -19,6 +19,7 @@ package androidx.media3.exoplayer.rtsp;
import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.usToMs;
import static java.lang.Math.min; import static java.lang.Math.min;
import android.net.Uri; import android.net.Uri;
...@@ -314,7 +315,22 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -314,7 +315,22 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
pendingSeekPositionUs = positionUs; 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++) { for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
rtspLoaderWrappers.get(i).seekTo(positionUs); rtspLoaderWrappers.get(i).seekTo(positionUs);
} }
...@@ -530,6 +546,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -530,6 +546,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
break; break;
} }
} }
rtspClient.signalPlaybackEnded();
} }
@Override @Override
...@@ -817,6 +835,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -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. */ /** Resets the {@link Loadable} and {@link SampleQueue} to prepare for an RTSP seek. */
public void seekTo(long positionUs) { public void seekTo(long positionUs) {
if (!canceled) { if (!canceled) {
......
...@@ -70,6 +70,12 @@ import java.util.concurrent.LinkedBlockingQueue; ...@@ -70,6 +70,12 @@ import java.util.concurrent.LinkedBlockingQueue;
} }
@Override @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() { public InterleavedBinaryDataListener getInterleavedBinaryDataListener() {
return this; return this;
} }
......
...@@ -64,6 +64,11 @@ import java.io.IOException; ...@@ -64,6 +64,11 @@ import java.io.IOException;
return port == UdpDataSource.UDP_PORT_UNSET ? C.INDEX_UNSET : port; return port == UdpDataSource.UDP_PORT_UNSET ? C.INDEX_UNSET : port;
} }
@Override
public boolean needsClosingOnLoadCompletion() {
return true;
}
@Nullable @Nullable
@Override @Override
public RtspMessageChannel.InterleavedBinaryDataListener getInterleavedBinaryDataListener() { public RtspMessageChannel.InterleavedBinaryDataListener getInterleavedBinaryDataListener() {
......
...@@ -442,6 +442,11 @@ public final class RtspPlaybackTest { ...@@ -442,6 +442,11 @@ public final class RtspPlaybackTest {
} }
@Override @Override
public boolean needsClosingOnLoadCompletion() {
return false;
}
@Override
public RtspMessageChannel.InterleavedBinaryDataListener getInterleavedBinaryDataListener() { public RtspMessageChannel.InterleavedBinaryDataListener getInterleavedBinaryDataListener() {
return null; return null;
} }
...@@ -454,6 +459,11 @@ public final class RtspPlaybackTest { ...@@ -454,6 +459,11 @@ public final class RtspPlaybackTest {
return Util.formatInvariant( return Util.formatInvariant(
"RTP/AVP/TCP;unicast;interleaved=%d-%d", LOCAL_PORT + 2, LOCAL_PORT + 3); "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 { private static class ForwardingRtpDataChannelFactory implements RtpDataChannel.Factory {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment