diff --git a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspHeaders.java b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspHeaders.java index 6b56b7b5ce..13dc5e4b60 100644 --- a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspHeaders.java +++ b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspHeaders.java @@ -66,6 +66,9 @@ import java.util.Map; public static final String VIA = "via"; public static final String WWW_AUTHENTICATE = "www-authenticate"; + /** An empty header object. */ + public static final RtspHeaders EMPTY = new RtspHeaders.Builder().build(); + /** Builds {@link RtspHeaders} instances. */ public static final class Builder { private final ImmutableListMultimap.Builder namesAndValuesBuilder; @@ -75,6 +78,16 @@ import java.util.Map; namesAndValuesBuilder = new ImmutableListMultimap.Builder<>(); } + /** + * Creates a new instance to build upon the provided {@link RtspHeaders}. + * + * @param namesAndValuesBuilder A {@link ImmutableListMultimap.Builder} that this builder builds + * upon. + */ + private Builder(ImmutableListMultimap.Builder namesAndValuesBuilder) { + this.namesAndValuesBuilder = namesAndValuesBuilder; + } + /** * Adds a header name and header value pair. * @@ -130,6 +143,31 @@ import java.util.Map; private final ImmutableListMultimap namesAndValues; + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof RtspHeaders)) { + return false; + } + RtspHeaders headers = (RtspHeaders) obj; + return namesAndValues.equals(headers.namesAndValues); + } + + @Override + public int hashCode() { + return namesAndValues.hashCode(); + } + + /** Returns a {@link Builder} initialized with the values of this instance. */ + public Builder buildUpon() { + ImmutableListMultimap.Builder namesAndValuesBuilder = + new ImmutableListMultimap.Builder<>(); + namesAndValuesBuilder.putAll(namesAndValues); + return new Builder(namesAndValuesBuilder); + } + /** * Returns a map that associates header names to the list of values associated with the * corresponding header name. diff --git a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspResponse.java b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspResponse.java index 7d56f1c580..38a7f9a7e0 100644 --- a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspResponse.java +++ b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspResponse.java @@ -41,4 +41,14 @@ package com.google.android.exoplayer2.source.rtsp; this.headers = headers; this.messageBody = messageBody; } + + /** + * Creates a new instance with an empty {@link #messageBody}. + * + * @param status The status code of this response, as defined in RFC 2326 section 11. + * @param headers The headers of this response. + */ + public RtspResponse(int status, RtspHeaders headers) { + this(status, headers, /* messageBody= */ ""); + } } 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 e75dd117dc..8cee5af601 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 @@ -15,21 +15,20 @@ */ package com.google.android.exoplayer2.source.rtsp; -import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import androidx.annotation.Nullable; -import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.robolectric.RobolectricUtil; import com.google.android.exoplayer2.source.rtsp.RtspClient.PlaybackEventListener; import com.google.android.exoplayer2.source.rtsp.RtspClient.SessionInfoListener; import com.google.android.exoplayer2.source.rtsp.RtspMediaSource.RtspPlaybackException; -import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; import java.util.concurrent.atomic.AtomicBoolean; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import java.util.concurrent.atomic.AtomicReference; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -41,16 +40,37 @@ import org.robolectric.annotation.internal.DoNotInstrument; @DoNotInstrument public final class RtspClientTest { - private @MonotonicNonNull RtspClient rtspClient; - private @MonotonicNonNull RtspServer rtspServer; + 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 static final RtspClient.PlaybackEventListener EMPTY_PLAYBACK_LISTENER = + new PlaybackEventListener() { + @Override + public void onRtspSetupCompleted() {} + + @Override + public void onPlaybackStarted( + long startPositionUs, ImmutableList trackTimingList) {} + + @Override + public void onPlaybackError(RtspPlaybackException error) {} + }; + + private ImmutableList rtpPacketStreamDumps; + private RtspClient rtspClient; + private RtspServer rtspServer; @Before public void setUp() throws Exception { - rtspServer = - new RtspServer( - RtpPacketStreamDump.parse( - TestUtil.getString( - ApplicationProvider.getApplicationContext(), "media/rtsp/aac-dump.json"))); + rtpPacketStreamDumps = + ImmutableList.of( + RtspTestUtils.readRtpPacketStreamDump("media/rtsp/h264-dump.json"), + RtspTestUtils.readRtpPacketStreamDump("media/rtsp/aac-dump.json"), + // MP4A-LATM is not supported at the moment. + RtspTestUtils.readRtpPacketStreamDump("media/rtsp/mp4a-latm-dump.json")); } @After @@ -60,40 +80,173 @@ public final class RtspClientTest { } @Test - public void connectServerAndClient_withServerSupportsDescribe_updatesSessionTimeline() + public void connectServerAndClient_serverSupportsDescribe_updatesSessionTimeline() throws Exception { - int serverRtspPortNumber = checkNotNull(rtspServer).startAndGetPortNumber(); + class ResponseProvider implements RtspServer.ResponseProvider { + @Override + public RtspResponse getOptionsResponse() { + return new RtspResponse( + /* status= */ 200, + new RtspHeaders.Builder().add(RtspHeaders.PUBLIC, "OPTIONS, DESCRIBE").build()); + } - AtomicBoolean sessionTimelineUpdateEventReceived = new AtomicBoolean(); + @Override + public RtspResponse getDescribeResponse(Uri requestedUri) { + return RtspTestUtils.newDescribeResponseWithSdpMessage( + SESSION_DESCRIPTION, rtpPacketStreamDumps, requestedUri); + } + } + rtspServer = new RtspServer(new ResponseProvider()); + + AtomicReference> tracksInSession = new AtomicReference<>(); rtspClient = new RtspClient( new SessionInfoListener() { @Override public void onSessionTimelineUpdated( RtspSessionTiming timing, ImmutableList tracks) { - sessionTimelineUpdateEventReceived.set(!tracks.isEmpty()); + tracksInSession.set(tracks); } @Override public void onSessionTimelineRequestFailed( String message, @Nullable Throwable cause) {} }, - new PlaybackEventListener() { - @Override - public void onRtspSetupCompleted() {} - - @Override - public void onPlaybackStarted( - long startPositionUs, ImmutableList trackTimingList) {} - - @Override - public void onPlaybackError(RtspPlaybackException error) {} - }, + EMPTY_PLAYBACK_LISTENER, /* userAgent= */ "ExoPlayer:RtspClientTest", - /* uri= */ Uri.parse( - Util.formatInvariant("rtsp://localhost:%d/test", serverRtspPortNumber))); + RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber())); + rtspClient.start(); + RobolectricUtil.runMainLooperUntil(() -> tracksInSession.get() != null); + + assertThat(tracksInSession.get()).hasSize(2); + } + + @Test + public void + connectServerAndClient_serverSupportsDescribeNoHeaderInOptions_updatesSessionTimeline() + throws Exception { + class ResponseProvider implements RtspServer.ResponseProvider { + @Override + public RtspResponse getOptionsResponse() { + return new RtspResponse(/* status= */ 200, RtspHeaders.EMPTY); + } + + @Override + public RtspResponse getDescribeResponse(Uri requestedUri) { + return RtspTestUtils.newDescribeResponseWithSdpMessage( + SESSION_DESCRIPTION, rtpPacketStreamDumps, requestedUri); + } + } + rtspServer = new RtspServer(new ResponseProvider()); + + 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())); + rtspClient.start(); + RobolectricUtil.runMainLooperUntil(() -> tracksInSession.get() != null); + + assertThat(tracksInSession.get()).hasSize(2); + } + + @Test + public void connectServerAndClient_serverDoesNotSupportDescribe_doesNotUpdateTimeline() + throws Exception { + AtomicBoolean clientHasSentDescribeRequest = new AtomicBoolean(); + + class ResponseProvider implements RtspServer.ResponseProvider { + @Override + public RtspResponse getOptionsResponse() { + return new RtspResponse( + /* status= */ 200, + new RtspHeaders.Builder().add(RtspHeaders.PUBLIC, "OPTIONS").build()); + } + + @Override + public RtspResponse getDescribeResponse(Uri requestedUri) { + clientHasSentDescribeRequest.set(true); + return RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED; + } + } + rtspServer = new RtspServer(new ResponseProvider()); + + AtomicReference failureMessage = new AtomicReference<>(); + rtspClient = + new RtspClient( + new SessionInfoListener() { + @Override + public void onSessionTimelineUpdated( + RtspSessionTiming timing, ImmutableList tracks) {} + + @Override + public void onSessionTimelineRequestFailed( + String message, @Nullable Throwable cause) { + failureMessage.set(message); + } + }, + EMPTY_PLAYBACK_LISTENER, + /* userAgent= */ "ExoPlayer:RtspClientTest", + RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber())); + rtspClient.start(); + RobolectricUtil.runMainLooperUntil(() -> failureMessage.get() != null); + + assertThat(failureMessage.get()).contains("DESCRIBE not supported."); + assertThat(clientHasSentDescribeRequest.get()).isFalse(); + } + + @Test + public void connectServerAndClient_malformedSdpInDescribeResponse_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) { + // This session description misses required the o, t and s tags. + return RtspTestUtils.newDescribeResponseWithSdpMessage( + /* sessionDescription= */ "v=0\r\n", rtpPacketStreamDumps, requestedUri); + } + } + rtspServer = new RtspServer(new ResponseProvider()); + + AtomicReference failureCause = new AtomicReference<>(); + rtspClient = + new RtspClient( + new SessionInfoListener() { + @Override + public void onSessionTimelineUpdated( + RtspSessionTiming timing, ImmutableList tracks) {} + + @Override + public void onSessionTimelineRequestFailed( + String message, @Nullable Throwable cause) { + failureCause.set(cause); + } + }, + EMPTY_PLAYBACK_LISTENER, + /* userAgent= */ "ExoPlayer:RtspClientTest", + RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber())); rtspClient.start(); - RobolectricUtil.runMainLooperUntil(sessionTimelineUpdateEventReceived::get); + RobolectricUtil.runMainLooperUntil(() -> failureCause.get() != null); + assertThat(failureCause.get()).hasCauseThat().isInstanceOf(ParserException.class); } } diff --git a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspHeadersTest.java b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspHeadersTest.java index 450f92b3d4..72427f9518 100644 --- a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspHeadersTest.java +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspHeadersTest.java @@ -66,6 +66,31 @@ public final class RtspHeadersTest { assertThat(headers.get("Transport")).isEqualTo("RTP/AVP;unicast;client_port=65458-65459"); } + @Test + public void buildUpon_createEqualHeaders() { + RtspHeaders headers = + new RtspHeaders.Builder() + .addAll( + ImmutableMap.of( + "Content-Length", "707", + "Transport", "RTP/AVP;unicast;client_port=65458-65459\r\n")) + .build(); + + assertThat(headers.buildUpon().build()).isEqualTo(headers); + } + + @Test + public void buildUpon_buildsUponExistingHeaders() { + RtspHeaders headers = new RtspHeaders.Builder().add("Content-Length", "707").build(); + + assertThat(headers.buildUpon().add("Content-Encoding", "utf-8").build()) + .isEqualTo( + new RtspHeaders.Builder() + .add("Content-Length", "707") + .add("Content-Encoding", "utf-8") + .build()); + } + @Test public void get_getsHeaderValuesCaseInsensitively() { RtspHeaders headers = @@ -144,7 +169,8 @@ public final class RtspHeadersTest { } @Test - public void asMap_withMultipleValuesMappedToTheSameName_getsTheMappedValuesInAdditionOrder() { + public void + asMultiMap_withMultipleValuesMappedToTheSameName_getsTheMappedValuesInAdditionOrder() { RtspHeaders headers = new RtspHeaders.Builder() .addAll( 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 new file mode 100644 index 0000000000..29ab21d8bc --- /dev/null +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMediaPeriodTest.java @@ -0,0 +1,106 @@ +/* + * 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.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.robolectric.RobolectricUtil; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.upstream.DefaultAllocator; +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 org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.internal.DoNotInstrument; + +/** Tests the {@link RtspMediaPeriod} using the {@link RtspServer}. */ +@RunWith(AndroidJUnit4.class) +@DoNotInstrument +public final class RtspMediaPeriodTest { + + private RtspMediaPeriod mediaPeriod; + private RtspServer rtspServer; + + @After + public void tearDown() { + Util.closeQuietly(rtspServer); + } + + @Test + public void prepareMediaPeriod_refreshesSourceInfoAndCallsOnPrepared() throws Exception { + RtpPacketStreamDump rtpPacketStreamDump = + RtspTestUtils.readRtpPacketStreamDump("media/rtsp/aac-dump.json"); + + rtspServer = + new RtspServer( + new 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( + "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", + ImmutableList.of(rtpPacketStreamDump), + requestedUri); + } + }); + + AtomicBoolean prepareCallbackCalled = new AtomicBoolean(); + AtomicLong refreshedSourceDurationMs = new AtomicLong(); + + mediaPeriod = + new RtspMediaPeriod( + new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE), + new TransferRtpDataChannelFactory(), + RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber()), + /* listener= */ timing -> refreshedSourceDurationMs.set(timing.getDurationMs()), + /* userAgent= */ "ExoPlayer:RtspPeriodTest"); + + mediaPeriod.prepare( + new MediaPeriod.Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + prepareCallbackCalled.set(true); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + source.continueLoading(/* positionUs= */ 0); + } + }, + /* positionUs= */ 0); + RobolectricUtil.runMainLooperUntil(prepareCallbackCalled::get); + mediaPeriod.release(); + + assertThat(refreshedSourceDurationMs.get()).isEqualTo(50_460); + } +} diff --git a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMessageChannelTest.java b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMessageChannelTest.java index a7125d03b1..1cd54053e6 100644 --- a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMessageChannelTest.java +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMessageChannelTest.java @@ -55,8 +55,7 @@ public final class RtspMessageChannelTest { new RtspHeaders.Builder() .add(RtspHeaders.CSEQ, "2") .add(RtspHeaders.PUBLIC, "OPTIONS") - .build(), - ""); + .build()); RtspResponse describeResponse = new RtspResponse( @@ -84,8 +83,7 @@ public final class RtspMessageChannelTest { new RtspHeaders.Builder() .add(RtspHeaders.CSEQ, "5") .add(RtspHeaders.TRANSPORT, "RTP/AVP/TCP;unicast;interleaved=0-1") - .build(), - ""); + .build()); // Channel: 0, size: 5, data: 01 02 03 04 05. byte[] interleavedData1 = Util.getBytesFromHexString("0000050102030405"); diff --git a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMessageUtilTest.java b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMessageUtilTest.java index 3454ea3b83..f0663ac342 100644 --- a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMessageUtilTest.java +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMessageUtilTest.java @@ -250,8 +250,7 @@ public final class RtspMessageUtilTest { "4", RtspHeaders.TRANSPORT, "RTP/AVP;unicast;client_port=65458-65459;server_port=5354-5355")) - .build(), - /* messageBody= */ ""); + .build()); List messageLines = RtspMessageUtil.serializeResponse(response); List expectedLines = @@ -340,9 +339,7 @@ public final class RtspMessageUtilTest { public void serialize_failedResponse_succeeds() { RtspResponse response = new RtspResponse( - /* status= */ 454, - new RtspHeaders.Builder().add(RtspHeaders.CSEQ, "4").build(), - /* messageBody= */ ""); + /* status= */ 454, new RtspHeaders.Builder().add(RtspHeaders.CSEQ, "4").build()); List messageLines = RtspMessageUtil.serializeResponse(response); List expectedLines = Arrays.asList("RTSP/1.0 454 Session Not Found", "cseq: 4", "", ""); 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 89c2853354..eb0f0d259b 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 @@ -23,7 +23,6 @@ import android.net.Uri; import android.os.Handler; import android.os.Looper; import com.google.android.exoplayer2.util.Util; -import com.google.common.collect.ImmutableMap; import java.io.Closeable; import java.io.IOException; import java.net.InetAddress; @@ -31,31 +30,28 @@ import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.util.List; -import java.util.Map; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** The RTSP server. */ public final class RtspServer implements Closeable { - private static final String PUBLIC_SUPPORTED_METHODS = "OPTIONS, DESCRIBE"; + /** Provides RTSP response. */ + public interface ResponseProvider { - /** RTSP error Method Not Allowed (RFC2326 Section 7.1.1). */ - private static final int STATUS_OK = 200; + /** Returns an RTSP OPTIONS {@link RtspResponse response}. */ + RtspResponse getOptionsResponse(); - private static final int STATUS_METHOD_NOT_ALLOWED = 405; - - 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"; + /** Returns an RTSP DESCRIBE {@link RtspResponse response}. */ + default RtspResponse getDescribeResponse(Uri requestedUri) { + return RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED; + } + } private final Thread listenerThread; /** Runs on the thread on which the constructor was called. */ private final Handler mainHandler; - private final RtpPacketStreamDump rtpPacketStreamDump; + private final ResponseProvider responseProvider; private @MonotonicNonNull ServerSocket serverSocket; private @MonotonicNonNull RtspMessageChannel connectedClient; @@ -67,11 +63,11 @@ public final class RtspServer implements Closeable { * *

The constructor must be called on a {@link Looper} thread. */ - public RtspServer(RtpPacketStreamDump rtpPacketStreamDump) { - this.rtpPacketStreamDump = rtpPacketStreamDump; + public RtspServer(ResponseProvider responseProvider) { listenerThread = new Thread(this::listenToIncomingRtspConnection, "ExoPlayerTest:RtspConnectionMonitor"); mainHandler = Util.createHandlerForCurrentLooper(); + this.responseProvider = responseProvider; } /** @@ -123,54 +119,25 @@ public final class RtspServer implements Closeable { String cSeq = checkNotNull(request.headers.get(RtspHeaders.CSEQ)); switch (request.method) { case METHOD_OPTIONS: - onOptionsRequestReceived(cSeq); + sendResponse(responseProvider.getOptionsResponse(), cSeq); break; case METHOD_DESCRIBE: - onDescribeRequestReceived(request.uri, cSeq); + sendResponse(responseProvider.getDescribeResponse(request.uri), cSeq); break; default: - sendErrorResponse(STATUS_METHOD_NOT_ALLOWED, cSeq); + sendResponse(RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED, cSeq); } } - private void onOptionsRequestReceived(String cSeq) { - sendResponseWithCommonHeaders( - /* status= */ STATUS_OK, - /* cSeq= */ cSeq, - /* additionalHeaders= */ ImmutableMap.of(RtspHeaders.PUBLIC, PUBLIC_SUPPORTED_METHODS), - /* messageBody= */ ""); - } - - private void onDescribeRequestReceived(Uri requestedUri, String cSeq) { - String sdpMessage = SESSION_DESCRIPTION + rtpPacketStreamDump.mediaDescription + "\r\n"; - sendResponseWithCommonHeaders( - /* status= */ STATUS_OK, - /* cSeq= */ cSeq, - /* additionalHeaders= */ ImmutableMap.of( - RtspHeaders.CONTENT_BASE, requestedUri.toString(), - RtspHeaders.CONTENT_TYPE, "application/sdp", - RtspHeaders.CONTENT_LENGTH, String.valueOf(sdpMessage.length())), - /* messageBody= */ sdpMessage); - } - - private void sendErrorResponse(int status, String cSeq) { - sendResponseWithCommonHeaders( - status, cSeq, /* additionalHeaders= */ ImmutableMap.of(), /* messageBody= */ ""); - } - - private void sendResponseWithCommonHeaders( - int status, String cSeq, Map additionalHeaders, String messageBody) { - RtspHeaders.Builder headerBuilder = new RtspHeaders.Builder(); - headerBuilder.add(RtspHeaders.CSEQ, cSeq); - headerBuilder.addAll(additionalHeaders); + private void sendResponse(RtspResponse response, String cSeq) { connectedClient.send( RtspMessageUtil.serializeResponse( new RtspResponse( - /* status= */ status, - /* headers= */ headerBuilder.build(), - /* messageBody= */ messageBody))); + response.status, + response.headers.buildUpon().add(RtspHeaders.CSEQ, cSeq).build(), + response.messageBody))); } } 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 new file mode 100644 index 0000000000..c6621c2ef3 --- /dev/null +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspTestUtils.java @@ -0,0 +1,69 @@ +/* + * 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 android.net.Uri; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.util.Util; +import java.io.IOException; +import java.util.List; + +/** Utility methods for RTSP tests. */ +/* package */ final class RtspTestUtils { + + /** RTSP error Method Not Allowed (RFC2326 Section 7.1.1). */ + public static final RtspResponse RTSP_ERROR_METHOD_NOT_ALLOWED = + new RtspResponse(454, RtspHeaders.EMPTY); + + /** + * Parses and returns an {@link RtpPacketStreamDump} from the file identified by {@code filepath}. + * + *

See {@link RtpPacketStreamDump#parse} for details on the dump file format. + */ + public static RtpPacketStreamDump readRtpPacketStreamDump(String filepath) throws IOException { + return RtpPacketStreamDump.parse( + TestUtil.getString(ApplicationProvider.getApplicationContext(), filepath)); + } + + /** Returns an {@link RtspResponse} with a SDP message body. */ + public static RtspResponse newDescribeResponseWithSdpMessage( + String sessionDescription, List rtpPacketStreamDumps, Uri requestedUri) { + + StringBuilder sdpMessageBuilder = new StringBuilder(sessionDescription); + for (RtpPacketStreamDump rtpPacketStreamDump : rtpPacketStreamDumps) { + sdpMessageBuilder.append(rtpPacketStreamDump.mediaDescription).append("\r\n"); + } + String sdpMessage = sdpMessageBuilder.toString(); + + return new RtspResponse( + 200, + new RtspHeaders.Builder() + .add(RtspHeaders.CONTENT_BASE, requestedUri.toString()) + .add( + RtspHeaders.CONTENT_LENGTH, + String.valueOf(sdpMessage.getBytes(RtspMessageChannel.CHARSET).length)) + .build(), + /* messageBody= */ sdpMessage); + } + + /** Returns the test RTSP {@link Uri}. */ + public static Uri getTestUri(int serverRtspPortNumber) { + return Uri.parse(Util.formatInvariant("rtsp://localhost:%d/test", serverRtspPortNumber)); + } + + private RtspTestUtils() {} +} diff --git a/testdata/src/test/assets/media/rtsp/h264-dump.json b/testdata/src/test/assets/media/rtsp/h264-dump.json new file mode 100644 index 0000000000..6b2e10a770 --- /dev/null +++ b/testdata/src/test/assets/media/rtsp/h264-dump.json @@ -0,0 +1,9 @@ +{ + "trackName": "track1", + "firstSequenceNumber": 0, + "firstTimestamp": 0, + "transmitIntervalMs": 30, + "mediaDescription": "m=video 0 RTP/AVP 96\r\nc=IN IP4 0.0.0.0\r\nb=AS:500\r\na=rtpmap:96 H264/90000\r\na=fmtp:96 packetization-mode=1;profile-level-id=4dE01E;sprop-parameter-sets=Z01AHpZ2BQHtgKBAAAOpgACvyA0YAgQAgRe98HhEI3A=,aN48gA==\r\na=control:track1\r\n", + "packets": [ + ] +} diff --git a/testdata/src/test/assets/media/rtsp/mp4a-latm-dump.json b/testdata/src/test/assets/media/rtsp/mp4a-latm-dump.json new file mode 100644 index 0000000000..a7ee934a83 --- /dev/null +++ b/testdata/src/test/assets/media/rtsp/mp4a-latm-dump.json @@ -0,0 +1,9 @@ +{ + "trackName": "track3", + "firstSequenceNumber": 0, + "firstTimestamp": 0, + "transmitIntervalMs": 30, + "mediaDescription": "m=audio 0 RTP/AVP 97\r\nc=IN IP4 0.0.0.0\r\nb=AS:61\r\na=rtpmap:97 MP4A-LATM/44100/2\r\na=fmtp:97 profile-level-id=15;object=2;cpresent=0;config=400024203FC0\r\na=control:track3\r\n", + "packets": [ + ] +}