diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 655ed9f7c46..3d9bc861909 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -40,6 +40,9 @@ * HLS: * Correctly populate `Format.label` for audio only HLS streams ([#9608](https://github.com/google/ExoPlayer/issues/9608)). +* RTSP + * Provide a client API to override the `SocketFactory` used for any server + connection ([#9606](https://github.com/google/ExoPlayer/pull/9606)). ### 2.16.0 (2021-11-04) diff --git a/docs/rtsp.md b/docs/rtsp.md index 9c4cd38753d..674f100efda 100644 --- a/docs/rtsp.md +++ b/docs/rtsp.md @@ -51,6 +51,30 @@ player.prepare(); ~~~ {: .language-java} +### Passing a custom SocketFactory + +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 +`.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)); +// Create a player instance. +ExoPlayer player = new ExoPlayer.Builder(context).build(); +// Set the media source to be played. +player.setMediaSource(mediaSource); +// Prepare the player. +player.prepare(); +~~~ +{: .language-java} + +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). + ## Using RTSP behind a NAT (RTP/TCP support) ## ExoPlayer uses UDP as the default protocol for RTP transport. 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 a208ea6f305..b8c47b74e48 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 @@ public interface PlaybackEventListener { 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,17 +156,21 @@ public interface PlaybackEventListener { * @param playbackEventListener The {@link PlaybackEventListener}. * @param userAgent The user agent. * @param uri The RTSP playback URI. + * @param socketFactory The {@link SocketFactory} for the client connection. + * @param debugLoggingEnabled Whether to print 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.debugLoggingEnabled = debugLoggingEnabled; + this.socketFactory = socketFactory; this.pendingSetupRtpLoadInfos = new ArrayDeque<>(); this.pendingRequests = new SparseArray<>(); this.messageSender = new MessageSender(); @@ -286,10 +291,11 @@ private void maybeLogMessage(List message) { } /** 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 435bb59a30e..3a60ea7d003 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.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; @@ -96,12 +97,13 @@ interface Listener { /** * Creates an RTSP media period. - * * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param rtpDataChannelFactory A {@link RtpDataChannel.Factory} for {@link RtpDataChannel}. * @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 +111,7 @@ public RtspMediaPeriod( Uri uri, Listener listener, String userAgent, + SocketFactory socketFactory, boolean debugLoggingEnabled) { this.allocator = allocator; this.rtpDataChannelFactory = rtpDataChannelFactory; @@ -122,6 +125,7 @@ public RtspMediaPeriod( /* 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 fbd7ab80755..a1e7dcc6833 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.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,6 +70,7 @@ public static final class Factory implements MediaSourceFactory { private long timeoutMs; private String userAgent; + @Nullable private SocketFactory socketFactory; private boolean forceUseRtpTcp; private boolean debugLoggingEnabled; @@ -118,6 +120,18 @@ public Factory setDebugLoggingEnabled(boolean debugLoggingEnabled) { 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 the timeout in milliseconds, the default value is {@link #DEFAULT_TIMEOUT_MS}. * @@ -203,6 +217,7 @@ public RtspMediaSource createMediaSource(MediaItem mediaItem) { ? new TransferRtpDataChannelFactory(timeoutMs) : new UdpDataSourceRtpDataChannelFactory(timeoutMs), userAgent, + socketFactory == null ? SocketFactory.getDefault() : socketFactory, debugLoggingEnabled); } } @@ -226,6 +241,7 @@ public RtspPlaybackException(String message, Throwable e) { 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,12 +254,14 @@ public RtspPlaybackException(String message, Throwable e) { 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.debugLoggingEnabled = debugLoggingEnabled; + this.socketFactory = socketFactory; this.timelineDurationUs = C.TIME_UNSET; this.timelineIsPlaceholder = true; } @@ -282,6 +300,7 @@ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long star 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 0018c8a9e43..55d2366f406 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.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,79 @@ public void tearDown() { 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()); + + final AtomicReference didCallCreateSocket = new AtomicReference<>(); + + final 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 { + return SocketFactory.getDefault().createSocket(s, i, inetAddress, i1); + } + + @Override + public Socket createSocket(InetAddress inetAddress, int i) throws IOException { + return SocketFactory.getDefault().createSocket(inetAddress, i); + } + + @Override + public Socket createSocket( + InetAddress inetAddress, int i, InetAddress inetAddress1, int i1) throws IOException { + 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 +190,7 @@ public void onSessionTimelineRequestFailed( EMPTY_PLAYBACK_LISTENER, /* userAgent= */ "ExoPlayer:RtspClientTest", RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber()), + /* socketFactory */ SocketFactory.getDefault(), /* debugLoggingEnabled= */ false); rtspClient.start(); RobolectricUtil.runMainLooperUntil(() -> tracksInSession.get() != null); @@ -164,6 +242,7 @@ public void onSessionTimelineRequestFailed( EMPTY_PLAYBACK_LISTENER, /* userAgent= */ "ExoPlayer:RtspClientTest", RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber()), + /* socketFactory */ SocketFactory.getDefault(), /* debugLoggingEnabled= */ false); rtspClient.start(); RobolectricUtil.runMainLooperUntil(() -> tracksInSession.get() != null); @@ -207,6 +286,7 @@ public void onSessionTimelineRequestFailed( EMPTY_PLAYBACK_LISTENER, /* userAgent= */ "ExoPlayer:RtspClientTest", RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber()), + /* socketFactory */ SocketFactory.getDefault(), /* debugLoggingEnabled= */ false); rtspClient.start(); RobolectricUtil.runMainLooperUntil(() -> tracksInSession.get() != null); @@ -253,6 +333,7 @@ public void onSessionTimelineRequestFailed( EMPTY_PLAYBACK_LISTENER, /* userAgent= */ "ExoPlayer:RtspClientTest", RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber()), + /* socketFactory */ SocketFactory.getDefault(), /* debugLoggingEnabled= */ false); rtspClient.start(); RobolectricUtil.runMainLooperUntil(() -> failureMessage.get() != null); @@ -299,6 +380,7 @@ public void onSessionTimelineRequestFailed( EMPTY_PLAYBACK_LISTENER, /* userAgent= */ "ExoPlayer:RtspClientTest", RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber()), + /* socketFactory */ 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 da7da55d8eb..45da1a3785c 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.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 RtspResponse getDescribeResponse(Uri requestedUri) { 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 c82f68c0a3b..9f464ee5c37 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.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 @@ private ExoPlayer createExoPlayer( MediaItem.fromUri(RtspTestUtils.getTestUri(serverRtspPortNumber)), rtpDataChannelFactory, "ExoPlayer:PlaybackTest", + /* socketFactory */ SocketFactory.getDefault(), /* debugLoggingEnabled= */ false), false); return player;