Add RTSP state machine.

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

PiperOrigin-RevId: 396799104
This commit is contained in:
claincly 2021-09-15 11:34:20 +01:00 committed by Christos Tsilopoulos
parent 416ec75b94
commit 5f0395ee2d
2 changed files with 56 additions and 0 deletions

View file

@ -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));
}

View file

@ -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);
}
}