Commit 8cc1328d by claincly Committed by Oliver Woodman

Allow customizing the RtspServer using RtspServerResponseProvider.

PiperOrigin-RevId: 379282201
parent 581e543d
...@@ -66,6 +66,9 @@ import java.util.Map; ...@@ -66,6 +66,9 @@ import java.util.Map;
public static final String VIA = "via"; public static final String VIA = "via";
public static final String WWW_AUTHENTICATE = "www-authenticate"; 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. */ /** Builds {@link RtspHeaders} instances. */
public static final class Builder { public static final class Builder {
private final ImmutableListMultimap.Builder<String, String> namesAndValuesBuilder; private final ImmutableListMultimap.Builder<String, String> namesAndValuesBuilder;
...@@ -76,6 +79,16 @@ import java.util.Map; ...@@ -76,6 +79,16 @@ import java.util.Map;
} }
/** /**
* 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. * Adds a header name and header value pair.
* *
* @param headerName The name of the header. * @param headerName The name of the header.
...@@ -130,6 +143,31 @@ import java.util.Map; ...@@ -130,6 +143,31 @@ import java.util.Map;
private final ImmutableListMultimap<String, String> namesAndValues; 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 * Returns a map that associates header names to the list of values associated with the
* corresponding header name. * corresponding header name.
......
...@@ -41,4 +41,14 @@ package com.google.android.exoplayer2.source.rtsp; ...@@ -41,4 +41,14 @@ package com.google.android.exoplayer2.source.rtsp;
this.headers = headers; this.headers = headers;
this.messageBody = messageBody; 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= */ "");
}
} }
...@@ -67,6 +67,31 @@ public final class RtspHeadersTest { ...@@ -67,6 +67,31 @@ public final class RtspHeadersTest {
} }
@Test @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() { public void get_getsHeaderValuesCaseInsensitively() {
RtspHeaders headers = RtspHeaders headers =
new RtspHeaders.Builder() new RtspHeaders.Builder()
...@@ -144,7 +169,8 @@ public final class RtspHeadersTest { ...@@ -144,7 +169,8 @@ public final class RtspHeadersTest {
} }
@Test @Test
public void asMap_withMultipleValuesMappedToTheSameName_getsTheMappedValuesInAdditionOrder() { public void
asMultiMap_withMultipleValuesMappedToTheSameName_getsTheMappedValuesInAdditionOrder() {
RtspHeaders headers = RtspHeaders headers =
new RtspHeaders.Builder() new RtspHeaders.Builder()
.addAll( .addAll(
......
/*
* 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);
}
}
...@@ -55,8 +55,7 @@ public final class RtspMessageChannelTest { ...@@ -55,8 +55,7 @@ public final class RtspMessageChannelTest {
new RtspHeaders.Builder() new RtspHeaders.Builder()
.add(RtspHeaders.CSEQ, "2") .add(RtspHeaders.CSEQ, "2")
.add(RtspHeaders.PUBLIC, "OPTIONS") .add(RtspHeaders.PUBLIC, "OPTIONS")
.build(), .build());
"");
RtspResponse describeResponse = RtspResponse describeResponse =
new RtspResponse( new RtspResponse(
...@@ -84,8 +83,7 @@ public final class RtspMessageChannelTest { ...@@ -84,8 +83,7 @@ public final class RtspMessageChannelTest {
new RtspHeaders.Builder() new RtspHeaders.Builder()
.add(RtspHeaders.CSEQ, "5") .add(RtspHeaders.CSEQ, "5")
.add(RtspHeaders.TRANSPORT, "RTP/AVP/TCP;unicast;interleaved=0-1") .add(RtspHeaders.TRANSPORT, "RTP/AVP/TCP;unicast;interleaved=0-1")
.build(), .build());
"");
// Channel: 0, size: 5, data: 01 02 03 04 05. // Channel: 0, size: 5, data: 01 02 03 04 05.
byte[] interleavedData1 = Util.getBytesFromHexString("0000050102030405"); byte[] interleavedData1 = Util.getBytesFromHexString("0000050102030405");
......
...@@ -250,8 +250,7 @@ public final class RtspMessageUtilTest { ...@@ -250,8 +250,7 @@ public final class RtspMessageUtilTest {
"4", "4",
RtspHeaders.TRANSPORT, RtspHeaders.TRANSPORT,
"RTP/AVP;unicast;client_port=65458-65459;server_port=5354-5355")) "RTP/AVP;unicast;client_port=65458-65459;server_port=5354-5355"))
.build(), .build());
/* messageBody= */ "");
List<String> messageLines = RtspMessageUtil.serializeResponse(response); List<String> messageLines = RtspMessageUtil.serializeResponse(response);
List<String> expectedLines = List<String> expectedLines =
...@@ -340,9 +339,7 @@ public final class RtspMessageUtilTest { ...@@ -340,9 +339,7 @@ public final class RtspMessageUtilTest {
public void serialize_failedResponse_succeeds() { public void serialize_failedResponse_succeeds() {
RtspResponse response = RtspResponse response =
new RtspResponse( new RtspResponse(
/* status= */ 454, /* status= */ 454, new RtspHeaders.Builder().add(RtspHeaders.CSEQ, "4").build());
new RtspHeaders.Builder().add(RtspHeaders.CSEQ, "4").build(),
/* messageBody= */ "");
List<String> messageLines = RtspMessageUtil.serializeResponse(response); List<String> messageLines = RtspMessageUtil.serializeResponse(response);
List<String> expectedLines = Arrays.asList("RTSP/1.0 454 Session Not Found", "cseq: 4", "", ""); List<String> expectedLines = Arrays.asList("RTSP/1.0 454 Session Not Found", "cseq: 4", "", "");
......
...@@ -23,7 +23,6 @@ import android.net.Uri; ...@@ -23,7 +23,6 @@ import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableMap;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
...@@ -31,31 +30,28 @@ import java.net.ServerSocket; ...@@ -31,31 +30,28 @@ import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.net.SocketException; import java.net.SocketException;
import java.util.List; import java.util.List;
import java.util.Map;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** The RTSP server. */ /** The RTSP server. */
public final class RtspServer implements Closeable { 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). */ /** Returns an RTSP OPTIONS {@link RtspResponse response}. */
private static final int STATUS_OK = 200; RtspResponse getOptionsResponse();
private static final int STATUS_METHOD_NOT_ALLOWED = 405; /** Returns an RTSP DESCRIBE {@link RtspResponse response}. */
default RtspResponse getDescribeResponse(Uri requestedUri) {
private static final String SESSION_DESCRIPTION = return RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED;
"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 final Thread listenerThread; private final Thread listenerThread;
/** Runs on the thread on which the constructor was called. */ /** Runs on the thread on which the constructor was called. */
private final Handler mainHandler; private final Handler mainHandler;
private final RtpPacketStreamDump rtpPacketStreamDump; private final ResponseProvider responseProvider;
private @MonotonicNonNull ServerSocket serverSocket; private @MonotonicNonNull ServerSocket serverSocket;
private @MonotonicNonNull RtspMessageChannel connectedClient; private @MonotonicNonNull RtspMessageChannel connectedClient;
...@@ -67,11 +63,11 @@ public final class RtspServer implements Closeable { ...@@ -67,11 +63,11 @@ public final class RtspServer implements Closeable {
* *
* <p>The constructor must be called on a {@link Looper} thread. * <p>The constructor must be called on a {@link Looper} thread.
*/ */
public RtspServer(RtpPacketStreamDump rtpPacketStreamDump) { public RtspServer(ResponseProvider responseProvider) {
this.rtpPacketStreamDump = rtpPacketStreamDump;
listenerThread = listenerThread =
new Thread(this::listenToIncomingRtspConnection, "ExoPlayerTest:RtspConnectionMonitor"); new Thread(this::listenToIncomingRtspConnection, "ExoPlayerTest:RtspConnectionMonitor");
mainHandler = Util.createHandlerForCurrentLooper(); mainHandler = Util.createHandlerForCurrentLooper();
this.responseProvider = responseProvider;
} }
/** /**
...@@ -123,54 +119,25 @@ public final class RtspServer implements Closeable { ...@@ -123,54 +119,25 @@ public final class RtspServer implements Closeable {
String cSeq = checkNotNull(request.headers.get(RtspHeaders.CSEQ)); String cSeq = checkNotNull(request.headers.get(RtspHeaders.CSEQ));
switch (request.method) { switch (request.method) {
case METHOD_OPTIONS: case METHOD_OPTIONS:
onOptionsRequestReceived(cSeq); sendResponse(responseProvider.getOptionsResponse(), cSeq);
break; break;
case METHOD_DESCRIBE: case METHOD_DESCRIBE:
onDescribeRequestReceived(request.uri, cSeq); sendResponse(responseProvider.getDescribeResponse(request.uri), cSeq);
break; break;
default: default:
sendErrorResponse(STATUS_METHOD_NOT_ALLOWED, cSeq); sendResponse(RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED, cSeq);
} }
} }
private void onOptionsRequestReceived(String cSeq) { private void sendResponse(RtspResponse response, 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);
connectedClient.send( connectedClient.send(
RtspMessageUtil.serializeResponse( RtspMessageUtil.serializeResponse(
new RtspResponse( new RtspResponse(
/* status= */ status, response.status,
/* headers= */ headerBuilder.build(), response.headers.buildUpon().add(RtspHeaders.CSEQ, cSeq).build(),
/* messageBody= */ messageBody))); response.messageBody)));
} }
} }
......
/*
* 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() {}
}
{
"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": [
]
}
{
"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": [
]
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment