mirror of
https://github.com/samsonjs/media.git
synced 2026-03-31 10:25:48 +00:00
Allow customizing the RtspServer using RtspServerResponseProvider.
PiperOrigin-RevId: 379282201
This commit is contained in:
parent
581e543d39
commit
8cc1328d89
11 changed files with 473 additions and 91 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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= */ "");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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", "", "");
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {}
|
||||
}
|
||||
9
testdata/src/test/assets/media/rtsp/h264-dump.json
vendored
Normal file
9
testdata/src/test/assets/media/rtsp/h264-dump.json
vendored
Normal 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": [
|
||||
]
|
||||
}
|
||||
9
testdata/src/test/assets/media/rtsp/mp4a-latm-dump.json
vendored
Normal file
9
testdata/src/test/assets/media/rtsp/mp4a-latm-dump.json
vendored
Normal 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": [
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue