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 71c94c2db9..0c4cff6446 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 @@ -543,20 +543,27 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } private void onDescribeResponseReceived(RtspDescribeResponse response) { + RtspSessionTiming sessionTiming = RtspSessionTiming.DEFAULT; @Nullable String sessionRangeAttributeString = response.sessionDescription.attributes.get(SessionDescription.ATTR_RANGE); - - try { - sessionInfoListener.onSessionTimelineUpdated( - sessionRangeAttributeString != null - ? RtspSessionTiming.parseTiming(sessionRangeAttributeString) - : RtspSessionTiming.DEFAULT, - buildTrackList(response.sessionDescription, uri)); - hasUpdatedTimelineAndTracks = true; - } catch (ParserException e) { - sessionInfoListener.onSessionTimelineRequestFailed("SDP format error.", /* cause= */ e); + if (sessionRangeAttributeString != null) { + try { + sessionTiming = RtspSessionTiming.parseTiming(sessionRangeAttributeString); + } catch (ParserException e) { + sessionInfoListener.onSessionTimelineRequestFailed("SDP format error.", /* cause= */ e); + return; + } } + + ImmutableList tracks = buildTrackList(response.sessionDescription, uri); + if (tracks.isEmpty()) { + sessionInfoListener.onSessionTimelineRequestFailed("No playable track.", /* cause= */ null); + return; + } + + sessionInfoListener.onSessionTimelineUpdated(sessionTiming, tracks); + hasUpdatedTimelineAndTracks = true; } private void onSetupResponseReceived(RtspSetupResponse response) { 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 a038b53315..f6c9298b07 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 @@ -228,7 +228,7 @@ public final class RtspMediaSource extends BaseMediaSource { allocator, rtpDataChannelFactory, uri, - (timing) -> { + /* listener= */ timing -> { timelineDurationUs = C.msToUs(timing.getDurationMs()); timelineIsSeekable = !timing.isLive(); timelineIsLive = timing.isLive(); 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 new file mode 100644 index 0000000000..c5f80cf673 --- /dev/null +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspPlaybackTest.java @@ -0,0 +1,190 @@ +/* + * 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; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.Player.Listener; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.robolectric.RobolectricUtil; +import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig; +import com.google.android.exoplayer2.testutil.FakeClock; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.internal.DoNotInstrument; + +/** Playback testing for RTSP. */ +@Config(sdk = 29) +@DoNotInstrument +@RunWith(AndroidJUnit4.class) +public final class RtspPlaybackTest { + + 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" + + "a=range:npt=0-50.46\r\n"; + + private RtpPacketStreamDump aacRtpPacketStreamDump; + // ExoPlayer does not support extracting MP4A-LATM RTP payload at the moment. + private RtpPacketStreamDump mp4aLatmRtpPacketStreamDump; + + @Rule + public ShadowMediaCodecConfig mediaCodecConfig = + ShadowMediaCodecConfig.forAllSupportedMimeTypes(); + + @Before + public void setUp() throws Exception { + aacRtpPacketStreamDump = RtspTestUtils.readRtpPacketStreamDump("media/rtsp/aac-dump.json"); + mp4aLatmRtpPacketStreamDump = + RtspTestUtils.readRtpPacketStreamDump("media/rtsp/mp4a-latm-dump.json"); + } + + @Test + public void prepare_withSupportedTrack_sendsPlayRequest() throws Exception { + ResponseProvider responseProvider = + new ResponseProvider(ImmutableList.of(aacRtpPacketStreamDump, mp4aLatmRtpPacketStreamDump)); + try (RtspServer rtspServer = new RtspServer(responseProvider)) { + + SimpleExoPlayer player = createSimpleExoPlayer(rtspServer.startAndGetPortNumber()); + player.prepare(); + RobolectricUtil.runMainLooperUntil(responseProvider::hasReceivedPlayRequest); + player.release(); + + // Only setup the supported track (aac). + ImmutableList receivedSetupUris = responseProvider.getReceivedSetupUris(); + assertThat(receivedSetupUris).hasSize(1); + assertThat(receivedSetupUris.get(0).toString()).contains(aacRtpPacketStreamDump.trackName); + } + } + + @Test + public void prepare_noSupportedTrack_throwsPreparationError() throws Exception { + try (RtspServer rtspServer = + new RtspServer(new ResponseProvider(ImmutableList.of(mp4aLatmRtpPacketStreamDump)))) { + SimpleExoPlayer player = createSimpleExoPlayer(rtspServer.startAndGetPortNumber()); + + AtomicReference playbackError = new AtomicReference<>(); + player.prepare(); + player.addListener( + new Listener() { + @Override + public void onPlayerError(ExoPlaybackException error) { + playbackError.set(error); + } + }); + RobolectricUtil.runMainLooperUntil(() -> playbackError.get() != null); + player.release(); + + assertThat(playbackError.get()) + .hasCauseThat() + .hasMessageThat() + .contains("No playable track."); + } + } + + private static SimpleExoPlayer createSimpleExoPlayer(int serverRtspPortNumber) { + SimpleExoPlayer player = + new SimpleExoPlayer.Builder(ApplicationProvider.getApplicationContext()) + .setClock(new FakeClock(/* isAutoAdvancing= */ true)) + .build(); + + player.setMediaSource( + new RtspMediaSource.Factory() + .setForceUseRtpTcp(true) + .setUserAgent("ExoPlayer:PlaybackTest") + .createMediaSource(MediaItem.fromUri(RtspTestUtils.getTestUri(serverRtspPortNumber)))); + + return player; + } + + private static final class ResponseProvider implements RtspServer.ResponseProvider { + + private static final String SESSION_ID = "00000000"; + + private final ArrayList receivedSetupUris; + private final ImmutableList rtpPacketStreamDumps; + + private boolean hasReceivedPlayRequest; + + /** + * Creates a new instance. + * + * @param rtpPacketStreamDumps A list of {@link RtpPacketStreamDump}. + */ + public ResponseProvider(List rtpPacketStreamDumps) { + this.rtpPacketStreamDumps = ImmutableList.copyOf(rtpPacketStreamDumps); + receivedSetupUris = new ArrayList<>(); + } + + /** Returns whether a PLAY request is received. */ + public boolean hasReceivedPlayRequest() { + return hasReceivedPlayRequest; + } + + /** Returns a list of the received SETUP requests' {@link Uri URIs}. */ + public ImmutableList getReceivedSetupUris() { + return ImmutableList.copyOf(receivedSetupUris); + } + + // RtspServer.ResponseProvider implementation. Called on the main thread. + + @Override + public RtspResponse getOptionsResponse() { + return new RtspResponse( + /* status= */ 200, + new RtspHeaders.Builder() + .add(RtspHeaders.PUBLIC, "OPTIONS, DESCRIBE, SETUP, PLAY") + .build()); + } + + @Override + public RtspResponse getDescribeResponse(Uri requestedUri) { + return RtspTestUtils.newDescribeResponseWithSdpMessage( + SESSION_DESCRIPTION, rtpPacketStreamDumps, requestedUri); + } + + @Override + public RtspResponse getSetupResponse(Uri requestedUri, RtspHeaders headers) { + receivedSetupUris.add(requestedUri); + return new RtspResponse( + /* status= */ 200, headers.buildUpon().add(RtspHeaders.SESSION, SESSION_ID).build()); + } + + @Override + public RtspResponse getPlayResponse() { + hasReceivedPlayRequest = true; + return new RtspResponse( + /* status= */ 200, + new RtspHeaders.Builder() + .add(RtspHeaders.RTP_INFO, RtspTestUtils.getRtpInfoForDumps(rtpPacketStreamDumps)) + .build()); + } + } +} diff --git a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspServer.java b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspServer.java index eb0f0d259b..0799b26fa8 100644 --- a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspServer.java +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspServer.java @@ -17,6 +17,8 @@ package com.google.android.exoplayer2.source.rtsp; import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_DESCRIBE; import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_OPTIONS; +import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_PLAY; +import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_SETUP; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import android.net.Uri; @@ -45,6 +47,16 @@ public final class RtspServer implements Closeable { default RtspResponse getDescribeResponse(Uri requestedUri) { return RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED; } + + /** Returns an RTSP SETUP {@link RtspResponse response}. */ + default RtspResponse getSetupResponse(Uri requestedUri, RtspHeaders headers) { + return RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED; + } + + /** Returns an RTSP PLAY {@link RtspResponse response}. */ + default RtspResponse getPlayResponse() { + return RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED; + } } private final Thread listenerThread; @@ -126,6 +138,14 @@ public final class RtspServer implements Closeable { sendResponse(responseProvider.getDescribeResponse(request.uri), cSeq); break; + case METHOD_SETUP: + sendResponse(responseProvider.getSetupResponse(request.uri, request.headers), cSeq); + break; + + case METHOD_PLAY: + sendResponse(responseProvider.getPlayResponse(), cSeq); + break; + default: sendResponse(RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED, cSeq); } diff --git a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspTestUtils.java b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspTestUtils.java index c6621c2ef3..ce0f0c232d 100644 --- a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspTestUtils.java +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspTestUtils.java @@ -19,12 +19,17 @@ import android.net.Uri; import androidx.test.core.app.ApplicationProvider; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.Util; +import com.google.common.base.Joiner; import java.io.IOException; +import java.util.ArrayList; import java.util.List; /** Utility methods for RTSP tests. */ /* package */ final class RtspTestUtils { + private static final String TEST_BASE_URI = "rtsp://localhost:%d/test"; + private static final String RTP_TIME_FORMAT = "url=rtsp://localhost/test/%s;seq=%d;rtptime=%d"; + /** RTSP error Method Not Allowed (RFC2326 Section 7.1.1). */ public static final RtspResponse RTSP_ERROR_METHOD_NOT_ALLOWED = new RtspResponse(454, RtspHeaders.EMPTY); @@ -62,7 +67,20 @@ import java.util.List; /** Returns the test RTSP {@link Uri}. */ public static Uri getTestUri(int serverRtspPortNumber) { - return Uri.parse(Util.formatInvariant("rtsp://localhost:%d/test", serverRtspPortNumber)); + return Uri.parse(Util.formatInvariant(TEST_BASE_URI, serverRtspPortNumber)); + } + + public static String getRtpInfoForDumps(List rtpPacketStreamDumps) { + ArrayList rtpInfos = new ArrayList<>(rtpPacketStreamDumps.size()); + for (RtpPacketStreamDump rtpPacketStreamDump : rtpPacketStreamDumps) { + rtpInfos.add( + Util.formatInvariant( + RTP_TIME_FORMAT, + rtpPacketStreamDump.trackName, + rtpPacketStreamDump.firstSequenceNumber, + rtpPacketStreamDump.firstTimestamp)); + } + return Joiner.on(",").join(rtpInfos); } private RtspTestUtils() {}