Commit 1dddb8eb by claincly Committed by bachinger

Allow RtspServer read a RTP dump file.

#minor-release

PiperOrigin-RevId: 377001305
parent 08c882a6
/*
* 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 com.google.android.exoplayer2.ParserException;
import com.google.common.collect.ImmutableList;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/** A value wrapper for a dumped RTP packet stream. */
/* package */ class RtpPacketStreamDump {
/** The name of the RTP track. */
public final String trackName;
/** The sequence number of the first RTP packet in the dump file. */
public final int firstSequenceNumber;
/** The timestamp of the first RTP packet in the dump file. */
public final long firstTimestamp;
/** The interval between transmitting two consecutive RTP packets, in milliseconds. */
public final long transmissionIntervalMs;
/** The description of the dumped media in SDP(RFC2327) format. */
public final String mediaDescription;
/** A list of hex strings. Each hex string represents a binary RTP packet. */
public final ImmutableList<String> packets;
/**
* Parses a JSON string into an {@code RtpPacketStreamDump}.
*
* <p>The input JSON must include the following key-value pairs:
*
* <ul>
* <li>Key: "trackName", Value type: String. The name of the RTP track.
* <li>Key: "firstSequenceNumber", Value type: int. The sequence number of the first RTP packet
* in the dump file.
* <li>Key: "firstTimestamp", Value type: long. The timestamp of the first RTP packet in the
* dump file.
* <li>Key: "transmissionIntervalMs", Value type: long. The interval between transmitting two
* consecutive RTP packets, in milliseconds.
* <li>Key: "mediaDescription", Value type: String. The description of the dumped media in
* SDP(RFC2327) format.
* <li>Key: "packets", Value type: Array of hex strings. Each element is a hex string
* representing an RTP packet's binary data.
* </ul>
*
* @param jsonString The JSON string that contains the dumped RTP packets and metadata.
* @return The parsed {@code RtpDumpFile}.
* @throws ParserException If the argument does not contain all required key-value pairs, or there
* are incorrect values.
*/
public static RtpPacketStreamDump parse(String jsonString) throws ParserException {
try {
JSONObject jsonObject = new JSONObject(jsonString);
String trackName = jsonObject.getString("trackName");
int firstSequenceNumber = jsonObject.getInt("firstSequenceNumber");
long firstTimestamp = jsonObject.getLong("firstTimestamp");
long transmissionIntervalMs = jsonObject.getLong("transmitIntervalMs");
String mediaDescription = jsonObject.getString("mediaDescription");
ImmutableList.Builder<String> packetsBuilder = new ImmutableList.Builder<>();
JSONArray jsonPackets = jsonObject.getJSONArray("packets");
for (int i = 0; i < jsonPackets.length(); i++) {
packetsBuilder.add(jsonPackets.getString(i));
}
return new RtpPacketStreamDump(
trackName,
firstSequenceNumber,
firstTimestamp,
transmissionIntervalMs,
mediaDescription,
packetsBuilder.build());
} catch (JSONException e) {
throw ParserException.createForMalformedManifest(/* message= */ null, e);
}
}
private RtpPacketStreamDump(
String trackName,
int firstSequenceNumber,
long firstTimestamp,
long transmissionIntervalMs,
String mediaDescription,
ImmutableList<String> packets) {
this.trackName = trackName;
this.firstSequenceNumber = firstSequenceNumber;
this.firstTimestamp = firstTimestamp;
this.transmissionIntervalMs = transmissionIntervalMs;
this.mediaDescription = mediaDescription;
this.packets = ImmutableList.copyOf(packets);
}
}
......@@ -19,9 +19,11 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
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.robolectric.RobolectricUtil;
import com.google.android.exoplayer2.source.rtsp.RtspClient.SessionInfoListener;
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;
......@@ -39,8 +41,12 @@ public final class RtspClientTest {
private @MonotonicNonNull RtspServer rtspServer;
@Before
public void setUp() {
rtspServer = new RtspServer();
public void setUp() throws Exception {
rtspServer =
new RtspServer(
RtpPacketStreamDump.parse(
TestUtil.getString(
ApplicationProvider.getApplicationContext(), "media/rtsp/aac-dump.json")));
}
@After
......@@ -50,7 +56,7 @@ public final class RtspClientTest {
}
@Test
public void connectServerAndClient_withServerSupportsOnlyOptions_sessionTimelineRequestFails()
public void connectServerAndClient_withServerSupportsDescribe_updatesSessionTimeline()
throws Exception {
int serverRtspPortNumber = checkNotNull(rtspServer).startAndGetPortNumber();
......@@ -60,13 +66,13 @@ public final class RtspClientTest {
new SessionInfoListener() {
@Override
public void onSessionTimelineUpdated(
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks) {}
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks) {
sessionTimelineUpdateEventReceived.set(!tracks.isEmpty());
}
@Override
public void onSessionTimelineRequestFailed(
String message, @Nullable Throwable cause) {
sessionTimelineUpdateEventReceived.set(true);
}
String message, @Nullable Throwable cause) {}
},
/* userAgent= */ "ExoPlayer:RtspClientTest",
/* uri= */ Uri.parse(
......
......@@ -15,12 +15,15 @@
*/
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.util.Assertions.checkNotNull;
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;
......@@ -28,19 +31,32 @@ 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";
private static final String PUBLIC_SUPPORTED_METHODS = "OPTIONS, DESCRIBE";
/** RTSP error Method Not Allowed (RFC2326 Section 7.1.1). */
private static final int STATUS_OK = 200;
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";
private final Thread listenerThread;
/** Runs on the thread on which the constructor was called. */
private final Handler mainHandler;
private final RtpPacketStreamDump rtpPacketStreamDump;
private @MonotonicNonNull ServerSocket serverSocket;
private @MonotonicNonNull RtspMessageChannel connectedClient;
......@@ -51,7 +67,8 @@ public final class RtspServer implements Closeable {
*
* <p>The constructor must be called on a {@link Looper} thread.
*/
public RtspServer() {
public RtspServer(RtpPacketStreamDump rtpPacketStreamDump) {
this.rtpPacketStreamDump = rtpPacketStreamDump;
listenerThread =
new Thread(this::listenToIncomingRtspConnection, "ExoPlayerTest:RtspConnectionMonitor");
mainHandler = Util.createHandlerForCurrentLooper();
......@@ -98,34 +115,62 @@ public final class RtspServer implements Closeable {
private final class MessageListener implements RtspMessageChannel.MessageListener {
@Override
public void onRtspMessageReceived(List<String> message) {
mainHandler.post(() -> handleRtspMessage(message));
}
private void handleRtspMessage(List<String> message) {
RtspRequest request = RtspMessageUtil.parseRequest(message);
String cSeq = checkNotNull(request.headers.get(RtspHeaders.CSEQ));
switch (request.method) {
case METHOD_OPTIONS:
onOptionsRequestReceived(request);
onOptionsRequestReceived(cSeq);
break;
case METHOD_DESCRIBE:
onDescribeRequestReceived(request.uri, cSeq);
break;
default:
connectedClient.send(
RtspMessageUtil.serializeResponse(
new RtspResponse(
/* status= */ STATUS_METHOD_NOT_ALLOWED,
/* headers= */ new RtspHeaders.Builder()
.add(
RtspHeaders.CSEQ, checkNotNull(request.headers.get(RtspHeaders.CSEQ)))
.build(),
/* messageBody= */ "")));
sendErrorResponse(STATUS_METHOD_NOT_ALLOWED, cSeq);
}
}
private void onOptionsRequestReceived(RtspRequest request) {
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);
connectedClient.send(
RtspMessageUtil.serializeResponse(
new RtspResponse(
/* status= */ 200,
/* headers= */ new RtspHeaders.Builder()
.add(RtspHeaders.CSEQ, checkNotNull(request.headers.get(RtspHeaders.CSEQ)))
.add(RtspHeaders.PUBLIC, PUBLIC_SUPPORTED_METHODS)
.build(),
/* messageBody= */ "")));
/* status= */ status,
/* headers= */ headerBuilder.build(),
/* messageBody= */ messageBody)));
}
}
......
This diff could not be displayed because it is too large.
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