Use RTSP Setup response timeout value in KeepAliveMonitor intervalMs

Set KeepAliveMonitor to send a keep-alive message at half the timeout value, if provided, by the RTSP Setup response.

Issue: androidx/media#662
PiperOrigin-RevId: 570946237
This commit is contained in:
michaelkatz 2023-10-05 02:11:23 -07:00 committed by Copybara-Service
parent bf7b91e57e
commit 42c1846984
4 changed files with 107 additions and 22 deletions

View File

@ -33,6 +33,9 @@
* Allow multiple of the same DASH identifier in segment template url. * Allow multiple of the same DASH identifier in segment template url.
* Smooth Streaming Extension: * Smooth Streaming Extension:
* RTSP Extension: * RTSP Extension:
* Use RTSP Setup Response timeout value in time interval of sending
keep-alive RTSP Options requests
([#662](https://github.com/androidx/media/issues/662)).
* Decoder Extensions (FFmpeg, VP9, AV1, MIDI, etc.): * Decoder Extensions (FFmpeg, VP9, AV1, MIDI, etc.):
* Add `DecoderOutputBuffer.shouldBeSkipped` to directly mark output * Add `DecoderOutputBuffer.shouldBeSkipped` to directly mark output
buffers that don't need to be presented. This is preferred over buffers that don't need to be presented. This is preferred over
@ -138,6 +141,8 @@ This release includes the following changes since the
([#577](https://github.com/androidx/media/issues/577)). ([#577](https://github.com/androidx/media/issues/577)).
* Ignore custom Rtsp request methods in Options response public header * Ignore custom Rtsp request methods in Options response public header
([#613](https://github.com/androidx/media/issues/613)). ([#613](https://github.com/androidx/media/issues/613)).
* Decoder Extensions (FFmpeg, VP9, AV1, etc.):
* MIDI extension:
* Leanback extension: * Leanback extension:
* Fix bug where disabling a surface can cause an `ArithmeticException` in * Fix bug where disabling a surface can cause an `ArithmeticException` in
Leanback code ([#617](https://github.com/androidx/media/issues/617)). Leanback code ([#617](https://github.com/androidx/media/issues/617)).

View File

@ -98,7 +98,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public static final int RTSP_STATE_PLAYING = 2; public static final int RTSP_STATE_PLAYING = 2;
private static final String TAG = "RtspClient"; private static final String TAG = "RtspClient";
private static final long DEFAULT_RTSP_KEEP_ALIVE_INTERVAL_MS = 30_000;
/**
* The default divisor used on the session timeout value to be set as the {@link
* KeepAliveMonitor#intervalMs}.
*/
private static final int DEFAULT_RTSP_KEEP_ALIVE_INTERVAL_DIVISOR = 2;
/** A listener for session information update. */ /** A listener for session information update. */
public interface SessionInfoListener { public interface SessionInfoListener {
@ -145,6 +150,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private RtspMessageChannel messageChannel; private RtspMessageChannel messageChannel;
@Nullable private RtspAuthUserInfo rtspAuthUserInfo; @Nullable private RtspAuthUserInfo rtspAuthUserInfo;
@Nullable private String sessionId; @Nullable private String sessionId;
private long sessionTimeoutMs;
@Nullable private KeepAliveMonitor keepAliveMonitor; @Nullable private KeepAliveMonitor keepAliveMonitor;
@Nullable private RtspAuthenticationInfo rtspAuthenticationInfo; @Nullable private RtspAuthenticationInfo rtspAuthenticationInfo;
private @RtspState int rtspState; private @RtspState int rtspState;
@ -186,6 +192,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.messageSender = new MessageSender(); this.messageSender = new MessageSender();
this.uri = RtspMessageUtil.removeUserInfo(uri); this.uri = RtspMessageUtil.removeUserInfo(uri);
this.messageChannel = new RtspMessageChannel(new MessageListener()); this.messageChannel = new RtspMessageChannel(new MessageListener());
this.sessionTimeoutMs = RtspMessageUtil.DEFAULT_RTSP_TIMEOUT_MS;
this.rtspAuthUserInfo = RtspMessageUtil.parseUserInfo(uri); this.rtspAuthUserInfo = RtspMessageUtil.parseUserInfo(uri);
this.pendingSeekPositionUs = C.TIME_UNSET; this.pendingSeekPositionUs = C.TIME_UNSET;
this.rtspState = RTSP_STATE_UNINITIALIZED; this.rtspState = RTSP_STATE_UNINITIALIZED;
@ -733,6 +740,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
rtspState = RTSP_STATE_READY; rtspState = RTSP_STATE_READY;
sessionId = response.sessionHeader.sessionId; sessionId = response.sessionHeader.sessionId;
sessionTimeoutMs = response.sessionHeader.timeoutMs;
continueSetupRtspTrack(); continueSetupRtspTrack();
} }
@ -741,7 +749,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
rtspState = RTSP_STATE_PLAYING; rtspState = RTSP_STATE_PLAYING;
if (keepAliveMonitor == null) { if (keepAliveMonitor == null) {
keepAliveMonitor = new KeepAliveMonitor(DEFAULT_RTSP_KEEP_ALIVE_INTERVAL_MS); keepAliveMonitor =
new KeepAliveMonitor(
/* intervalMs= */ sessionTimeoutMs / DEFAULT_RTSP_KEEP_ALIVE_INTERVAL_DIVISOR);
keepAliveMonitor.start(); keepAliveMonitor.start();
} }

View File

@ -407,13 +407,19 @@ public final class RtspMessageUtilTest {
} }
@Test @Test
public void parseSessionHeader_withSessionIdContainingSpecialCharactersAndTimeout_succeeds() public void parseSessionHeader_usingDefaultTimeout_succeeds() throws Exception {
throws Exception { String sessionHeaderString = "610a63df-9b57.4856_97ac$665f+56e9c04";
String sessionHeaderString = "610a63df-9b57.4856_97ac$665f+56e9c04;timeout=60";
RtspMessageUtil.RtspSessionHeader sessionHeader = RtspMessageUtil.RtspSessionHeader sessionHeader =
RtspMessageUtil.parseSessionHeader(sessionHeaderString); RtspMessageUtil.parseSessionHeader(sessionHeaderString);
assertThat(sessionHeader.sessionId).isEqualTo("610a63df-9b57.4856_97ac$665f+56e9c04"); assertThat(sessionHeader.timeoutMs).isEqualTo(RtspMessageUtil.DEFAULT_RTSP_TIMEOUT_MS);
assertThat(sessionHeader.timeoutMs).isEqualTo(60_000); }
@Test
public void parseSessionHeader_withCustomTimeout_succeeds() throws Exception {
String sessionHeaderString = "610a63df-9b57.4856_97ac$665f+56e9c04;timeout=30";
RtspMessageUtil.RtspSessionHeader sessionHeader =
RtspMessageUtil.parseSessionHeader(sessionHeaderString);
assertThat(sessionHeader.timeoutMs).isEqualTo(30_000);
} }
@Test @Test

View File

@ -46,7 +46,9 @@ import com.google.common.collect.ImmutableList;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import javax.net.SocketFactory; import javax.net.SocketFactory;
import org.junit.After; import org.junit.After;
@ -55,6 +57,7 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;
/** Playback testing for RTSP. */ /** Playback testing for RTSP. */
@Config(sdk = 29) @Config(sdk = 29)
@ -103,9 +106,12 @@ public final class RtspPlaybackTest {
new ResponseProvider( new ResponseProvider(
clock, clock,
ImmutableList.of(aacRtpPacketStreamDump, mpeg2tsRtpPacketStreamDump), ImmutableList.of(aacRtpPacketStreamDump, mpeg2tsRtpPacketStreamDump),
fakeRtpDataChannel); fakeRtpDataChannel,
RtspMessageUtil.DEFAULT_RTSP_TIMEOUT_MS,
/* optionsRequestCounter= */ Optional.empty());
rtspServer = new RtspServer(responseProvider); rtspServer = new RtspServer(responseProvider);
ExoPlayer player = createExoPlayer(rtspServer.startAndGetPortNumber(), rtpDataChannelFactory); ExoPlayer player =
createExoPlayer(clock, rtspServer.startAndGetPortNumber(), rtpDataChannelFactory);
PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory); PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory);
player.prepare(); player.prepare();
@ -125,8 +131,13 @@ public final class RtspPlaybackTest {
rtspServer = rtspServer =
new RtspServer( new RtspServer(
new ResponseProvider( new ResponseProvider(
clock, ImmutableList.of(mpeg2tsRtpPacketStreamDump), fakeRtpDataChannel)); clock,
ExoPlayer player = createExoPlayer(rtspServer.startAndGetPortNumber(), rtpDataChannelFactory); ImmutableList.of(mpeg2tsRtpPacketStreamDump),
fakeRtpDataChannel,
RtspMessageUtil.DEFAULT_RTSP_TIMEOUT_MS,
/* optionsRequestCounter= */ Optional.empty()));
ExoPlayer player =
createExoPlayer(clock, rtspServer.startAndGetPortNumber(), rtpDataChannelFactory);
AtomicReference<Throwable> playbackError = new AtomicReference<>(); AtomicReference<Throwable> playbackError = new AtomicReference<>();
player.prepare(); player.prepare();
@ -158,7 +169,7 @@ public final class RtspPlaybackTest {
new UdpDataSourceRtpDataChannelFactory(DEFAULT_TIMEOUT_MS), rtpTcpDataChannelFactory); new UdpDataSourceRtpDataChannelFactory(DEFAULT_TIMEOUT_MS), rtpTcpDataChannelFactory);
rtspServer = new RtspServer(responseProviderSupportingOnlyTcp); rtspServer = new RtspServer(responseProviderSupportingOnlyTcp);
ExoPlayer player = ExoPlayer player =
createExoPlayer(rtspServer.startAndGetPortNumber(), forwardingRtpDataChannelFactory); createExoPlayer(clock, rtspServer.startAndGetPortNumber(), forwardingRtpDataChannelFactory);
PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory); PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory);
player.prepare(); player.prepare();
@ -183,7 +194,8 @@ public final class RtspPlaybackTest {
ImmutableList.of(aacRtpPacketStreamDump, mpeg2tsRtpPacketStreamDump), ImmutableList.of(aacRtpPacketStreamDump, mpeg2tsRtpPacketStreamDump),
fakeUdpRtpDataChannel); fakeUdpRtpDataChannel);
rtspServer = new RtspServer(responseProvider); rtspServer = new RtspServer(responseProvider);
ExoPlayer player = createExoPlayer(rtspServer.startAndGetPortNumber(), rtpDataChannelFactory); ExoPlayer player =
createExoPlayer(clock, rtspServer.startAndGetPortNumber(), rtpDataChannelFactory);
AtomicReference<PlaybackException> playbackError = new AtomicReference<>(); AtomicReference<PlaybackException> playbackError = new AtomicReference<>();
player.prepare(); player.prepare();
@ -220,7 +232,7 @@ public final class RtspPlaybackTest {
new ForwardingRtpDataChannelFactory(rtpDataChannelFactory, rtpDataChannelFactory); new ForwardingRtpDataChannelFactory(rtpDataChannelFactory, rtpDataChannelFactory);
rtspServer = new RtspServer(responseProviderSupportingOnlyTcp); rtspServer = new RtspServer(responseProviderSupportingOnlyTcp);
ExoPlayer player = ExoPlayer player =
createExoPlayer(rtspServer.startAndGetPortNumber(), forwardingRtpDataChannelFactory); createExoPlayer(clock, rtspServer.startAndGetPortNumber(), forwardingRtpDataChannelFactory);
AtomicReference<PlaybackException> playbackError = new AtomicReference<>(); AtomicReference<PlaybackException> playbackError = new AtomicReference<>();
player.prepare(); player.prepare();
@ -240,8 +252,39 @@ public final class RtspPlaybackTest {
assertThat(playbackError.get()).hasCauseThat().hasMessageThat().isEqualTo("SETUP 461"); assertThat(playbackError.get()).hasCauseThat().hasMessageThat().isEqualTo("SETUP 461");
} }
@Test
public void play_withCustomSessionTimeoutDuration_sendsKeepAliveOptionsRequest()
throws Exception {
FakeUdpDataSourceRtpDataChannel fakeRtpDataChannel = new FakeUdpDataSourceRtpDataChannel();
RtpDataChannel.Factory rtpDataChannelFactory = (trackId) -> fakeRtpDataChannel;
FakeClock fakeClock = new FakeClock(/* initialTimeMs= */ 0, true);
Optional<AtomicInteger> optionsRequestCounter = Optional.of(new AtomicInteger());
ResponseProvider responseProvider =
new ResponseProvider(
fakeClock,
ImmutableList.of(aacRtpPacketStreamDump),
fakeRtpDataChannel,
/* sessionTimeoutMs= */ 30_000L,
optionsRequestCounter);
rtspServer = new RtspServer(responseProvider);
ExoPlayer player =
createExoPlayer(fakeClock, rtspServer.startAndGetPortNumber(), rtpDataChannelFactory);
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
// Reset optionsRequestCounter to count requests made by the keep-alive monitor
optionsRequestCounter.get().getAndSet(0);
fakeClock.advanceTime(/* timeDiffMs= */ 16_000L);
ShadowLooper.idleMainLooper();
assertThat(optionsRequestCounter.get().get()).isEqualTo(1);
player.release();
}
private ExoPlayer createExoPlayer( private ExoPlayer createExoPlayer(
int serverRtspPortNumber, RtpDataChannel.Factory rtpDataChannelFactory) { Clock clock, int serverRtspPortNumber, RtpDataChannel.Factory rtpDataChannelFactory) {
ExoPlayer player = ExoPlayer player =
new ExoPlayer.Builder(applicationContext, capturingRenderersFactory) new ExoPlayer.Builder(applicationContext, capturingRenderersFactory)
.setClock(clock) .setClock(clock)
@ -260,11 +303,14 @@ public final class RtspPlaybackTest {
private static class ResponseProvider implements RtspServer.ResponseProvider { private static class ResponseProvider implements RtspServer.ResponseProvider {
protected static final String SESSION_ID = "00000000"; protected static final String SESSION_ID = "00000000";
private static final String SESSION_TIMEOUT_HEADER_TAG = ";timeout=";
protected final Clock clock; protected final Clock clock;
protected final ArrayList<RtpPacketStreamDump> dumpsForSetUpTracks; protected final List<RtpPacketStreamDump> dumpsForSetUpTracks = new ArrayList<>();
protected final ImmutableList<RtpPacketStreamDump> rtpPacketStreamDumps; protected final ImmutableList<RtpPacketStreamDump> rtpPacketStreamDumps;
private final RtspMessageChannel.InterleavedBinaryDataListener binaryDataListener; private final RtspMessageChannel.InterleavedBinaryDataListener binaryDataListener;
private final long sessionTimeoutMs;
private final Optional<AtomicInteger> optionsRequestCounter;
protected RtpPacketTransmitter packetTransmitter; protected RtpPacketTransmitter packetTransmitter;
@ -275,15 +321,21 @@ public final class RtspPlaybackTest {
* @param rtpPacketStreamDumps A list of {@link RtpPacketStreamDump}. * @param rtpPacketStreamDumps A list of {@link RtpPacketStreamDump}.
* @param binaryDataListener A {@link RtspMessageChannel.InterleavedBinaryDataListener} to send * @param binaryDataListener A {@link RtspMessageChannel.InterleavedBinaryDataListener} to send
* RTP data. * RTP data.
* @param sessionTimeoutMs Duration RTSP server will keep the session active without receiving
* any requests.
* @param optionsRequestCounter for how many RTSP Options requests were sent.
*/ */
public ResponseProvider( ResponseProvider(
Clock clock, Clock clock,
List<RtpPacketStreamDump> rtpPacketStreamDumps, List<RtpPacketStreamDump> rtpPacketStreamDumps,
RtspMessageChannel.InterleavedBinaryDataListener binaryDataListener) { RtspMessageChannel.InterleavedBinaryDataListener binaryDataListener,
long sessionTimeoutMs,
Optional<AtomicInteger> optionsRequestCounter) {
this.clock = clock; this.clock = clock;
this.rtpPacketStreamDumps = ImmutableList.copyOf(rtpPacketStreamDumps); this.rtpPacketStreamDumps = ImmutableList.copyOf(rtpPacketStreamDumps);
this.binaryDataListener = binaryDataListener; this.binaryDataListener = binaryDataListener;
dumpsForSetUpTracks = new ArrayList<>(); this.sessionTimeoutMs = sessionTimeoutMs;
this.optionsRequestCounter = optionsRequestCounter;
} }
/** Returns a list of the received SETUP requests' corresponding {@link RtpPacketStreamDump}. */ /** Returns a list of the received SETUP requests' corresponding {@link RtpPacketStreamDump}. */
@ -295,6 +347,7 @@ public final class RtspPlaybackTest {
@Override @Override
public RtspResponse getOptionsResponse() { public RtspResponse getOptionsResponse() {
optionsRequestCounter.ifPresent(AtomicInteger::getAndIncrement);
return new RtspResponse( return new RtspResponse(
/* status= */ 200, /* status= */ 200,
new RtspHeaders.Builder() new RtspHeaders.Builder()
@ -316,9 +369,15 @@ public final class RtspPlaybackTest {
packetTransmitter = new RtpPacketTransmitter(rtpPacketStreamDump, clock); packetTransmitter = new RtpPacketTransmitter(rtpPacketStreamDump, clock);
} }
} }
return new RtspResponse( return new RtspResponse(
/* status= */ 200, headers.buildUpon().add(RtspHeaders.SESSION, SESSION_ID).build()); /* status= */ 200,
headers
.buildUpon()
.add(
RtspHeaders.SESSION,
// Convert sessionTimeoutMs to seconds
SESSION_ID + SESSION_TIMEOUT_HEADER_TAG + (sessionTimeoutMs / 1000))
.build());
} }
@Override @Override
@ -348,7 +407,12 @@ public final class RtspPlaybackTest {
Clock clock, Clock clock,
List<RtpPacketStreamDump> rtpPacketStreamDumps, List<RtpPacketStreamDump> rtpPacketStreamDumps,
RtspMessageChannel.InterleavedBinaryDataListener binaryDataListener) { RtspMessageChannel.InterleavedBinaryDataListener binaryDataListener) {
super(clock, rtpPacketStreamDumps, binaryDataListener); super(
clock,
rtpPacketStreamDumps,
binaryDataListener,
RtspMessageUtil.DEFAULT_RTSP_TIMEOUT_MS,
/* optionsRequestCounter= */ Optional.empty());
} }
@Override @Override