Retry RTSP Setup with TCP if response with UDP is UnsupportedTransport

If RTSP Setup Request with UDP receives HTTP Error Status 461 UnsupportedTransport, then client will retry with TCP.

Issue: google/ExoPlayer#11069
PiperOrigin-RevId: 518807829
This commit is contained in:
michaelkatz 2023-03-23 09:54:54 +00:00 committed by Tianyi Feng
parent df558b59db
commit ecf4d8b891
6 changed files with 346 additions and 66 deletions

View File

@ -42,6 +42,10 @@
* DASH:
* Fix handling of empty segment timelines
([#11014](https://github.com/google/ExoPlayer/issues/11014)).
* RTSP:
* Retry with TCP if RTSP Setup with UDP fails with RTSP Error 461
UnsupportedTransport
([#11069](https://github.com/google/ExoPlayer/issues/11069)).
* IMA DAI extension:
* Fix a bug where a new ad group is inserted in live streams because the
calculated content position in consecutive timelines varies slightly.

View File

@ -49,6 +49,7 @@ import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.rtsp.RtspMediaPeriod.RtpLoadInfo;
import androidx.media3.exoplayer.rtsp.RtspMediaSource.RtspPlaybackException;
import androidx.media3.exoplayer.rtsp.RtspMediaSource.RtspUdpUnsupportedTransportException;
import androidx.media3.exoplayer.rtsp.RtspMessageChannel.InterleavedBinaryDataListener;
import androidx.media3.exoplayer.rtsp.RtspMessageUtil.RtspAuthUserInfo;
import androidx.media3.exoplayer.rtsp.RtspMessageUtil.RtspSessionHeader;
@ -577,8 +578,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
receivedAuthorizationRequest = true;
return;
}
// fall through: if unauthorized and no userInfo present, or previous authentication
// unsuccessful.
// if unauthorized and no userInfo present, or previous authentication
// unsuccessful, then dispatch RtspPlaybackException
dispatchRtspError(
new RtspPlaybackException(
RtspMessageUtil.toMethodString(requestMethod) + " " + response.status));
return;
case 461:
String exceptionMessage =
RtspMessageUtil.toMethodString(requestMethod) + " " + response.status;
// If request was SETUP with UDP transport protocol, then throw
// RtspUdpUnsupportedTransportException.
String transportHeaderValue =
checkNotNull(matchingRequest.headers.get(RtspHeaders.TRANSPORT));
dispatchRtspError(
requestMethod == METHOD_SETUP && !transportHeaderValue.contains("TCP")
? new RtspUdpUnsupportedTransportException(exceptionMessage)
: new RtspPlaybackException(exceptionMessage));
return;
default:
dispatchRtspError(
new RtspPlaybackException(

View File

@ -518,7 +518,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// using TCP. Retrying will setup new loadables, so will not retry with the current
// loadables.
retryWithRtpTcp();
isUsingRtpTcp = true;
}
return;
}
@ -644,7 +643,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override
public void onPlaybackError(RtspPlaybackException error) {
playbackException = error;
if (error instanceof RtspMediaSource.RtspUdpUnsupportedTransportException && !isUsingRtpTcp) {
// Retry playback with TCP if we receive RtspUdpUnsupportedTransportException, and we are
// not already using TCP. Retrying will setup new loadables.
retryWithRtpTcp();
} else {
playbackException = error;
}
}
@Override
@ -668,6 +673,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
private void retryWithRtpTcp() {
// Retry should only run once.
isUsingRtpTcp = true;
rtspClient.retryWithRtpTcp();
@Nullable

View File

@ -192,7 +192,7 @@ public final class RtspMediaSource extends BaseMediaSource {
}
/** Thrown when an exception or error is encountered during loading an RTSP stream. */
public static final class RtspPlaybackException extends IOException {
public static class RtspPlaybackException extends IOException {
public RtspPlaybackException(String message) {
super(message);
}
@ -206,6 +206,13 @@ public final class RtspMediaSource extends BaseMediaSource {
}
}
/** Thrown when an RTSP Unsupported Transport error (461) is encountered during RTSP Setup. */
public static final class RtspUdpUnsupportedTransportException extends RtspPlaybackException {
public RtspUdpUnsupportedTransportException(String message) {
super(message);
}
}
private final MediaItem mediaItem;
private final RtpDataChannel.Factory rtpDataChannelFactory;
private final String userAgent;

View File

@ -453,4 +453,77 @@ public final class RtspClientTest {
RobolectricUtil.runMainLooperUntil(timelineRequestFailed::get);
assertThat(rtspClient.getState()).isEqualTo(RtspClient.RTSP_STATE_UNINITIALIZED);
}
@Test
public void connectServerAndClient_describeResponseRequiresAuthentication_doesNotUpdateTimeline()
throws Exception {
class ResponseProvider implements RtspServer.ResponseProvider {
@Override
public RtspResponse getOptionsResponse() {
return new RtspResponse(
/* status= */ 200,
new RtspHeaders.Builder().add(RtspHeaders.PUBLIC, "OPTIONS, DESCRIBE").build());
}
@Override
public RtspResponse getDescribeResponse(Uri requestedUri, RtspHeaders headers) {
String authorizationHeader = headers.get(RtspHeaders.AUTHORIZATION);
if (authorizationHeader == null) {
return new RtspResponse(
/* status= */ 401,
new RtspHeaders.Builder()
.add(RtspHeaders.CSEQ, headers.get(RtspHeaders.CSEQ))
.add(
RtspHeaders.WWW_AUTHENTICATE,
"Digest realm=\"RTSP server\","
+ " nonce=\"0cdfe9719e7373b7d5bb2913e2115f3f\","
+ " opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"")
.add(RtspHeaders.WWW_AUTHENTICATE, "BASIC realm=\"WallyWorld\"")
.build());
}
if (!authorizationHeader.contains("Digest")) {
return new RtspResponse(
401,
new RtspHeaders.Builder()
.add(RtspHeaders.CSEQ, headers.get(RtspHeaders.CSEQ))
.build());
}
return RtspTestUtils.newDescribeResponseWithSdpMessage(
"v=0\r\n"
+ "o=- 1606776316530225 1 IN IP4 127.0.0.1\r\n"
+ "s=Exoplayer test\r\n"
+ "t=0 0\r\n"
// The session is 50.46s long.
+ "a=range:npt=0-50.46\r\n",
rtpPacketStreamDumps,
requestedUri);
}
}
rtspServer = new RtspServer(new ResponseProvider());
AtomicBoolean timelineRequestFailed = new AtomicBoolean();
rtspClient =
new RtspClient(
new SessionInfoListener() {
@Override
public void onSessionTimelineUpdated(
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks) {}
@Override
public void onSessionTimelineRequestFailed(
String message, @Nullable Throwable cause) {
timelineRequestFailed.set(true);
}
},
EMPTY_PLAYBACK_LISTENER,
/* userAgent= */ "ExoPlayer:RtspClientTest",
RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber()),
SocketFactory.getDefault(),
/* debugLoggingEnabled= */ false);
rtspClient.start();
RobolectricUtil.runMainLooperUntil(timelineRequestFailed::get);
assertThat(rtspClient.getState()).isEqualTo(RtspClient.RTSP_STATE_UNINITIALIZED);
}
}

View File

@ -15,6 +15,7 @@
*/
package androidx.media3.exoplayer.rtsp;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static com.google.common.truth.Truth.assertThat;
import static java.lang.Math.min;
@ -42,11 +43,13 @@ import androidx.media3.test.utils.robolectric.TestPlayerRunHelper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicReference;
import javax.net.SocketFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@ -58,30 +61,20 @@ import org.robolectric.annotation.Config;
@RunWith(AndroidJUnit4.class)
public final class RtspPlaybackTest {
private static final long DEFAULT_TIMEOUT_MS = 8000;
private static final String SESSION_DESCRIPTION =
"v=0\r\n"
+ "o=- 1606776316530225 1 IN IP4 127.0.0.1\r\n"
+ "s=Exoplayer test\r\n"
+ "t=0 0\r\n";
private final Context applicationContext;
private final CapturingRenderersFactory capturingRenderersFactory;
private final Clock clock;
private final FakeUdpDataSourceRtpDataChannel fakeRtpDataChannel;
private final RtpDataChannel.Factory rtpDataChannelFactory;
private Context applicationContext;
private CapturingRenderersFactory capturingRenderersFactory;
private Clock clock;
private RtpPacketStreamDump aacRtpPacketStreamDump;
// ExoPlayer does not support extracting MP4A-LATM RTP payload at the moment.
private RtpPacketStreamDump mpeg2tsRtpPacketStreamDump;
/** Creates a new instance. */
public RtspPlaybackTest() {
applicationContext = ApplicationProvider.getApplicationContext();
capturingRenderersFactory = new CapturingRenderersFactory(applicationContext);
clock = new FakeClock(/* isAutoAdvancing= */ true);
fakeRtpDataChannel = new FakeUdpDataSourceRtpDataChannel();
rtpDataChannelFactory = (trackId) -> fakeRtpDataChannel;
}
private RtspServer rtspServer;
@Rule
public ShadowMediaCodecConfig mediaCodecConfig =
@ -89,61 +82,162 @@ public final class RtspPlaybackTest {
@Before
public void setUp() throws Exception {
applicationContext = ApplicationProvider.getApplicationContext();
capturingRenderersFactory = new CapturingRenderersFactory(applicationContext);
clock = new FakeClock(/* isAutoAdvancing= */ true);
aacRtpPacketStreamDump = RtspTestUtils.readRtpPacketStreamDump("media/rtsp/aac-dump.json");
mpeg2tsRtpPacketStreamDump =
RtspTestUtils.readRtpPacketStreamDump("media/rtsp/mpeg2ts-dump.json");
}
@After
public void tearDown() {
Util.closeQuietly(rtspServer);
}
@Test
public void prepare_withSupportedTrack_playsTrackUntilEnded() throws Exception {
FakeUdpDataSourceRtpDataChannel fakeRtpDataChannel = new FakeUdpDataSourceRtpDataChannel();
RtpDataChannel.Factory rtpDataChannelFactory = (trackId) -> fakeRtpDataChannel;
ResponseProvider responseProvider =
new ResponseProvider(
clock,
ImmutableList.of(aacRtpPacketStreamDump, mpeg2tsRtpPacketStreamDump),
fakeRtpDataChannel);
rtspServer = new RtspServer(responseProvider);
ExoPlayer player = createExoPlayer(rtspServer.startAndGetPortNumber(), rtpDataChannelFactory);
try (RtspServer rtspServer = new RtspServer(responseProvider)) {
ExoPlayer player = createExoPlayer(rtspServer.startAndGetPortNumber(), rtpDataChannelFactory);
PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory);
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory);
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
// Only setup the supported track (aac).
assertThat(responseProvider.getDumpsForSetUpTracks()).containsExactly(aacRtpPacketStreamDump);
DumpFileAsserts.assertOutput(
applicationContext, playbackOutput, "playbackdumps/rtsp/aac.dump");
}
// Only setup the supported track (aac).
assertThat(responseProvider.getDumpsForSetUpTracks()).containsExactly(aacRtpPacketStreamDump);
DumpFileAsserts.assertOutput(applicationContext, playbackOutput, "playbackdumps/rtsp/aac.dump");
}
@Test
public void prepare_noSupportedTrack_throwsPreparationError() throws Exception {
try (RtspServer rtspServer =
FakeUdpDataSourceRtpDataChannel fakeRtpDataChannel = new FakeUdpDataSourceRtpDataChannel();
RtpDataChannel.Factory rtpDataChannelFactory = (trackId) -> fakeRtpDataChannel;
rtspServer =
new RtspServer(
new ResponseProvider(
clock, ImmutableList.of(mpeg2tsRtpPacketStreamDump), fakeRtpDataChannel))) {
ExoPlayer player = createExoPlayer(rtspServer.startAndGetPortNumber(), rtpDataChannelFactory);
clock, ImmutableList.of(mpeg2tsRtpPacketStreamDump), fakeRtpDataChannel));
ExoPlayer player = createExoPlayer(rtspServer.startAndGetPortNumber(), rtpDataChannelFactory);
AtomicReference<Throwable> playbackError = new AtomicReference<>();
player.prepare();
player.addListener(
new Listener() {
@Override
public void onPlayerError(PlaybackException error) {
playbackError.set(error);
}
});
RobolectricUtil.runMainLooperUntil(() -> playbackError.get() != null);
player.release();
AtomicReference<Throwable> playbackError = new AtomicReference<>();
player.prepare();
player.addListener(
new Listener() {
@Override
public void onPlayerError(PlaybackException error) {
playbackError.set(error);
}
});
RobolectricUtil.runMainLooperUntil(() -> playbackError.get() != null);
player.release();
assertThat(playbackError.get())
.hasCauseThat()
.hasMessageThat()
.contains("No playable track.");
}
assertThat(playbackError.get()).hasCauseThat().hasMessageThat().contains("No playable track.");
}
@Test
public void prepare_withUdpUnsupportedWithFallback_fallsbackToTcpAndPlaysUntilEnd()
throws Exception {
FakeTcpDataSourceRtpDataChannel fakeTcpRtpDataChannel = new FakeTcpDataSourceRtpDataChannel();
RtpDataChannel.Factory rtpTcpDataChannelFactory = (trackId) -> fakeTcpRtpDataChannel;
ResponseProviderSupportingOnlyTcp responseProviderSupportingOnlyTcp =
new ResponseProviderSupportingOnlyTcp(
clock,
ImmutableList.of(aacRtpPacketStreamDump, mpeg2tsRtpPacketStreamDump),
fakeTcpRtpDataChannel);
ForwardingRtpDataChannelFactory forwardingRtpDataChannelFactory =
new ForwardingRtpDataChannelFactory(
new UdpDataSourceRtpDataChannelFactory(DEFAULT_TIMEOUT_MS), rtpTcpDataChannelFactory);
rtspServer = new RtspServer(responseProviderSupportingOnlyTcp);
ExoPlayer player =
createExoPlayer(rtspServer.startAndGetPortNumber(), forwardingRtpDataChannelFactory);
PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory);
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
// Only setup the supported track (aac).
assertThat(responseProviderSupportingOnlyTcp.getDumpsForSetUpTracks())
.containsExactly(aacRtpPacketStreamDump);
DumpFileAsserts.assertOutput(applicationContext, playbackOutput, "playbackdumps/rtsp/aac.dump");
}
@Test
public void prepare_withUdpUnsupportedWithoutFallback_throwsRtspPlaybackException()
throws Exception {
FakeUdpDataSourceRtpDataChannel fakeUdpRtpDataChannel = new FakeUdpDataSourceRtpDataChannel();
RtpDataChannel.Factory rtpDataChannelFactory = (trackId) -> fakeUdpRtpDataChannel;
ResponseProviderSupportingOnlyTcp responseProvider =
new ResponseProviderSupportingOnlyTcp(
clock,
ImmutableList.of(aacRtpPacketStreamDump, mpeg2tsRtpPacketStreamDump),
fakeUdpRtpDataChannel);
rtspServer = new RtspServer(responseProvider);
ExoPlayer player = createExoPlayer(rtspServer.startAndGetPortNumber(), rtpDataChannelFactory);
AtomicReference<PlaybackException> playbackError = new AtomicReference<>();
player.prepare();
player.addListener(
new Listener() {
@Override
public void onPlayerError(PlaybackException error) {
playbackError.set(error);
}
});
RobolectricUtil.runMainLooperUntil(() -> playbackError.get() != null);
player.release();
assertThat(playbackError.get())
.hasCauseThat()
.isInstanceOf(RtspMediaSource.RtspPlaybackException.class);
assertThat(playbackError.get())
.hasCauseThat()
.hasMessageThat()
.contains("No fallback data channel factory for TCP retry");
}
@Test
public void prepare_withUdpUnsupportedWithUdpFallback_throwsRtspUdpUnsupportedTransportException()
throws Exception {
FakeUdpDataSourceRtpDataChannel fakeUdpRtpDataChannel = new FakeUdpDataSourceRtpDataChannel();
RtpDataChannel.Factory rtpDataChannelFactory = (trackId) -> fakeUdpRtpDataChannel;
ResponseProviderSupportingOnlyTcp responseProviderSupportingOnlyTcp =
new ResponseProviderSupportingOnlyTcp(
clock,
ImmutableList.of(aacRtpPacketStreamDump, mpeg2tsRtpPacketStreamDump),
fakeUdpRtpDataChannel);
ForwardingRtpDataChannelFactory forwardingRtpDataChannelFactory =
new ForwardingRtpDataChannelFactory(rtpDataChannelFactory, rtpDataChannelFactory);
rtspServer = new RtspServer(responseProviderSupportingOnlyTcp);
ExoPlayer player =
createExoPlayer(rtspServer.startAndGetPortNumber(), forwardingRtpDataChannelFactory);
AtomicReference<PlaybackException> playbackError = new AtomicReference<>();
player.prepare();
player.addListener(
new Listener() {
@Override
public void onPlayerError(PlaybackException error) {
playbackError.set(error);
}
});
RobolectricUtil.runMainLooperUntil(() -> playbackError.get() != null);
player.release();
assertThat(playbackError.get())
.hasCauseThat()
.isInstanceOf(RtspMediaSource.RtspUdpUnsupportedTransportException.class);
assertThat(playbackError.get()).hasCauseThat().hasMessageThat().isEqualTo("SETUP 461");
}
private ExoPlayer createExoPlayer(
@ -163,16 +257,16 @@ public final class RtspPlaybackTest {
return player;
}
private static final class ResponseProvider implements RtspServer.ResponseProvider {
private static class ResponseProvider implements RtspServer.ResponseProvider {
private static final String SESSION_ID = "00000000";
protected static final String SESSION_ID = "00000000";
private final Clock clock;
private final ArrayList<RtpPacketStreamDump> dumpsForSetUpTracks;
private final ImmutableList<RtpPacketStreamDump> rtpPacketStreamDumps;
protected final Clock clock;
protected final ArrayList<RtpPacketStreamDump> dumpsForSetUpTracks;
protected final ImmutableList<RtpPacketStreamDump> rtpPacketStreamDumps;
private final RtspMessageChannel.InterleavedBinaryDataListener binaryDataListener;
private RtpPacketTransmitter packetTransmitter;
protected RtpPacketTransmitter packetTransmitter;
/**
* Creates a new instance.
@ -240,22 +334,54 @@ public final class RtspPlaybackTest {
}
}
private static final class FakeUdpDataSourceRtpDataChannel extends BaseDataSource
implements RtpDataChannel, RtspMessageChannel.InterleavedBinaryDataListener {
private static final class ResponseProviderSupportingOnlyTcp extends ResponseProvider {
private static final int LOCAL_PORT = 40000;
/**
* Creates a new instance.
*
* @param clock The {@link Clock} used in the test.
* @param rtpPacketStreamDumps A list of {@link RtpPacketStreamDump}.
* @param binaryDataListener A {@link RtspMessageChannel.InterleavedBinaryDataListener} to send
* RTP data.
*/
public ResponseProviderSupportingOnlyTcp(
Clock clock,
List<RtpPacketStreamDump> rtpPacketStreamDumps,
RtspMessageChannel.InterleavedBinaryDataListener binaryDataListener) {
super(clock, rtpPacketStreamDumps, binaryDataListener);
}
@Override
public RtspResponse getSetupResponse(Uri requestedUri, RtspHeaders headers) {
String transportHeaderValue = checkNotNull(headers.get(RtspHeaders.TRANSPORT));
if (!transportHeaderValue.contains("TCP")) {
return new RtspResponse(
/* status= */ 461, headers.buildUpon().add(RtspHeaders.SESSION, SESSION_ID).build());
}
for (RtpPacketStreamDump rtpPacketStreamDump : rtpPacketStreamDumps) {
if (requestedUri.toString().contains(rtpPacketStreamDump.trackName)) {
dumpsForSetUpTracks.add(rtpPacketStreamDump);
packetTransmitter = new RtpPacketTransmitter(rtpPacketStreamDump, clock);
}
}
return new RtspResponse(
/* status= */ 200, headers.buildUpon().add(RtspHeaders.SESSION, SESSION_ID).build());
}
}
private abstract static class FakeBaseDataSourceRtpDataChannel extends BaseDataSource
implements RtpDataChannel, RtspMessageChannel.InterleavedBinaryDataListener {
protected static final int LOCAL_PORT = 40000;
private final ConcurrentLinkedQueue<byte[]> packetQueue;
public FakeUdpDataSourceRtpDataChannel() {
public FakeBaseDataSourceRtpDataChannel() {
super(/* isNetwork= */ false);
packetQueue = new ConcurrentLinkedQueue<>();
}
@Override
public String getTransport() {
return Util.formatInvariant("RTP/AVP;unicast;client_port=%d-%d", LOCAL_PORT, LOCAL_PORT + 1);
}
public abstract String getTransport();
@Override
public int getLocalPort() {
@ -307,4 +433,49 @@ public final class RtspPlaybackTest {
return byteToRead;
}
}
private static final class FakeUdpDataSourceRtpDataChannel
extends FakeBaseDataSourceRtpDataChannel {
@Override
public String getTransport() {
return Util.formatInvariant("RTP/AVP;unicast;client_port=%d-%d", LOCAL_PORT, LOCAL_PORT + 1);
}
@Override
public RtspMessageChannel.InterleavedBinaryDataListener getInterleavedBinaryDataListener() {
return null;
}
}
private static final class FakeTcpDataSourceRtpDataChannel
extends FakeBaseDataSourceRtpDataChannel {
@Override
public String getTransport() {
return Util.formatInvariant(
"RTP/AVP/TCP;unicast;interleaved=%d-%d", LOCAL_PORT + 2, LOCAL_PORT + 3);
}
}
private static class ForwardingRtpDataChannelFactory implements RtpDataChannel.Factory {
private final RtpDataChannel.Factory rtpChannelFactory;
private final RtpDataChannel.Factory rtpFallbackChannelFactory;
public ForwardingRtpDataChannelFactory(
RtpDataChannel.Factory rtpChannelFactory,
RtpDataChannel.Factory rtpFallbackChannelFactory) {
this.rtpChannelFactory = rtpChannelFactory;
this.rtpFallbackChannelFactory = rtpFallbackChannelFactory;
}
@Override
public RtpDataChannel createAndOpenDataChannel(int trackId) throws IOException {
return rtpChannelFactory.createAndOpenDataChannel(trackId);
}
@Override
public RtpDataChannel.Factory createFallbackDataChannelFactory() {
return rtpFallbackChannelFactory;
}
}
}