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 41f404d097..67dd8b75b3 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 @@ -43,11 +43,11 @@ import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.rtsp.RtspClient.PlaybackEventListener; import com.google.android.exoplayer2.source.rtsp.RtspMediaSource.RtspPlaybackException; +import com.google.android.exoplayer2.source.rtsp.rtp.RtpDataChannel; import com.google.android.exoplayer2.source.rtsp.rtp.RtpDataLoadable; import com.google.android.exoplayer2.trackselection.ExoTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.Loader.Loadable; import com.google.android.exoplayer2.util.Util; @@ -72,6 +72,7 @@ public final class RtspMediaPeriod implements MediaPeriod { private final InternalListener internalListener; private final RtspClient rtspClient; + private final RtpDataChannel.Factory rtpDataChannelFactory; private final List rtspLoaderWrappers; private final List selectedLoadInfos; @@ -93,9 +94,13 @@ public final class RtspMediaPeriod implements MediaPeriod { * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param rtspTracks A list of tracks in an RTSP playback session. * @param rtspClient The {@link RtspClient} for the current RTSP playback. + * @param rtpDataChannelFactory A {@link RtpDataChannel.Factory} for {@link RtpDataChannel}. */ public RtspMediaPeriod( - Allocator allocator, List rtspTracks, RtspClient rtspClient) { + Allocator allocator, + List rtspTracks, + RtspClient rtspClient, + RtpDataChannel.Factory rtpDataChannelFactory) { this.allocator = allocator; handler = Util.createHandlerForCurrentLooper(); @@ -103,6 +108,7 @@ public final class RtspMediaPeriod implements MediaPeriod { rtspLoaderWrappers = new ArrayList<>(rtspTracks.size()); this.rtspClient = rtspClient; this.rtspClient.setPlaybackEventListener(internalListener); + this.rtpDataChannelFactory = rtpDataChannelFactory; for (int i = 0; i < rtspTracks.size(); i++) { RtspMediaTrack rtspMediaTrack = rtspTracks.get(i); @@ -506,7 +512,7 @@ public final class RtspMediaPeriod implements MediaPeriod { playbackException = error; } - /** Handles the {@link Loadable} whose {@link DataSource} timed out. */ + /** Handles the {@link Loadable} whose {@link RtpDataChannel} timed out. */ private void handleSocketTimeout(RtpDataLoadable loadable) { // TODO(b/172331505) Allow for retry when loading is not ending. if (getBufferedPositionUs() == Long.MIN_VALUE) { @@ -648,7 +654,8 @@ public final class RtspMediaPeriod implements MediaPeriod { trackId, mediaTrack, /* eventListener= */ transportEventListener, - /* output= */ internalListener); + /* output= */ internalListener, + rtpDataChannelFactory); } /** 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 888eb40e59..0211b3b35b 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 @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.rtsp.RtspClient.SessionInfoListener; +import com.google.android.exoplayer2.source.rtsp.rtp.RtpDataChannel; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; @@ -131,6 +132,7 @@ public final class RtspMediaSource extends BaseMediaSource { } private final MediaItem mediaItem; + private final RtpDataChannel.Factory rtpDataChannelFactory; private @MonotonicNonNull RtspClient rtspClient; @Nullable private ImmutableList rtspMediaTracks; @@ -138,6 +140,7 @@ public final class RtspMediaSource extends BaseMediaSource { private RtspMediaSource(MediaItem mediaItem) { this.mediaItem = mediaItem; + rtpDataChannelFactory = RtpDataChannel.DEFAULT_FACTORY; } @Override @@ -174,7 +177,8 @@ public final class RtspMediaSource extends BaseMediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { - return new RtspMediaPeriod(allocator, checkNotNull(rtspMediaTracks), checkNotNull(rtspClient)); + return new RtspMediaPeriod( + allocator, checkNotNull(rtspMediaTracks), checkNotNull(rtspClient), rtpDataChannelFactory); } @Override diff --git a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/rtp/RtpDataChannel.java b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/rtp/RtpDataChannel.java new file mode 100644 index 0000000000..d451ec9e59 --- /dev/null +++ b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/rtp/RtpDataChannel.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.rtsp.rtp; + +import com.google.android.exoplayer2.upstream.DataSource; + +/** An RTP {@link DataSource}. */ +public interface RtpDataChannel extends DataSource { + + /** Creates {@link RtpDataChannel} for RTSP streams. */ + interface Factory { + + /** Creates a new {@link RtpDataChannel} instance. */ + RtpDataChannel createDataChannel(); + } + + /** The default {@link Factory} that returns a new {@link UdpDataSourceRtpDataChannel}. */ + Factory DEFAULT_FACTORY = UdpDataSourceRtpDataChannel::new; + + /** Returns the local port used in the underlying transport channel. */ + int getLocalPort(); +} diff --git a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/rtp/RtpDataLoadable.java b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/rtp/RtpDataLoadable.java index 2e04b860df..66c367137d 100644 --- a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/rtp/RtpDataLoadable.java +++ b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/rtp/RtpDataLoadable.java @@ -30,7 +30,6 @@ import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.source.rtsp.RtspMediaTrack; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.Loader; -import com.google.android.exoplayer2.upstream.UdpDataSource; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -39,10 +38,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * A {@link Loader.Loadable} that sets up a sockets listening to incoming RTP traffic, carried by * UDP packets. * - *

Uses a {@link UdpDataSource} to listen on incoming packets. The local UDP port is selected by - * the runtime on opening; it also opens another {@link UdpDataSource} for RTCP on the RTP UDP port + *

Uses a {@link RtpDataChannel} to listen on incoming packets. The local UDP port is selected by + * the runtime on opening; it also opens another {@link RtpDataChannel} for RTCP on the RTP UDP port * number plus one one. Pass a listener via constructor to receive a callback when the local port is - * opened. {@link #load} will throw an {@link IOException} if either of the two data sources fails + * opened. {@link #load} will throw an {@link IOException} if either of the two data channels fails * to open. * *

Received RTP packets' payloads will be extracted by an {@link RtpExtractor}, and will be @@ -76,6 +75,7 @@ public final class RtpDataLoadable implements Loader.Loadable { private final EventListener eventListener; private final ExtractorOutput output; private final Handler playbackThreadHandler; + private final RtpDataChannel.Factory rtpDataChannelFactory; private @MonotonicNonNull RtpExtractor extractor; @@ -92,17 +92,20 @@ public final class RtpDataLoadable implements Loader.Loadable { * @param rtspMediaTrack The {@link RtspMediaTrack} to load. * @param eventListener The {@link EventListener}. * @param output A {@link ExtractorOutput} instance to which the received and extracted data will + * @param rtpDataChannelFactory A {@link RtpDataChannel.Factory} for {@link RtpDataChannel}. */ public RtpDataLoadable( int trackId, RtspMediaTrack rtspMediaTrack, EventListener eventListener, - ExtractorOutput output) { + ExtractorOutput output, + RtpDataChannel.Factory rtpDataChannelFactory) { this.trackId = trackId; this.rtspMediaTrack = rtspMediaTrack; this.eventListener = eventListener; this.output = output; this.playbackThreadHandler = Util.createHandlerForCurrentLooper(); + this.rtpDataChannelFactory = rtpDataChannelFactory; pendingSeekPositionUs = C.TIME_UNSET; } @@ -139,30 +142,30 @@ public final class RtpDataLoadable implements Loader.Loadable { @Override public void load() throws IOException { - @Nullable UdpDataSource firstDataSource = null; - @Nullable UdpDataSource secondDataSource = null; + @Nullable RtpDataChannel firstDataChannel = null; + @Nullable RtpDataChannel secondDataChannel = null; try { - // Open and set up the data sources. + // Open and set up the data channel. // From RFC3550 Section 11: "For UDP and similar protocols, RTP SHOULD use an even destination // port number and the corresponding RTCP stream SHOULD use the next higher (odd) destination - // port number". Some RTSP servers are strict about this rule. - // We open a data source first, and depending its port number, open the next data source with - // a port number that is either the higher or the lower. - firstDataSource = new UdpDataSource(); - firstDataSource.open(new DataSpec(Uri.parse(RTP_BIND_ADDRESS))); + // port number". Some RTSP servers are strict about this rule. We open a data channel first, + // and depending its port number, open the next data channel with a port number that is either + // the higher or the lower. + firstDataChannel = rtpDataChannelFactory.createDataChannel(); + firstDataChannel.open(new DataSpec(Uri.parse(RTP_BIND_ADDRESS))); - int firstPort = firstDataSource.getLocalPort(); + int firstPort = firstDataChannel.getLocalPort(); boolean isFirstPortNumberEven = (firstPort % 2 == 0); int secondPort = isFirstPortNumberEven ? firstPort + 1 : firstPort - 1; // RTCP always uses the immediate next port. - secondDataSource = new UdpDataSource(); - secondDataSource.open(new DataSpec(Uri.parse(RTP_ANY_INCOMING_IPV4 + ":" + secondPort))); + secondDataChannel = rtpDataChannelFactory.createDataChannel(); + secondDataChannel.open(new DataSpec(Uri.parse(RTP_ANY_INCOMING_IPV4 + ":" + secondPort))); // RTP data port is always the lower and even-numbered port. - UdpDataSource dataSource = isFirstPortNumberEven ? firstDataSource : secondDataSource; - int dataPort = dataSource.getLocalPort(); + RtpDataChannel dataChannel = isFirstPortNumberEven ? firstDataChannel : secondDataChannel; + int dataPort = dataChannel.getLocalPort(); int rtcpPort = dataPort + 1; String transport = Util.formatInvariant(DEFAULT_TRANSPORT_FORMAT, dataPort, rtcpPort); playbackThreadHandler.post(() -> eventListener.onTransportReady(transport)); @@ -170,7 +173,7 @@ public final class RtpDataLoadable implements Loader.Loadable { // Sets up the extractor. ExtractorInput extractorInput = new DefaultExtractorInput( - checkNotNull(dataSource), /* position= */ 0, /* length= */ C.LENGTH_UNSET); + checkNotNull(dataChannel), /* position= */ 0, /* length= */ C.LENGTH_UNSET); extractor = new RtpExtractor(rtspMediaTrack.payloadFormat, trackId); extractor.init(output); @@ -183,8 +186,8 @@ public final class RtpDataLoadable implements Loader.Loadable { extractor.read(extractorInput, /* seekPosition= */ new PositionHolder()); } } finally { - closeQuietly(firstDataSource); - closeQuietly(secondDataSource); + closeQuietly(firstDataChannel); + closeQuietly(secondDataChannel); } } diff --git a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/rtp/UdpDataSourceRtpDataChannel.java b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/rtp/UdpDataSourceRtpDataChannel.java new file mode 100644 index 0000000000..f1417c2717 --- /dev/null +++ b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/rtp/UdpDataSourceRtpDataChannel.java @@ -0,0 +1,64 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.rtsp.rtp; + +import android.net.Uri; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.upstream.UdpDataSource; +import java.io.IOException; + +/** An {@link RtpDataChannel} for UDP transport. */ +public final class UdpDataSourceRtpDataChannel implements RtpDataChannel { + private final UdpDataSource dataSource; + + /** Creates a new instance. */ + public UdpDataSourceRtpDataChannel() { + dataSource = new UdpDataSource(); + } + + @Override + public int getLocalPort() { + return dataSource.getLocalPort(); + } + + @Override + public void addTransferListener(TransferListener transferListener) { + dataSource.addTransferListener(transferListener); + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + return dataSource.open(dataSpec); + } + + @Nullable + @Override + public Uri getUri() { + return dataSource.getUri(); + } + + @Override + public void close() { + dataSource.close(); + } + + @Override + public int read(byte[] target, int offset, int length) throws IOException { + return dataSource.read(target, offset, length); + } +} 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 9fa8a17010..a3bb6dfe54 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 @@ -23,6 +23,7 @@ import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.rtsp.rtp.RtpDataChannel; import com.google.android.exoplayer2.source.rtsp.sdp.MediaDescription; import com.google.android.exoplayer2.source.rtsp.sdp.SessionDescription; import com.google.android.exoplayer2.upstream.DefaultAllocator; @@ -68,7 +69,8 @@ public class RtspMediaPeriodTest { .addAttribute(SessionDescription.ATTR_CONTROL, "track1") .build(), Uri.parse("rtsp://localhost/test"))), - PLACEHOLDER_RTSP_CLIENT); + PLACEHOLDER_RTSP_CLIENT, + RtpDataChannel.DEFAULT_FACTORY); AtomicBoolean prepareCallbackCalled = new AtomicBoolean(false); rtspMediaPeriod.prepare( @@ -95,7 +97,8 @@ public class RtspMediaPeriodTest { new RtspMediaPeriod( new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE), ImmutableList.of(), - PLACEHOLDER_RTSP_CLIENT); + PLACEHOLDER_RTSP_CLIENT, + RtpDataChannel.DEFAULT_FACTORY); assertThat(rtspMediaPeriod.getBufferedPositionUs()).isEqualTo(C.TIME_END_OF_SOURCE); }