Commit b05e8f50 by claincly Committed by Oliver Woodman

Add RTP streaming test to playback test.

The test prepare_withSupportedTrack_playsTrackUntilEnded

- sets up the supported AAC track with the RTSP server;
- uses RtpPacketTransmitter to send RTP packets from the server to the client;
- runs the player until the playback has ended, and
- asserts on the data RTSP has received and queued to the SampleQueue.

In the test, it was necessary to create a FakeUdpDataSourceRtpDataChannel. The
reason we cannot reuse TransferRtpDataChannel is, we rely on BlockingQueue.poll
timeout to identify the end of an RTSP stream, but the time out mechanism is
unstable in Robolectric. For example, when the timeout is set to 8,000 ms, the
actual timeout occasionally happens after 2,000,000 ms (in FakeClock).

PiperOrigin-RevId: 380528710
parent 607fa8bf
...@@ -486,13 +486,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -486,13 +486,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public void onPlaybackStarted( public void onPlaybackStarted(
long startPositionUs, ImmutableList<RtspTrackTiming> trackTimingList) { long startPositionUs, ImmutableList<RtspTrackTiming> trackTimingList) {
// Validate that the trackTimingList contains timings for the selected tracks. // Validate that the trackTimingList contains timings for the selected tracks.
ArrayList<Uri> trackUrisWithTiming = new ArrayList<>(trackTimingList.size()); ArrayList<String> trackUrisWithTiming = new ArrayList<>(trackTimingList.size());
for (int i = 0; i < trackTimingList.size(); i++) { for (int i = 0; i < trackTimingList.size(); i++) {
trackUrisWithTiming.add(trackTimingList.get(i).uri); trackUrisWithTiming.add(checkNotNull(trackTimingList.get(i).uri.getPath()));
} }
for (int i = 0; i < selectedLoadInfos.size(); i++) { for (int i = 0; i < selectedLoadInfos.size(); i++) {
RtpLoadInfo loadInfo = selectedLoadInfos.get(i); RtpLoadInfo loadInfo = selectedLoadInfos.get(i);
if (!trackUrisWithTiming.contains(loadInfo.getTrackUri())) { if (!trackUrisWithTiming.contains(loadInfo.getTrackUri().getPath())) {
playbackException = playbackException =
new RtspPlaybackException( new RtspPlaybackException(
"Server did not provide timing for track " + loadInfo.getTrackUri()); "Server did not provide timing for track " + loadInfo.getTrackUri());
...@@ -556,8 +556,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -556,8 +556,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
RtspMediaTrack rtspMediaTrack = tracks.get(i); RtspMediaTrack rtspMediaTrack = tracks.get(i);
RtspLoaderWrapper loaderWrapper = RtspLoaderWrapper loaderWrapper =
new RtspLoaderWrapper(rtspMediaTrack, /* trackId= */ i, rtpDataChannelFactory); new RtspLoaderWrapper(rtspMediaTrack, /* trackId= */ i, rtpDataChannelFactory);
loaderWrapper.startLoading();
rtspLoaderWrappers.add(loaderWrapper); rtspLoaderWrappers.add(loaderWrapper);
loaderWrapper.startLoading();
} }
listener.onSourceInfoRefreshed(timing); listener.onSourceInfoRefreshed(timing);
......
...@@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull; ...@@ -20,6 +20,7 @@ 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.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.MediaItem;
...@@ -192,7 +193,8 @@ public final class RtspMediaSource extends BaseMediaSource { ...@@ -192,7 +193,8 @@ public final class RtspMediaSource extends BaseMediaSource {
private boolean timelineIsLive; private boolean timelineIsLive;
private boolean timelineIsPlaceholder; private boolean timelineIsPlaceholder;
private RtspMediaSource( @VisibleForTesting
/* package */ RtspMediaSource(
MediaItem mediaItem, RtpDataChannel.Factory rtpDataChannelFactory, String userAgent) { MediaItem mediaItem, RtpDataChannel.Factory rtpDataChannelFactory, String userAgent) {
this.mediaItem = mediaItem; this.mediaItem = mediaItem;
this.rtpDataChannelFactory = rtpDataChannelFactory; this.rtpDataChannelFactory = rtpDataChannelFactory;
......
/*
* 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.source.rtsp.RtspMessageChannel.InterleavedBinaryDataListener;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
/** Transmits media RTP packets periodically. */
/* package */ final class RtpPacketTransmitter {
private static final byte[] END_OF_STREAM = new byte[0];
private final ImmutableList<String> packets;
private final HandlerWrapper transmissionHandler;
private final long transmissionIntervalMs;
private RtspMessageChannel.InterleavedBinaryDataListener binaryDataListener;
private int packetIndex;
private volatile boolean isTransmitting;
/**
* Creates a new instance.
*
* @param rtpPacketStreamDump The {@link RtpPacketStreamDump} to provide RTP packets.
* @param clock The {@link Clock} to use.
*/
public RtpPacketTransmitter(RtpPacketStreamDump rtpPacketStreamDump, Clock clock) {
this.packets = ImmutableList.copyOf(rtpPacketStreamDump.packets);
this.transmissionHandler =
clock.createHandler(Util.getCurrentOrMainLooper(), /* callback= */ null);
this.transmissionIntervalMs = rtpPacketStreamDump.transmissionIntervalMs;
}
/**
* Starts transmitting binary data to the {@link InterleavedBinaryDataListener}.
*
* <p>Calling this method after starting the transmission has no effect.
*/
public void startTransmitting(InterleavedBinaryDataListener binaryDataListener) {
if (isTransmitting) {
return;
}
this.binaryDataListener = binaryDataListener;
packetIndex = 0;
isTransmitting = true;
transmissionHandler.post(this::transmitNextPacket);
}
/** Stops transmitting, if transmitting has started. */
private void stopTransmitting() {
if (!isTransmitting) {
return;
}
signalEndOfStream();
transmissionHandler.removeCallbacksAndMessages(/* token= */ null);
isTransmitting = false;
}
private void transmitNextPacket() {
if (packetIndex == packets.size()) {
stopTransmitting();
return;
}
byte[] data = Util.getBytesFromHexString(packets.get(packetIndex++));
binaryDataListener.onInterleavedBinaryDataReceived(data);
transmissionHandler.postDelayed(this::transmitNextPacket, transmissionIntervalMs);
}
private void signalEndOfStream() {
binaryDataListener.onInterleavedBinaryDataReceived(END_OF_STREAM);
}
}
...@@ -19,6 +19,7 @@ import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_DESCR ...@@ -19,6 +19,7 @@ import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_DESCR
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_PLAY;
import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_SETUP; import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_SETUP;
import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_TEARDOWN;
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;
...@@ -57,6 +58,11 @@ public final class RtspServer implements Closeable { ...@@ -57,6 +58,11 @@ public final class RtspServer implements Closeable {
default RtspResponse getPlayResponse() { default RtspResponse getPlayResponse() {
return RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED; return RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED;
} }
/** Returns an RTSP TEARDOWN {@link RtspResponse response}. */
default RtspResponse getTearDownResponse() {
return new RtspResponse(/* status= */ 200, RtspHeaders.EMPTY);
}
} }
private final Thread listenerThread; private final Thread listenerThread;
...@@ -74,6 +80,8 @@ public final class RtspServer implements Closeable { ...@@ -74,6 +80,8 @@ public final class RtspServer implements Closeable {
* Creates a new instance. * Creates a new instance.
* *
* <p>The constructor must be called on a {@link Looper} thread. * <p>The constructor must be called on a {@link Looper} thread.
*
* @param responseProvider A {@link ResponseProvider}.
*/ */
public RtspServer(ResponseProvider responseProvider) { public RtspServer(ResponseProvider responseProvider) {
listenerThread = listenerThread =
...@@ -146,6 +154,10 @@ public final class RtspServer implements Closeable { ...@@ -146,6 +154,10 @@ public final class RtspServer implements Closeable {
sendResponse(responseProvider.getPlayResponse(), cSeq); sendResponse(responseProvider.getPlayResponse(), cSeq);
break; break;
case METHOD_TEARDOWN:
sendResponse(responseProvider.getTearDownResponse(), cSeq);
break;
default: default:
sendResponse(RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED, cSeq); sendResponse(RtspTestUtils.RTSP_ERROR_METHOD_NOT_ALLOWED, cSeq);
} }
......
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