diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 50cb07500a..51e6bd1913 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -33,6 +33,9 @@ * Allow multiple of the same DASH identifier in segment template url. * Smooth Streaming 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.): * Add `DecoderOutputBuffer.shouldBeSkipped` to directly mark output 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)). * Ignore custom Rtsp request methods in Options response public header ([#613](https://github.com/androidx/media/issues/613)). +* Decoder Extensions (FFmpeg, VP9, AV1, etc.): +* MIDI extension: * Leanback extension: * Fix bug where disabling a surface can cause an `ArithmeticException` in Leanback code ([#617](https://github.com/androidx/media/issues/617)). diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspClient.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspClient.java index 1589a2c43d..11c47251a7 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspClient.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspClient.java @@ -98,7 +98,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; public static final int RTSP_STATE_PLAYING = 2; 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. */ public interface SessionInfoListener { @@ -145,6 +150,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private RtspMessageChannel messageChannel; @Nullable private RtspAuthUserInfo rtspAuthUserInfo; @Nullable private String sessionId; + private long sessionTimeoutMs; @Nullable private KeepAliveMonitor keepAliveMonitor; @Nullable private RtspAuthenticationInfo rtspAuthenticationInfo; private @RtspState int rtspState; @@ -186,6 +192,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; this.messageSender = new MessageSender(); this.uri = RtspMessageUtil.removeUserInfo(uri); this.messageChannel = new RtspMessageChannel(new MessageListener()); + this.sessionTimeoutMs = RtspMessageUtil.DEFAULT_RTSP_TIMEOUT_MS; this.rtspAuthUserInfo = RtspMessageUtil.parseUserInfo(uri); this.pendingSeekPositionUs = C.TIME_UNSET; this.rtspState = RTSP_STATE_UNINITIALIZED; @@ -733,6 +740,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; rtspState = RTSP_STATE_READY; sessionId = response.sessionHeader.sessionId; + sessionTimeoutMs = response.sessionHeader.timeoutMs; continueSetupRtspTrack(); } @@ -741,7 +749,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; rtspState = RTSP_STATE_PLAYING; 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(); } diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspMessageUtilTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspMessageUtilTest.java index d970117d4d..7439ea3eeb 100644 --- a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspMessageUtilTest.java +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspMessageUtilTest.java @@ -407,13 +407,19 @@ public final class RtspMessageUtilTest { } @Test - public void parseSessionHeader_withSessionIdContainingSpecialCharactersAndTimeout_succeeds() - throws Exception { - String sessionHeaderString = "610a63df-9b57.4856_97ac$665f+56e9c04;timeout=60"; + public void parseSessionHeader_usingDefaultTimeout_succeeds() throws Exception { + String sessionHeaderString = "610a63df-9b57.4856_97ac$665f+56e9c04"; RtspMessageUtil.RtspSessionHeader sessionHeader = RtspMessageUtil.parseSessionHeader(sessionHeaderString); - assertThat(sessionHeader.sessionId).isEqualTo("610a63df-9b57.4856_97ac$665f+56e9c04"); - assertThat(sessionHeader.timeoutMs).isEqualTo(60_000); + assertThat(sessionHeader.timeoutMs).isEqualTo(RtspMessageUtil.DEFAULT_RTSP_TIMEOUT_MS); + } + + @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 diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspPlaybackTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspPlaybackTest.java index e351420846..ee69958873 100644 --- a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspPlaybackTest.java +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspPlaybackTest.java @@ -46,7 +46,9 @@ import com.google.common.collect.ImmutableList; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import javax.net.SocketFactory; import org.junit.After; @@ -55,6 +57,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLooper; /** Playback testing for RTSP. */ @Config(sdk = 29) @@ -103,9 +106,12 @@ public final class RtspPlaybackTest { new ResponseProvider( clock, ImmutableList.of(aacRtpPacketStreamDump, mpeg2tsRtpPacketStreamDump), - fakeRtpDataChannel); + fakeRtpDataChannel, + RtspMessageUtil.DEFAULT_RTSP_TIMEOUT_MS, + /* optionsRequestCounter= */ Optional.empty()); rtspServer = new RtspServer(responseProvider); - ExoPlayer player = createExoPlayer(rtspServer.startAndGetPortNumber(), rtpDataChannelFactory); + ExoPlayer player = + createExoPlayer(clock, rtspServer.startAndGetPortNumber(), rtpDataChannelFactory); PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory); player.prepare(); @@ -125,8 +131,13 @@ public final class RtspPlaybackTest { rtspServer = new RtspServer( new ResponseProvider( - clock, ImmutableList.of(mpeg2tsRtpPacketStreamDump), fakeRtpDataChannel)); - ExoPlayer player = createExoPlayer(rtspServer.startAndGetPortNumber(), rtpDataChannelFactory); + clock, + ImmutableList.of(mpeg2tsRtpPacketStreamDump), + fakeRtpDataChannel, + RtspMessageUtil.DEFAULT_RTSP_TIMEOUT_MS, + /* optionsRequestCounter= */ Optional.empty())); + ExoPlayer player = + createExoPlayer(clock, rtspServer.startAndGetPortNumber(), rtpDataChannelFactory); AtomicReference playbackError = new AtomicReference<>(); player.prepare(); @@ -158,7 +169,7 @@ public final class RtspPlaybackTest { new UdpDataSourceRtpDataChannelFactory(DEFAULT_TIMEOUT_MS), rtpTcpDataChannelFactory); rtspServer = new RtspServer(responseProviderSupportingOnlyTcp); ExoPlayer player = - createExoPlayer(rtspServer.startAndGetPortNumber(), forwardingRtpDataChannelFactory); + createExoPlayer(clock, rtspServer.startAndGetPortNumber(), forwardingRtpDataChannelFactory); PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory); player.prepare(); @@ -183,7 +194,8 @@ public final class RtspPlaybackTest { ImmutableList.of(aacRtpPacketStreamDump, mpeg2tsRtpPacketStreamDump), fakeUdpRtpDataChannel); rtspServer = new RtspServer(responseProvider); - ExoPlayer player = createExoPlayer(rtspServer.startAndGetPortNumber(), rtpDataChannelFactory); + ExoPlayer player = + createExoPlayer(clock, rtspServer.startAndGetPortNumber(), rtpDataChannelFactory); AtomicReference playbackError = new AtomicReference<>(); player.prepare(); @@ -220,7 +232,7 @@ public final class RtspPlaybackTest { new ForwardingRtpDataChannelFactory(rtpDataChannelFactory, rtpDataChannelFactory); rtspServer = new RtspServer(responseProviderSupportingOnlyTcp); ExoPlayer player = - createExoPlayer(rtspServer.startAndGetPortNumber(), forwardingRtpDataChannelFactory); + createExoPlayer(clock, rtspServer.startAndGetPortNumber(), forwardingRtpDataChannelFactory); AtomicReference playbackError = new AtomicReference<>(); player.prepare(); @@ -240,8 +252,39 @@ public final class RtspPlaybackTest { 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 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( - int serverRtspPortNumber, RtpDataChannel.Factory rtpDataChannelFactory) { + Clock clock, int serverRtspPortNumber, RtpDataChannel.Factory rtpDataChannelFactory) { ExoPlayer player = new ExoPlayer.Builder(applicationContext, capturingRenderersFactory) .setClock(clock) @@ -260,11 +303,14 @@ public final class RtspPlaybackTest { private static class ResponseProvider implements RtspServer.ResponseProvider { protected static final String SESSION_ID = "00000000"; + private static final String SESSION_TIMEOUT_HEADER_TAG = ";timeout="; protected final Clock clock; - protected final ArrayList dumpsForSetUpTracks; + protected final List dumpsForSetUpTracks = new ArrayList<>(); protected final ImmutableList rtpPacketStreamDumps; private final RtspMessageChannel.InterleavedBinaryDataListener binaryDataListener; + private final long sessionTimeoutMs; + private final Optional optionsRequestCounter; protected RtpPacketTransmitter packetTransmitter; @@ -275,15 +321,21 @@ public final class RtspPlaybackTest { * @param rtpPacketStreamDumps A list of {@link RtpPacketStreamDump}. * @param binaryDataListener A {@link RtspMessageChannel.InterleavedBinaryDataListener} to send * 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, List rtpPacketStreamDumps, - RtspMessageChannel.InterleavedBinaryDataListener binaryDataListener) { + RtspMessageChannel.InterleavedBinaryDataListener binaryDataListener, + long sessionTimeoutMs, + Optional optionsRequestCounter) { this.clock = clock; this.rtpPacketStreamDumps = ImmutableList.copyOf(rtpPacketStreamDumps); this.binaryDataListener = binaryDataListener; - dumpsForSetUpTracks = new ArrayList<>(); + this.sessionTimeoutMs = sessionTimeoutMs; + this.optionsRequestCounter = optionsRequestCounter; } /** Returns a list of the received SETUP requests' corresponding {@link RtpPacketStreamDump}. */ @@ -295,6 +347,7 @@ public final class RtspPlaybackTest { @Override public RtspResponse getOptionsResponse() { + optionsRequestCounter.ifPresent(AtomicInteger::getAndIncrement); return new RtspResponse( /* status= */ 200, new RtspHeaders.Builder() @@ -316,9 +369,15 @@ public final class RtspPlaybackTest { packetTransmitter = new RtpPacketTransmitter(rtpPacketStreamDump, clock); } } - 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 @@ -348,7 +407,12 @@ public final class RtspPlaybackTest { Clock clock, List rtpPacketStreamDumps, RtspMessageChannel.InterleavedBinaryDataListener binaryDataListener) { - super(clock, rtpPacketStreamDumps, binaryDataListener); + super( + clock, + rtpPacketStreamDumps, + binaryDataListener, + RtspMessageUtil.DEFAULT_RTSP_TIMEOUT_MS, + /* optionsRequestCounter= */ Optional.empty()); } @Override