diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 787bd91eb5..e1b1b51667 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -38,6 +38,9 @@ * MediaSession extension * Remove deprecated call to `onStop(/* reset= */ true)` and provide an opt-out flag for apps that don't want to clear the playlist on stop. +* RTSP + * Provide a client API to override the `SocketFactory` used for any server + connection ([#9606](https://github.com/google/ExoPlayer/pull/9606)). ### 2.16.1 (2021-11-18) diff --git a/docs/rtsp.md b/docs/rtsp.md index 9c4cd38753..fea28f34a0 100644 --- a/docs/rtsp.md +++ b/docs/rtsp.md @@ -75,3 +75,24 @@ end-of-stream signal under poor network conditions. RTP/TCP offers better compatibility under some network setups. You can configure ExoPlayer to use RTP/TCP by default with `RtspMediaSource.Factory.setForceUseRtpTcp()`. + +### Passing a custom SocketFactory +Custom `SocketFactory` instances can be useful when particular routing is +required (e.g. when RTSP traffic needs to pass a specific interface, or the +socket needs additional connectivity flags). + +By default, `RtspMediaSource` will use Java's standard socket factory +(`SocketFactory.getDefault()`) to create connections to the remote endpoints. +This behavior can be overridden using +`RtspMediaSource.Factory.setSocketFactory()`. + +~~~ +// Create an RTSP media source pointing to an RTSP uri and override the socket +// factory. +MediaSource mediaSource = + new RtspMediaSource.Factory() + .setSocketFactory(...) + .createMediaSource(MediaItem.fromUri(rtspUri)); +~~~ +{: .language-java} + 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 a208ea6f30..b2c2f4653b 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 @@ -123,6 +123,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final SessionInfoListener sessionInfoListener; private final PlaybackEventListener playbackEventListener; private final String userAgent; + private final SocketFactory socketFactory; private final boolean debugLoggingEnabled; private final ArrayDeque pendingSetupRtpLoadInfos; // TODO(b/172331505) Add a timeout monitor for pending requests. @@ -155,16 +156,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * @param playbackEventListener The {@link PlaybackEventListener}. * @param userAgent The user agent. * @param uri The RTSP playback URI. + * @param socketFactory A socket factory for the RTSP connection. + * @param debugLoggingEnabled Whether to log RTSP messages. */ public RtspClient( SessionInfoListener sessionInfoListener, PlaybackEventListener playbackEventListener, String userAgent, Uri uri, + SocketFactory socketFactory, boolean debugLoggingEnabled) { this.sessionInfoListener = sessionInfoListener; this.playbackEventListener = playbackEventListener; this.userAgent = userAgent; + this.socketFactory = socketFactory; this.debugLoggingEnabled = debugLoggingEnabled; this.pendingSetupRtpLoadInfos = new ArrayDeque<>(); this.pendingRequests = new SparseArray<>(); @@ -286,10 +291,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } /** Returns a {@link Socket} that is connected to the {@code uri}. */ - private static Socket getSocket(Uri uri) throws IOException { + private Socket getSocket(Uri uri) throws IOException { checkArgument(uri.getHost() != null); int rtspPort = uri.getPort() > 0 ? uri.getPort() : DEFAULT_RTSP_PORT; - return SocketFactory.getDefault().createSocket(checkNotNull(uri.getHost()), rtspPort); + return socketFactory.createSocket(checkNotNull(uri.getHost()), rtspPort); } private void dispatchRtspError(Throwable error) { diff --git a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaPeriod.java b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaPeriod.java index de72e31956..5debe78fb3 100644 --- a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaPeriod.java +++ b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaPeriod.java @@ -55,6 +55,7 @@ import java.io.IOException; import java.net.BindException; import java.util.ArrayList; import java.util.List; +import javax.net.SocketFactory; import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -102,6 +103,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * @param uri The RTSP playback {@link Uri}. * @param listener A {@link Listener} to receive session information updates. * @param userAgent The user agent. + * @param socketFactory A socket factory for {@link RtspClient}'s connection. + * @param debugLoggingEnabled Whether to log RTSP messages. */ public RtspMediaPeriod( Allocator allocator, @@ -109,6 +112,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; Uri uri, Listener listener, String userAgent, + SocketFactory socketFactory, boolean debugLoggingEnabled) { this.allocator = allocator; this.rtpDataChannelFactory = rtpDataChannelFactory; @@ -122,6 +126,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /* playbackEventListener= */ internalListener, /* userAgent= */ userAgent, /* uri= */ uri, + socketFactory, debugLoggingEnabled); rtspLoaderWrappers = new ArrayList<>(); selectedLoadInfos = new ArrayList<>(); diff --git a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaSource.java b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaSource.java index fbd7ab8075..7e9dbab47c 100644 --- a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaSource.java +++ b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspMediaSource.java @@ -41,6 +41,7 @@ import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import javax.net.SocketFactory; /** An Rtsp {@link MediaSource} */ public final class RtspMediaSource extends BaseMediaSource { @@ -69,12 +70,14 @@ public final class RtspMediaSource extends BaseMediaSource { private long timeoutMs; private String userAgent; + private SocketFactory socketFactory; private boolean forceUseRtpTcp; private boolean debugLoggingEnabled; public Factory() { timeoutMs = DEFAULT_TIMEOUT_MS; userAgent = ExoPlayerLibraryInfo.VERSION_SLASHY; + socketFactory = SocketFactory.getDefault(); } /** @@ -104,6 +107,18 @@ public final class RtspMediaSource extends BaseMediaSource { return this; } + /** + * Sets a socket factory for {@link RtspClient}'s connection, the default value is {@link + * SocketFactory#getDefault()}. + * + * @param socketFactory A socket factory. + * @return This Factory, for convenience. + */ + public Factory setSocketFactory(SocketFactory socketFactory) { + this.socketFactory = socketFactory; + return this; + } + /** * Sets whether to log RTSP messages, the default value is {@code false}. * @@ -203,6 +218,7 @@ public final class RtspMediaSource extends BaseMediaSource { ? new TransferRtpDataChannelFactory(timeoutMs) : new UdpDataSourceRtpDataChannelFactory(timeoutMs), userAgent, + socketFactory, debugLoggingEnabled); } } @@ -226,6 +242,7 @@ public final class RtspMediaSource extends BaseMediaSource { private final RtpDataChannel.Factory rtpDataChannelFactory; private final String userAgent; private final Uri uri; + private final SocketFactory socketFactory; private final boolean debugLoggingEnabled; private long timelineDurationUs; @@ -238,11 +255,13 @@ public final class RtspMediaSource extends BaseMediaSource { MediaItem mediaItem, RtpDataChannel.Factory rtpDataChannelFactory, String userAgent, + SocketFactory socketFactory, boolean debugLoggingEnabled) { this.mediaItem = mediaItem; this.rtpDataChannelFactory = rtpDataChannelFactory; this.userAgent = userAgent; this.uri = checkNotNull(this.mediaItem.localConfiguration).uri; + this.socketFactory = socketFactory; this.debugLoggingEnabled = debugLoggingEnabled; this.timelineDurationUs = C.TIME_UNSET; this.timelineIsPlaceholder = true; @@ -282,6 +301,7 @@ public final class RtspMediaSource extends BaseMediaSource { notifySourceInfoRefreshed(); }, userAgent, + socketFactory, debugLoggingEnabled); } 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 0018c8a9e4..02d083278c 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 @@ -27,8 +27,12 @@ import com.google.android.exoplayer2.source.rtsp.RtspClient.SessionInfoListener; import com.google.android.exoplayer2.source.rtsp.RtspMediaSource.RtspPlaybackException; import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import javax.net.SocketFactory; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -77,6 +81,80 @@ public final class RtspClientTest { Util.closeQuietly(rtspClient); } + @Test + public void connectServerAndClient_usesCustomSocketFactory() 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) { + return RtspTestUtils.newDescribeResponseWithSdpMessage( + SESSION_DESCRIPTION, rtpPacketStreamDumps, requestedUri); + } + } + rtspServer = new RtspServer(new ResponseProvider()); + + AtomicBoolean didCallCreateSocket = new AtomicBoolean(); + SocketFactory socketFactory = + new SocketFactory() { + + @Override + public Socket createSocket(String host, int port) throws IOException { + didCallCreateSocket.set(true); + return SocketFactory.getDefault().createSocket(host, port); + } + + @Override + public Socket createSocket(String s, int i, InetAddress inetAddress, int i1) + throws IOException { + didCallCreateSocket.set(true); + return SocketFactory.getDefault().createSocket(s, i, inetAddress, i1); + } + + @Override + public Socket createSocket(InetAddress inetAddress, int i) throws IOException { + didCallCreateSocket.set(true); + return SocketFactory.getDefault().createSocket(inetAddress, i); + } + + @Override + public Socket createSocket( + InetAddress inetAddress, int i, InetAddress inetAddress1, int i1) throws IOException { + didCallCreateSocket.set(true); + return SocketFactory.getDefault().createSocket(inetAddress, i, inetAddress1, i1); + } + }; + + AtomicReference> tracksInSession = new AtomicReference<>(); + rtspClient = + new RtspClient( + new SessionInfoListener() { + @Override + public void onSessionTimelineUpdated( + RtspSessionTiming timing, ImmutableList tracks) { + tracksInSession.set(tracks); + } + + @Override + public void onSessionTimelineRequestFailed( + String message, @Nullable Throwable cause) {} + }, + EMPTY_PLAYBACK_LISTENER, + /* userAgent= */ "ExoPlayer:RtspClientTest", + RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber()), + socketFactory, + /* debugLoggingEnabled= */ false); + rtspClient.start(); + RobolectricUtil.runMainLooperUntil(() -> tracksInSession.get() != null); + + assertThat(didCallCreateSocket.get()).isTrue(); + } + @Test public void connectServerAndClient_serverSupportsDescribe_updatesSessionTimeline() throws Exception { @@ -113,6 +191,7 @@ public final class RtspClientTest { EMPTY_PLAYBACK_LISTENER, /* userAgent= */ "ExoPlayer:RtspClientTest", RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber()), + SocketFactory.getDefault(), /* debugLoggingEnabled= */ false); rtspClient.start(); RobolectricUtil.runMainLooperUntil(() -> tracksInSession.get() != null); @@ -164,6 +243,7 @@ public final class RtspClientTest { EMPTY_PLAYBACK_LISTENER, /* userAgent= */ "ExoPlayer:RtspClientTest", RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber()), + SocketFactory.getDefault(), /* debugLoggingEnabled= */ false); rtspClient.start(); RobolectricUtil.runMainLooperUntil(() -> tracksInSession.get() != null); @@ -207,6 +287,7 @@ public final class RtspClientTest { EMPTY_PLAYBACK_LISTENER, /* userAgent= */ "ExoPlayer:RtspClientTest", RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber()), + SocketFactory.getDefault(), /* debugLoggingEnabled= */ false); rtspClient.start(); RobolectricUtil.runMainLooperUntil(() -> tracksInSession.get() != null); @@ -253,6 +334,7 @@ public final class RtspClientTest { EMPTY_PLAYBACK_LISTENER, /* userAgent= */ "ExoPlayer:RtspClientTest", RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber()), + SocketFactory.getDefault(), /* debugLoggingEnabled= */ false); rtspClient.start(); RobolectricUtil.runMainLooperUntil(() -> failureMessage.get() != null); @@ -299,6 +381,7 @@ public final class RtspClientTest { EMPTY_PLAYBACK_LISTENER, /* userAgent= */ "ExoPlayer:RtspClientTest", RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber()), + SocketFactory.getDefault(), /* debugLoggingEnabled= */ false); rtspClient.start(); diff --git a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMediaPeriodTest.java b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMediaPeriodTest.java index da7da55d8e..8c669ccc36 100644 --- a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMediaPeriodTest.java +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMediaPeriodTest.java @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; +import javax.net.SocketFactory; import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; @@ -84,6 +85,7 @@ public final class RtspMediaPeriodTest { RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber()), /* listener= */ timing -> refreshedSourceDurationMs.set(timing.getDurationMs()), /* userAgent= */ "ExoPlayer:RtspPeriodTest", + /* socketFactory= */ SocketFactory.getDefault(), /* debugLoggingEnabled= */ false); mediaPeriod.prepare( diff --git a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspPlaybackTest.java b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspPlaybackTest.java index c82f68c0a3..1b86d031d3 100644 --- a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspPlaybackTest.java +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspPlaybackTest.java @@ -46,6 +46,7 @@ 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.Before; import org.junit.Rule; import org.junit.Test; @@ -156,6 +157,7 @@ public final class RtspPlaybackTest { MediaItem.fromUri(RtspTestUtils.getTestUri(serverRtspPortNumber)), rtpDataChannelFactory, "ExoPlayer:PlaybackTest", + SocketFactory.getDefault(), /* debugLoggingEnabled= */ false), false); return player;