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; ...@@ -19,9 +19,11 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.robolectric.RobolectricUtil; import com.google.android.exoplayer2.robolectric.RobolectricUtil;
import com.google.android.exoplayer2.source.rtsp.RtspClient.SessionInfoListener; 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.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
...@@ -39,8 +41,12 @@ public final class RtspClientTest { ...@@ -39,8 +41,12 @@ public final class RtspClientTest {
private @MonotonicNonNull RtspServer rtspServer; private @MonotonicNonNull RtspServer rtspServer;
@Before @Before
public void setUp() { public void setUp() throws Exception {
rtspServer = new RtspServer(); rtspServer =
new RtspServer(
RtpPacketStreamDump.parse(
TestUtil.getString(
ApplicationProvider.getApplicationContext(), "media/rtsp/aac-dump.json")));
} }
@After @After
...@@ -50,7 +56,7 @@ public final class RtspClientTest { ...@@ -50,7 +56,7 @@ public final class RtspClientTest {
} }
@Test @Test
public void connectServerAndClient_withServerSupportsOnlyOptions_sessionTimelineRequestFails() public void connectServerAndClient_withServerSupportsDescribe_updatesSessionTimeline()
throws Exception { throws Exception {
int serverRtspPortNumber = checkNotNull(rtspServer).startAndGetPortNumber(); int serverRtspPortNumber = checkNotNull(rtspServer).startAndGetPortNumber();
...@@ -60,13 +66,13 @@ public final class RtspClientTest { ...@@ -60,13 +66,13 @@ public final class RtspClientTest {
new SessionInfoListener() { new SessionInfoListener() {
@Override @Override
public void onSessionTimelineUpdated( public void onSessionTimelineUpdated(
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks) {} RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks) {
sessionTimelineUpdateEventReceived.set(!tracks.isEmpty());
}
@Override @Override
public void onSessionTimelineRequestFailed( public void onSessionTimelineRequestFailed(
String message, @Nullable Throwable cause) { String message, @Nullable Throwable cause) {}
sessionTimelineUpdateEventReceived.set(true);
}
}, },
/* userAgent= */ "ExoPlayer:RtspClientTest", /* userAgent= */ "ExoPlayer:RtspClientTest",
/* uri= */ Uri.parse( /* uri= */ Uri.parse(
......
...@@ -15,12 +15,15 @@ ...@@ -15,12 +15,15 @@
*/ */
package com.google.android.exoplayer2.source.rtsp; 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_OPTIONS;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
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;
...@@ -28,19 +31,32 @@ import java.net.ServerSocket; ...@@ -28,19 +31,32 @@ 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"; private static final String PUBLIC_SUPPORTED_METHODS = "OPTIONS, DESCRIBE";
/** RTSP error Method Not Allowed (RFC2326 Section 7.1.1). */ /** 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 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; 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 @MonotonicNonNull ServerSocket serverSocket; private @MonotonicNonNull ServerSocket serverSocket;
private @MonotonicNonNull RtspMessageChannel connectedClient; private @MonotonicNonNull RtspMessageChannel connectedClient;
...@@ -51,7 +67,8 @@ public final class RtspServer implements Closeable { ...@@ -51,7 +67,8 @@ 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() { public RtspServer(RtpPacketStreamDump rtpPacketStreamDump) {
this.rtpPacketStreamDump = rtpPacketStreamDump;
listenerThread = listenerThread =
new Thread(this::listenToIncomingRtspConnection, "ExoPlayerTest:RtspConnectionMonitor"); new Thread(this::listenToIncomingRtspConnection, "ExoPlayerTest:RtspConnectionMonitor");
mainHandler = Util.createHandlerForCurrentLooper(); mainHandler = Util.createHandlerForCurrentLooper();
...@@ -98,34 +115,62 @@ public final class RtspServer implements Closeable { ...@@ -98,34 +115,62 @@ public final class RtspServer implements Closeable {
private final class MessageListener implements RtspMessageChannel.MessageListener { private final class MessageListener implements RtspMessageChannel.MessageListener {
@Override @Override
public void onRtspMessageReceived(List<String> message) { public void onRtspMessageReceived(List<String> message) {
mainHandler.post(() -> handleRtspMessage(message));
}
private void handleRtspMessage(List<String> message) {
RtspRequest request = RtspMessageUtil.parseRequest(message); RtspRequest request = RtspMessageUtil.parseRequest(message);
String cSeq = checkNotNull(request.headers.get(RtspHeaders.CSEQ));
switch (request.method) { switch (request.method) {
case METHOD_OPTIONS: case METHOD_OPTIONS:
onOptionsRequestReceived(request); onOptionsRequestReceived(cSeq);
break;
case METHOD_DESCRIBE:
onDescribeRequestReceived(request.uri, cSeq);
break; break;
default: default:
connectedClient.send( sendErrorResponse(STATUS_METHOD_NOT_ALLOWED, cSeq);
RtspMessageUtil.serializeResponse(
new RtspResponse(
/* status= */ STATUS_METHOD_NOT_ALLOWED,
/* headers= */ new RtspHeaders.Builder()
.add(
RtspHeaders.CSEQ, checkNotNull(request.headers.get(RtspHeaders.CSEQ)))
.build(),
/* messageBody= */ "")));
} }
} }
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( connectedClient.send(
RtspMessageUtil.serializeResponse( RtspMessageUtil.serializeResponse(
new RtspResponse( new RtspResponse(
/* status= */ 200, /* status= */ status,
/* headers= */ new RtspHeaders.Builder() /* headers= */ headerBuilder.build(),
.add(RtspHeaders.CSEQ, checkNotNull(request.headers.get(RtspHeaders.CSEQ))) /* messageBody= */ messageBody)));
.add(RtspHeaders.PUBLIC, PUBLIC_SUPPORTED_METHODS)
.build(),
/* 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