Commit 5f0395ee by claincly Committed by Christos Tsilopoulos

Add RTSP state machine.

Please reference RFC2326 Section A.1 for the state transitions.

PiperOrigin-RevId: 396799104
parent 416ec75b
...@@ -40,6 +40,7 @@ import android.net.Uri; ...@@ -40,6 +40,7 @@ import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.SparseArray; import android.util.SparseArray;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
...@@ -57,6 +58,9 @@ import com.google.common.collect.Iterables; ...@@ -57,6 +58,9 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; 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.net.Socket;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.HashMap; import java.util.HashMap;
...@@ -68,6 +72,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -68,6 +72,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** The RTSP client. */ /** The RTSP client. */
/* package */ final class RtspClient implements Closeable { /* 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 String TAG = "RtspClient";
private static final long DEFAULT_RTSP_KEEP_ALIVE_INTERVAL_MS = 30_000; private static final long DEFAULT_RTSP_KEEP_ALIVE_INTERVAL_MS = 30_000;
...@@ -116,6 +137,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -116,6 +137,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Nullable private String sessionId; @Nullable private String sessionId;
@Nullable private KeepAliveMonitor keepAliveMonitor; @Nullable private KeepAliveMonitor keepAliveMonitor;
@Nullable private RtspAuthenticationInfo rtspAuthenticationInfo; @Nullable private RtspAuthenticationInfo rtspAuthenticationInfo;
@RtspState private int rtspState;
private boolean hasUpdatedTimelineAndTracks; private boolean hasUpdatedTimelineAndTracks;
private boolean receivedAuthorizationRequest; private boolean receivedAuthorizationRequest;
private long pendingSeekPositionUs; private long pendingSeekPositionUs;
...@@ -151,6 +173,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -151,6 +173,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.messageChannel = new RtspMessageChannel(new MessageListener()); this.messageChannel = new RtspMessageChannel(new MessageListener());
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;
} }
/** /**
...@@ -171,6 +194,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -171,6 +194,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
messageSender.sendOptionsRequest(uri, sessionId); messageSender.sendOptionsRequest(uri, sessionId);
} }
/** Returns the current {@link RtspState RTSP state}. */
@RtspState
public int getState() {
return rtspState;
}
/** /**
* Triggers RTSP SETUP requests after track selection. * Triggers RTSP SETUP requests after track selection.
* *
...@@ -327,6 +356,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -327,6 +356,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
public void sendSetupRequest(Uri trackUri, String transport, @Nullable String sessionId) { public void sendSetupRequest(Uri trackUri, String transport, @Nullable String sessionId) {
rtspState = RTSP_STATE_INIT;
sendRequest( sendRequest(
getRequestWithCommonHeaders( getRequestWithCommonHeaders(
METHOD_SETUP, METHOD_SETUP,
...@@ -336,6 +366,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -336,6 +366,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
public void sendPlayRequest(Uri uri, long offsetMs, String sessionId) { public void sendPlayRequest(Uri uri, long offsetMs, String sessionId) {
checkState(rtspState == RTSP_STATE_READY || rtspState == RTSP_STATE_PLAYING);
sendRequest( sendRequest(
getRequestWithCommonHeaders( getRequestWithCommonHeaders(
METHOD_PLAY, METHOD_PLAY,
...@@ -346,12 +377,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -346,12 +377,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
public void sendTeardownRequest(Uri uri, String sessionId) { 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( sendRequest(
getRequestWithCommonHeaders( getRequestWithCommonHeaders(
METHOD_TEARDOWN, sessionId, /* additionalHeaders= */ ImmutableMap.of(), uri)); METHOD_TEARDOWN, sessionId, /* additionalHeaders= */ ImmutableMap.of(), uri));
} }
public void sendPauseRequest(Uri uri, String sessionId) { public void sendPauseRequest(Uri uri, String sessionId) {
checkState(rtspState == RTSP_STATE_PLAYING);
sendRequest( sendRequest(
getRequestWithCommonHeaders( getRequestWithCommonHeaders(
METHOD_PAUSE, sessionId, /* additionalHeaders= */ ImmutableMap.of(), uri)); METHOD_PAUSE, sessionId, /* additionalHeaders= */ ImmutableMap.of(), uri));
...@@ -487,6 +526,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -487,6 +526,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
case 301: case 301:
case 302: case 302:
// Redirection request. // Redirection request.
if (rtspState != RTSP_STATE_UNINITIALIZED) {
rtspState = RTSP_STATE_INIT;
}
@Nullable String redirectionUriString = response.headers.get(RtspHeaders.LOCATION); @Nullable String redirectionUriString = response.headers.get(RtspHeaders.LOCATION);
if (redirectionUriString == null) { if (redirectionUriString == null) {
sessionInfoListener.onSessionTimelineRequestFailed( sessionInfoListener.onSessionTimelineRequestFailed(
...@@ -627,11 +669,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -627,11 +669,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
private void onSetupResponseReceived(RtspSetupResponse response) { private void onSetupResponseReceived(RtspSetupResponse response) {
checkState(rtspState != RTSP_STATE_UNINITIALIZED);
rtspState = RTSP_STATE_READY;
sessionId = response.sessionHeader.sessionId; sessionId = response.sessionHeader.sessionId;
continueSetupRtspTrack(); continueSetupRtspTrack();
} }
private void onPlayResponseReceived(RtspPlayResponse response) { private void onPlayResponseReceived(RtspPlayResponse response) {
checkState(rtspState == RTSP_STATE_READY);
rtspState = RTSP_STATE_PLAYING;
if (keepAliveMonitor == null) { if (keepAliveMonitor == null) {
keepAliveMonitor = new KeepAliveMonitor(DEFAULT_RTSP_KEEP_ALIVE_INTERVAL_MS); keepAliveMonitor = new KeepAliveMonitor(DEFAULT_RTSP_KEEP_ALIVE_INTERVAL_MS);
keepAliveMonitor.start(); keepAliveMonitor.start();
...@@ -643,6 +691,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -643,6 +691,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
private void onPauseResponseReceived() { private void onPauseResponseReceived() {
checkState(rtspState == RTSP_STATE_PLAYING);
rtspState = RTSP_STATE_READY;
if (pendingSeekPositionUs != C.TIME_UNSET) { if (pendingSeekPositionUs != C.TIME_UNSET) {
startPlayback(C.usToMs(pendingSeekPositionUs)); startPlayback(C.usToMs(pendingSeekPositionUs));
} }
......
...@@ -118,6 +118,7 @@ public final class RtspClientTest { ...@@ -118,6 +118,7 @@ public final class RtspClientTest {
RobolectricUtil.runMainLooperUntil(() -> tracksInSession.get() != null); RobolectricUtil.runMainLooperUntil(() -> tracksInSession.get() != null);
assertThat(tracksInSession.get()).hasSize(2); assertThat(tracksInSession.get()).hasSize(2);
assertThat(rtspClient.getState()).isEqualTo(RtspClient.RTSP_STATE_UNINITIALIZED);
} }
@Test @Test
...@@ -168,6 +169,7 @@ public final class RtspClientTest { ...@@ -168,6 +169,7 @@ public final class RtspClientTest {
RobolectricUtil.runMainLooperUntil(() -> tracksInSession.get() != null); RobolectricUtil.runMainLooperUntil(() -> tracksInSession.get() != null);
assertThat(tracksInSession.get()).hasSize(2); assertThat(tracksInSession.get()).hasSize(2);
assertThat(rtspClient.getState()).isEqualTo(RtspClient.RTSP_STATE_UNINITIALIZED);
} }
@Test @Test
...@@ -210,6 +212,7 @@ public final class RtspClientTest { ...@@ -210,6 +212,7 @@ public final class RtspClientTest {
RobolectricUtil.runMainLooperUntil(() -> tracksInSession.get() != null); RobolectricUtil.runMainLooperUntil(() -> tracksInSession.get() != null);
assertThat(tracksInSession.get()).hasSize(2); assertThat(tracksInSession.get()).hasSize(2);
assertThat(rtspClient.getState()).isEqualTo(RtspClient.RTSP_STATE_UNINITIALIZED);
} }
@Test @Test
...@@ -256,6 +259,7 @@ public final class RtspClientTest { ...@@ -256,6 +259,7 @@ public final class RtspClientTest {
assertThat(failureMessage.get()).contains("DESCRIBE not supported."); assertThat(failureMessage.get()).contains("DESCRIBE not supported.");
assertThat(clientHasSentDescribeRequest.get()).isFalse(); assertThat(clientHasSentDescribeRequest.get()).isFalse();
assertThat(rtspClient.getState()).isEqualTo(RtspClient.RTSP_STATE_UNINITIALIZED);
} }
@Test @Test
...@@ -300,5 +304,6 @@ public final class RtspClientTest { ...@@ -300,5 +304,6 @@ public final class RtspClientTest {
RobolectricUtil.runMainLooperUntil(() -> failureCause.get() != null); RobolectricUtil.runMainLooperUntil(() -> failureCause.get() != null);
assertThat(failureCause.get()).hasCauseThat().isInstanceOf(ParserException.class); assertThat(failureCause.get()).hasCauseThat().isInstanceOf(ParserException.class);
assertThat(rtspClient.getState()).isEqualTo(RtspClient.RTSP_STATE_UNINITIALIZED);
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment