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:
parent
011fc9d5d3
commit
29fc16484a
@ -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;
|
|||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
@ -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() {
|
||||||
|
@ -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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user