Allow customizing the RtspServer using RtspServerResponseProvider.

PiperOrigin-RevId: 379282201
This commit is contained in:
claincly 2021-06-14 16:21:54 +01:00 committed by Oliver Woodman
parent 581e543d39
commit 8cc1328d89
11 changed files with 473 additions and 91 deletions

View file

@ -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<String, String> 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<String, String> namesAndValuesBuilder) {
this.namesAndValuesBuilder = namesAndValuesBuilder;
}
/**
* Adds a header name and header value pair.
*
@ -130,6 +143,31 @@ import java.util.Map;
private final ImmutableListMultimap<String, String> 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<String, String> 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.

View file

@ -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= */ "");
}
}

View file

@ -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<RtspTrackTiming> trackTimingList) {}
@Override
public void onPlaybackError(RtspPlaybackException error) {}
};
private ImmutableList<RtpPacketStreamDump> 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<ImmutableList<RtspMediaTrack>> tracksInSession = new AtomicReference<>();
rtspClient =
new RtspClient(
new SessionInfoListener() {
@Override
public void onSessionTimelineUpdated(
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> 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<RtspTrackTiming> 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<ImmutableList<RtspMediaTrack>> tracksInSession = new AtomicReference<>();
rtspClient =
new RtspClient(
new SessionInfoListener() {
@Override
public void onSessionTimelineUpdated(
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> 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<String> failureMessage = new AtomicReference<>();
rtspClient =
new RtspClient(
new SessionInfoListener() {
@Override
public void onSessionTimelineUpdated(
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> 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<Throwable> failureCause = new AtomicReference<>();
rtspClient =
new RtspClient(
new SessionInfoListener() {
@Override
public void onSessionTimelineUpdated(
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> 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);
}
}

View file

@ -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(

View file

@ -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);
}
}

View file

@ -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");

View file

@ -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<String> messageLines = RtspMessageUtil.serializeResponse(response);
List<String> 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<String> messageLines = RtspMessageUtil.serializeResponse(response);
List<String> expectedLines = Arrays.asList("RTSP/1.0 454 Session Not Found", "cseq: 4", "", "");

View file

@ -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 {
*
* <p>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<String, String> 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)));
}
}

View file

@ -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}.
*
* <p>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<RtpPacketStreamDump> 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() {}
}

View file

@ -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": [
]
}

View file

@ -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": [
]
}