diff --git a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspClient.java b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspClient.java index 665a387466..715212d211 100644 --- a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspClient.java +++ b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspClient.java @@ -40,6 +40,7 @@ import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.util.SparseArray; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; @@ -57,6 +58,9 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; import java.io.Closeable; import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.net.Socket; import java.util.ArrayDeque; import java.util.HashMap; @@ -68,6 +72,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** The RTSP client. */ /* package */ final class RtspClient implements Closeable { + /** + * The RTSP session state (RFC2326, Section A.1). One of {@link #RTSP_STATE_UNINITIALIZED}, {@link + * #RTSP_STATE_INIT}, {@link #RTSP_STATE_READY}, or {@link #RTSP_STATE_PLAYING}. + */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({RTSP_STATE_UNINITIALIZED, RTSP_STATE_INIT, RTSP_STATE_READY, RTSP_STATE_PLAYING}) + public @interface RtspState {} + /** RTSP uninitialized state, the state before sending any SETUP request. */ + public static final int RTSP_STATE_UNINITIALIZED = -1; + /** RTSP initial state, the state after sending SETUP REQUEST. */ + public static final int RTSP_STATE_INIT = 0; + /** RTSP ready state, the state after receiving SETUP, or PAUSE response. */ + public static final int RTSP_STATE_READY = 1; + /** RTSP playing state, the state after receiving PLAY response. */ + 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; @@ -116,6 +137,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Nullable private String sessionId; @Nullable private KeepAliveMonitor keepAliveMonitor; @Nullable private RtspAuthenticationInfo rtspAuthenticationInfo; + @RtspState private int rtspState; private boolean hasUpdatedTimelineAndTracks; private boolean receivedAuthorizationRequest; private long pendingSeekPositionUs; @@ -151,6 +173,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; this.messageChannel = new RtspMessageChannel(new MessageListener()); this.rtspAuthUserInfo = RtspMessageUtil.parseUserInfo(uri); this.pendingSeekPositionUs = C.TIME_UNSET; + this.rtspState = RTSP_STATE_UNINITIALIZED; } /** @@ -171,6 +194,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; messageSender.sendOptionsRequest(uri, sessionId); } + /** Returns the current {@link RtspState RTSP state}. */ + @RtspState + public int getState() { + return rtspState; + } + /** * Triggers RTSP SETUP requests after track selection. * @@ -327,6 +356,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } public void sendSetupRequest(Uri trackUri, String transport, @Nullable String sessionId) { + rtspState = RTSP_STATE_INIT; sendRequest( getRequestWithCommonHeaders( METHOD_SETUP, @@ -336,6 +366,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } public void sendPlayRequest(Uri uri, long offsetMs, String sessionId) { + checkState(rtspState == RTSP_STATE_READY || rtspState == RTSP_STATE_PLAYING); sendRequest( getRequestWithCommonHeaders( METHOD_PLAY, @@ -346,12 +377,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } public void sendTeardownRequest(Uri uri, String sessionId) { + if (rtspState == RTSP_STATE_UNINITIALIZED || rtspState == RTSP_STATE_INIT) { + // No need to perform session teardown before a session is set up, where the state is + // RTSP_STATE_READY or RTSP_STATE_PLAYING. + return; + } + + rtspState = RTSP_STATE_INIT; sendRequest( getRequestWithCommonHeaders( METHOD_TEARDOWN, sessionId, /* additionalHeaders= */ ImmutableMap.of(), uri)); } public void sendPauseRequest(Uri uri, String sessionId) { + checkState(rtspState == RTSP_STATE_PLAYING); sendRequest( getRequestWithCommonHeaders( METHOD_PAUSE, sessionId, /* additionalHeaders= */ ImmutableMap.of(), uri)); @@ -487,6 +526,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; case 301: case 302: // Redirection request. + if (rtspState != RTSP_STATE_UNINITIALIZED) { + rtspState = RTSP_STATE_INIT; + } @Nullable String redirectionUriString = response.headers.get(RtspHeaders.LOCATION); if (redirectionUriString == null) { sessionInfoListener.onSessionTimelineRequestFailed( @@ -627,11 +669,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } private void onSetupResponseReceived(RtspSetupResponse response) { + checkState(rtspState != RTSP_STATE_UNINITIALIZED); + + rtspState = RTSP_STATE_READY; sessionId = response.sessionHeader.sessionId; continueSetupRtspTrack(); } private void onPlayResponseReceived(RtspPlayResponse response) { + checkState(rtspState == RTSP_STATE_READY); + + rtspState = RTSP_STATE_PLAYING; if (keepAliveMonitor == null) { keepAliveMonitor = new KeepAliveMonitor(DEFAULT_RTSP_KEEP_ALIVE_INTERVAL_MS); keepAliveMonitor.start(); @@ -643,6 +691,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } private void onPauseResponseReceived() { + checkState(rtspState == RTSP_STATE_PLAYING); + + rtspState = RTSP_STATE_READY; if (pendingSeekPositionUs != C.TIME_UNSET) { startPlayback(C.usToMs(pendingSeekPositionUs)); } diff --git a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspClientTest.java b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspClientTest.java index 2ab0154b63..0018c8a9e4 100644 --- a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspClientTest.java +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspClientTest.java @@ -118,6 +118,7 @@ public final class RtspClientTest { RobolectricUtil.runMainLooperUntil(() -> tracksInSession.get() != null); assertThat(tracksInSession.get()).hasSize(2); + assertThat(rtspClient.getState()).isEqualTo(RtspClient.RTSP_STATE_UNINITIALIZED); } @Test @@ -168,6 +169,7 @@ public final class RtspClientTest { RobolectricUtil.runMainLooperUntil(() -> tracksInSession.get() != null); assertThat(tracksInSession.get()).hasSize(2); + assertThat(rtspClient.getState()).isEqualTo(RtspClient.RTSP_STATE_UNINITIALIZED); } @Test @@ -210,6 +212,7 @@ public final class RtspClientTest { RobolectricUtil.runMainLooperUntil(() -> tracksInSession.get() != null); assertThat(tracksInSession.get()).hasSize(2); + assertThat(rtspClient.getState()).isEqualTo(RtspClient.RTSP_STATE_UNINITIALIZED); } @Test @@ -256,6 +259,7 @@ public final class RtspClientTest { assertThat(failureMessage.get()).contains("DESCRIBE not supported."); assertThat(clientHasSentDescribeRequest.get()).isFalse(); + assertThat(rtspClient.getState()).isEqualTo(RtspClient.RTSP_STATE_UNINITIALIZED); } @Test @@ -300,5 +304,6 @@ public final class RtspClientTest { RobolectricUtil.runMainLooperUntil(() -> failureCause.get() != null); assertThat(failureCause.get()).hasCauseThat().isInstanceOf(ParserException.class); + assertThat(rtspClient.getState()).isEqualTo(RtspClient.RTSP_STATE_UNINITIALIZED); } }