Commit 46645a9d by claincly Committed by Oliver Woodman

Add basic playback test.

In prepare_withSupportedTrack_sendsPlayRequest(), the DESCRIBE includes two
tracks, one AAC and one MP4A-LATM. The test is run until a PLAY is sent, and
asserts on only one SETUP is sent (for AAC).

In prepare_noSupportedTrack_throwsPreparationError(), the DESCRIBE includes one
track: one MP4A-LATM. This format is not supported at the moment, so the player
will throw out an error, on which we assert.
PiperOrigin-RevId: 380131458
parent 9c12d085
...@@ -543,20 +543,27 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -543,20 +543,27 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
private void onDescribeResponseReceived(RtspDescribeResponse response) { private void onDescribeResponseReceived(RtspDescribeResponse response) {
RtspSessionTiming sessionTiming = RtspSessionTiming.DEFAULT;
@Nullable @Nullable
String sessionRangeAttributeString = String sessionRangeAttributeString =
response.sessionDescription.attributes.get(SessionDescription.ATTR_RANGE); response.sessionDescription.attributes.get(SessionDescription.ATTR_RANGE);
if (sessionRangeAttributeString != null) {
try {
sessionTiming = RtspSessionTiming.parseTiming(sessionRangeAttributeString);
} catch (ParserException e) {
sessionInfoListener.onSessionTimelineRequestFailed("SDP format error.", /* cause= */ e);
return;
}
}
try { ImmutableList<RtspMediaTrack> tracks = buildTrackList(response.sessionDescription, uri);
sessionInfoListener.onSessionTimelineUpdated( if (tracks.isEmpty()) {
sessionRangeAttributeString != null sessionInfoListener.onSessionTimelineRequestFailed("No playable track.", /* cause= */ null);
? RtspSessionTiming.parseTiming(sessionRangeAttributeString) return;
: RtspSessionTiming.DEFAULT,
buildTrackList(response.sessionDescription, uri));
hasUpdatedTimelineAndTracks = true;
} catch (ParserException e) {
sessionInfoListener.onSessionTimelineRequestFailed("SDP format error.", /* cause= */ e);
} }
sessionInfoListener.onSessionTimelineUpdated(sessionTiming, tracks);
hasUpdatedTimelineAndTracks = true;
} }
private void onSetupResponseReceived(RtspSetupResponse response) { private void onSetupResponseReceived(RtspSetupResponse response) {
......
...@@ -228,7 +228,7 @@ public final class RtspMediaSource extends BaseMediaSource { ...@@ -228,7 +228,7 @@ public final class RtspMediaSource extends BaseMediaSource {
allocator, allocator,
rtpDataChannelFactory, rtpDataChannelFactory,
uri, uri,
(timing) -> { /* listener= */ timing -> {
timelineDurationUs = C.msToUs(timing.getDurationMs()); timelineDurationUs = C.msToUs(timing.getDurationMs());
timelineIsSeekable = !timing.isLive(); timelineIsSeekable = !timing.isLive();
timelineIsLive = timing.isLive(); timelineIsLive = timing.isLive();
......
/*
* 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.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player.Listener;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.robolectric.RobolectricUtil;
import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig;
import com.google.android.exoplayer2.testutil.FakeClock;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.internal.DoNotInstrument;
/** Playback testing for RTSP. */
@Config(sdk = 29)
@DoNotInstrument
@RunWith(AndroidJUnit4.class)
public final class RtspPlaybackTest {
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 RtpPacketStreamDump aacRtpPacketStreamDump;
// ExoPlayer does not support extracting MP4A-LATM RTP payload at the moment.
private RtpPacketStreamDump mp4aLatmRtpPacketStreamDump;
@Rule
public ShadowMediaCodecConfig mediaCodecConfig =
ShadowMediaCodecConfig.forAllSupportedMimeTypes();
@Before
public void setUp() throws Exception {
aacRtpPacketStreamDump = RtspTestUtils.readRtpPacketStreamDump("media/rtsp/aac-dump.json");
mp4aLatmRtpPacketStreamDump =
RtspTestUtils.readRtpPacketStreamDump("media/rtsp/mp4a-latm-dump.json");
}
@Test
public void prepare_withSupportedTrack_sendsPlayRequest() throws Exception {
ResponseProvider responseProvider =
new ResponseProvider(ImmutableList.of(aacRtpPacketStreamDump, mp4aLatmRtpPacketStreamDump));
try (RtspServer rtspServer = new RtspServer(responseProvider)) {
SimpleExoPlayer player = createSimpleExoPlayer(rtspServer.startAndGetPortNumber());
player.prepare();
RobolectricUtil.runMainLooperUntil(responseProvider::hasReceivedPlayRequest);
player.release();
// Only setup the supported track (aac).
ImmutableList<Uri> receivedSetupUris = responseProvider.getReceivedSetupUris();
assertThat(receivedSetupUris).hasSize(1);
assertThat(receivedSetupUris.get(0).toString()).contains(aacRtpPacketStreamDump.trackName);
}
}
@Test
public void prepare_noSupportedTrack_throwsPreparationError() throws Exception {
try (RtspServer rtspServer =
new RtspServer(new ResponseProvider(ImmutableList.of(mp4aLatmRtpPacketStreamDump)))) {
SimpleExoPlayer player = createSimpleExoPlayer(rtspServer.startAndGetPortNumber());
AtomicReference<Throwable> playbackError = new AtomicReference<>();
player.prepare();
player.addListener(
new Listener() {
@Override
public void onPlayerError(ExoPlaybackException error) {
playbackError.set(error);
}
});
RobolectricUtil.runMainLooperUntil(() -> playbackError.get() != null);
player.release();
assertThat(playbackError.get())
.hasCauseThat()
.hasMessageThat()
.contains("No playable track.");
}
}
private static SimpleExoPlayer createSimpleExoPlayer(int serverRtspPortNumber) {
SimpleExoPlayer player =
new SimpleExoPlayer.Builder(ApplicationProvider.getApplicationContext())
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
player.setMediaSource(
new RtspMediaSource.Factory()
.setForceUseRtpTcp(true)
.setUserAgent("ExoPlayer:PlaybackTest")
.createMediaSource(MediaItem.fromUri(RtspTestUtils.getTestUri(serverRtspPortNumber))));
return player;
}
private static final class ResponseProvider implements RtspServer.ResponseProvider {
private static final String SESSION_ID = "00000000";
private final ArrayList<Uri> receivedSetupUris;
private final ImmutableList<RtpPacketStreamDump> rtpPacketStreamDumps;
private boolean hasReceivedPlayRequest;
/**
* Creates a new instance.
*
* @param rtpPacketStreamDumps A list of {@link RtpPacketStreamDump}.
*/
public ResponseProvider(List<RtpPacketStreamDump> rtpPacketStreamDumps) {
this.rtpPacketStreamDumps = ImmutableList.copyOf(rtpPacketStreamDumps);
receivedSetupUris = new ArrayList<>();
}
/** Returns whether a PLAY request is received. */
public boolean hasReceivedPlayRequest() {
return hasReceivedPlayRequest;
}
/** Returns a list of the received SETUP requests' {@link Uri URIs}. */
public ImmutableList<Uri> getReceivedSetupUris() {
return ImmutableList.copyOf(receivedSetupUris);
}
// RtspServer.ResponseProvider implementation. Called on the main thread.
@Override
public RtspResponse getOptionsResponse() {
return new RtspResponse(
/* status= */ 200,
new RtspHeaders.Builder()
.add(RtspHeaders.PUBLIC, "OPTIONS, DESCRIBE, SETUP, PLAY")
.build());
}
@Override
public RtspResponse getDescribeResponse(Uri requestedUri) {
return RtspTestUtils.newDescribeResponseWithSdpMessage(
SESSION_DESCRIPTION, rtpPacketStreamDumps, requestedUri);
}
@Override
public RtspResponse getSetupResponse(Uri requestedUri, RtspHeaders headers) {
receivedSetupUris.add(requestedUri);
return new RtspResponse(
/* status= */ 200, headers.buildUpon().add(RtspHeaders.SESSION, SESSION_ID).build());
}
@Override
public RtspResponse getPlayResponse() {
hasReceivedPlayRequest = true;
return new RtspResponse(
/* status= */ 200,
new RtspHeaders.Builder()
.add(RtspHeaders.RTP_INFO, RtspTestUtils.getRtpInfoForDumps(rtpPacketStreamDumps))
.build());
}
}
}
...@@ -17,6 +17,8 @@ package com.google.android.exoplayer2.source.rtsp; ...@@ -17,6 +17,8 @@ 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_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.source.rtsp.RtspRequest.METHOD_PLAY;
import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_SETUP;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.net.Uri; import android.net.Uri;
...@@ -45,6 +47,16 @@ public final class RtspServer implements Closeable { ...@@ -45,6 +47,16 @@ public final class RtspServer implements Closeable {
default RtspResponse getDescribeResponse(Uri requestedUri) { default RtspResponse getDescribeResponse(Uri requestedUri) {
return RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED; return RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED;
} }
/** Returns an RTSP SETUP {@link RtspResponse response}. */
default RtspResponse getSetupResponse(Uri requestedUri, RtspHeaders headers) {
return RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED;
}
/** Returns an RTSP PLAY {@link RtspResponse response}. */
default RtspResponse getPlayResponse() {
return RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED;
}
} }
private final Thread listenerThread; private final Thread listenerThread;
...@@ -126,6 +138,14 @@ public final class RtspServer implements Closeable { ...@@ -126,6 +138,14 @@ public final class RtspServer implements Closeable {
sendResponse(responseProvider.getDescribeResponse(request.uri), cSeq); sendResponse(responseProvider.getDescribeResponse(request.uri), cSeq);
break; break;
case METHOD_SETUP:
sendResponse(responseProvider.getSetupResponse(request.uri, request.headers), cSeq);
break;
case METHOD_PLAY:
sendResponse(responseProvider.getPlayResponse(), cSeq);
break;
default: default:
sendResponse(RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED, cSeq); sendResponse(RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED, cSeq);
} }
......
...@@ -19,12 +19,17 @@ import android.net.Uri; ...@@ -19,12 +19,17 @@ import android.net.Uri;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import com.google.android.exoplayer2.testutil.TestUtil; 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.base.Joiner;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** Utility methods for RTSP tests. */ /** Utility methods for RTSP tests. */
/* package */ final class RtspTestUtils { /* package */ final class RtspTestUtils {
private static final String TEST_BASE_URI = "rtsp://localhost:%d/test";
private static final String RTP_TIME_FORMAT = "url=rtsp://localhost/test/%s;seq=%d;rtptime=%d";
/** RTSP error Method Not Allowed (RFC2326 Section 7.1.1). */ /** RTSP error Method Not Allowed (RFC2326 Section 7.1.1). */
public static final RtspResponse RTSP_ERROR_METHOD_NOT_ALLOWED = public static final RtspResponse RTSP_ERROR_METHOD_NOT_ALLOWED =
new RtspResponse(454, RtspHeaders.EMPTY); new RtspResponse(454, RtspHeaders.EMPTY);
...@@ -62,7 +67,20 @@ import java.util.List; ...@@ -62,7 +67,20 @@ import java.util.List;
/** Returns the test RTSP {@link Uri}. */ /** Returns the test RTSP {@link Uri}. */
public static Uri getTestUri(int serverRtspPortNumber) { public static Uri getTestUri(int serverRtspPortNumber) {
return Uri.parse(Util.formatInvariant("rtsp://localhost:%d/test", serverRtspPortNumber)); return Uri.parse(Util.formatInvariant(TEST_BASE_URI, serverRtspPortNumber));
}
public static String getRtpInfoForDumps(List<RtpPacketStreamDump> rtpPacketStreamDumps) {
ArrayList<String> rtpInfos = new ArrayList<>(rtpPacketStreamDumps.size());
for (RtpPacketStreamDump rtpPacketStreamDump : rtpPacketStreamDumps) {
rtpInfos.add(
Util.formatInvariant(
RTP_TIME_FORMAT,
rtpPacketStreamDump.trackName,
rtpPacketStreamDump.firstSequenceNumber,
rtpPacketStreamDump.firstTimestamp));
}
return Joiner.on(",").join(rtpInfos);
} }
private RtspTestUtils() {} private RtspTestUtils() {}
......
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