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();
/** 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}.

View File

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

View File

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

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.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) {

View File

@ -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;

View File

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

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