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
This commit is contained in:
claincly 2023-04-16 16:03:22 +01:00 committed by Rohit Singh
parent 011fc9d5d3
commit 29fc16484a
7 changed files with 85 additions and 17 deletions

View File

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

View File

@ -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;
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;
@Override @Override
public void load() throws IOException { public void load() throws IOException {
@Nullable RtpDataChannel dataChannel = null; // Allows to resume loading after canceling load.
if (loadCancelled) {
loadCancelled = false;
}
try { try {
dataChannel = rtpDataChannelFactory.createAndOpenDataChannel(trackId); if (dataChannel == null) {
String transport = dataChannel.getTransport(); dataChannel = rtpDataChannelFactory.createAndOpenDataChannel(trackId);
String transport = dataChannel.getTransport();
RtpDataChannel finalDataChannel = dataChannel; RtpDataChannel finalDataChannel = dataChannel;
playbackThreadHandler.post(() -> eventListener.onTransportReady(transport, finalDataChannel)); playbackThreadHandler.post(
() -> eventListener.onTransportReady(transport, finalDataChannel));
// Sets up the extractor. extractorInput =
ExtractorInput extractorInput = new DefaultExtractorInput(
new DefaultExtractorInput( checkNotNull(dataChannel), /* position= */ 0, /* length= */ C.LENGTH_UNSET);
checkNotNull(dataChannel), /* position= */ 0, /* length= */ C.LENGTH_UNSET); extractor = new RtpExtractor(rtspMediaTrack.payloadFormat, trackId);
extractor = new RtpExtractor(rtspMediaTrack.payloadFormat, trackId); extractor.init(output);
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;
}
} }
} }

View File

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

View File

@ -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;
} }
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;
break; break;
} }
} }
rtspClient.signalPlaybackEnded();
} }
@Override @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. */ /** 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) {

View File

@ -69,6 +69,12 @@ import java.util.concurrent.LinkedBlockingQueue;
return channelNumber; 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 @Override
public InterleavedBinaryDataListener getInterleavedBinaryDataListener() { public InterleavedBinaryDataListener getInterleavedBinaryDataListener() {
return this; return this;

View File

@ -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() {

View File

@ -441,6 +441,11 @@ public final class RtspPlaybackTest {
return Util.formatInvariant("RTP/AVP;unicast;client_port=%d-%d", LOCAL_PORT, LOCAL_PORT + 1); return Util.formatInvariant("RTP/AVP;unicast;client_port=%d-%d", LOCAL_PORT, LOCAL_PORT + 1);
} }
@Override
public boolean needsClosingOnLoadCompletion() {
return false;
}
@Override @Override
public RtspMessageChannel.InterleavedBinaryDataListener getInterleavedBinaryDataListener() { public RtspMessageChannel.InterleavedBinaryDataListener getInterleavedBinaryDataListener() {
return null; return null;
@ -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 {