Move RtspClient creation into RtspPeriod.

RtspMediaSource uses the timeline update paradigm from ProgressiveMediaPeriod.

#minor-release

PiperOrigin-RevId: 378150758
This commit is contained in:
claincly 2021-06-08 15:40:50 +01:00 committed by Oliver Woodman
parent 22b126cac3
commit 1ca0efdd9b
5 changed files with 183 additions and 203 deletions

View File

@ -96,16 +96,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
private final SessionInfoListener sessionInfoListener;
private final PlaybackEventListener playbackEventListener;
private final Uri uri;
@Nullable private final RtspAuthUserInfo rtspAuthUserInfo;
@Nullable private final String userAgent;
private final String userAgent;
private final ArrayDeque<RtpLoadInfo> pendingSetupRtpLoadInfos;
// TODO(b/172331505) Add a timeout monitor for pending requests.
private final SparseArray<RtspRequest> pendingRequests;
private final MessageSender messageSender;
private RtspMessageChannel messageChannel;
private @MonotonicNonNull PlaybackEventListener playbackEventListener;
@Nullable private String sessionId;
@Nullable private KeepAliveMonitor keepAliveMonitor;
@Nullable private RtspAuthenticationInfo rtspAuthenticationInfo;
@ -123,12 +123,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* <p>Note: all method invocations must be made from the playback thread.
*
* @param sessionInfoListener The {@link SessionInfoListener}.
* @param userAgent The user agent that will be used if needed, or {@code null} for the fallback
* to use the default user agent of the underlying platform.
* @param playbackEventListener The {@link PlaybackEventListener}.
* @param userAgent The user agent.
* @param uri The RTSP playback URI.
*/
public RtspClient(SessionInfoListener sessionInfoListener, @Nullable String userAgent, Uri uri) {
public RtspClient(
SessionInfoListener sessionInfoListener,
PlaybackEventListener playbackEventListener,
String userAgent,
Uri uri) {
this.sessionInfoListener = sessionInfoListener;
this.playbackEventListener = playbackEventListener;
this.uri = RtspMessageUtil.removeUserInfo(uri);
this.rtspAuthUserInfo = RtspMessageUtil.parseUserInfo(uri);
this.userAgent = userAgent;
@ -157,17 +162,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
messageSender.sendOptionsRequest(uri, sessionId);
}
/** Sets the {@link PlaybackEventListener} to receive playback events. */
public void setPlaybackEventListener(PlaybackEventListener playbackEventListener) {
this.playbackEventListener = playbackEventListener;
}
/**
* Triggers RTSP SETUP requests after track selection.
*
* <p>A {@link PlaybackEventListener} must be set via {@link #setPlaybackEventListener} before
* calling this method. All selected tracks (represented by {@link RtpLoadInfo}) must have valid
* transport.
* <p>All selected tracks (represented by {@link RtpLoadInfo}) must have valid transport.
*
* @param loadInfos A list of selected tracks represented by {@link RtpLoadInfo}.
*/
@ -224,7 +222,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
receivedAuthorizationRequest = false;
rtspAuthenticationInfo = null;
} catch (IOException e) {
checkNotNull(playbackEventListener).onPlaybackError(new RtspPlaybackException(e));
playbackEventListener.onPlaybackError(new RtspPlaybackException(e));
}
}
@ -237,7 +235,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private void continueSetupRtspTrack() {
@Nullable RtpLoadInfo loadInfo = pendingSetupRtpLoadInfos.pollFirst();
if (loadInfo == null) {
checkNotNull(playbackEventListener).onRtspSetupCompleted();
playbackEventListener.onRtspSetupCompleted();
return;
}
messageSender.sendSetupRequest(loadInfo.getTrackUri(), loadInfo.getTransport(), sessionId);
@ -258,7 +256,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
if (hasUpdatedTimelineAndTracks) {
// Playback event listener must be non-null after timeline has been updated.
checkNotNull(playbackEventListener).onPlaybackError(playbackException);
playbackEventListener.onPlaybackError(playbackException);
} else {
sessionInfoListener.onSessionTimelineRequestFailed(nullToEmpty(error.getMessage()), error);
}
@ -373,9 +371,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
Uri uri) {
RtspHeaders.Builder headersBuilder = new RtspHeaders.Builder();
headersBuilder.add(RtspHeaders.CSEQ, String.valueOf(cSeq++));
if (userAgent != null) {
headersBuilder.add(RtspHeaders.USER_AGENT, userAgent);
}
if (sessionId != null) {
headersBuilder.add(RtspHeaders.SESSION, sessionId);
@ -574,8 +570,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
keepAliveMonitor.start();
}
checkNotNull(playbackEventListener)
.onPlaybackStarted(
playbackEventListener.onPlaybackStarted(
C.msToUs(response.sessionTiming.startTimeMs), response.trackTimingList);
pendingSeekPositionUs = C.TIME_UNSET;
}

View File

@ -42,6 +42,7 @@ import com.google.android.exoplayer2.source.SampleStream.ReadFlags;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
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.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection;
@ -62,22 +63,31 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** A {@link MediaPeriod} that loads an RTSP stream. */
/* package */ final class RtspMediaPeriod implements MediaPeriod {
/** Listener for information about the period. */
interface Listener {
/** Called when the {@link RtspSessionTiming} is available. */
void onSourceInfoRefreshed(RtspSessionTiming timing);
}
/** The maximum times to retry if the underlying data channel failed to bind. */
private static final int PORT_BINDING_MAX_RETRY_COUNT = 3;
private final Allocator allocator;
private final Handler handler;
private final InternalListener internalListener;
private final RtspClient rtspClient;
private final List<RtspLoaderWrapper> rtspLoaderWrappers;
private final List<RtpLoadInfo> selectedLoadInfos;
private final Listener listener;
private final RtpDataChannel.Factory rtpDataChannelFactory;
private @MonotonicNonNull Callback callback;
private @MonotonicNonNull ImmutableList<TrackGroup> trackGroups;
@Nullable private IOException preparationError;
@Nullable private RtspPlaybackException playbackException;
private long lastSeekPositionUs;
private long pendingSeekPositionUs;
private boolean loadingFinished;
private boolean released;
@ -90,29 +100,31 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* Creates an RTSP media period.
*
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
* @param rtspTracks A list of tracks in an RTSP playback session.
* @param rtspClient The {@link RtspClient} for the current RTSP playback.
* @param rtpDataChannelFactory A {@link RtpDataChannel.Factory} for {@link RtpDataChannel}.
* @param uri The RTSP playback {@link Uri}.
* @param listener A {@link Listener} to receive session information updates.
*/
public RtspMediaPeriod(
Allocator allocator,
List<RtspMediaTrack> rtspTracks,
RtspClient rtspClient,
RtpDataChannel.Factory rtpDataChannelFactory) {
RtpDataChannel.Factory rtpDataChannelFactory,
Uri uri,
Listener listener,
String userAgent) {
this.allocator = allocator;
this.rtpDataChannelFactory = rtpDataChannelFactory;
this.listener = listener;
handler = Util.createHandlerForCurrentLooper();
internalListener = new InternalListener();
rtspLoaderWrappers = new ArrayList<>(rtspTracks.size());
this.rtspClient = rtspClient;
this.rtspClient.setPlaybackEventListener(internalListener);
rtspClient =
new RtspClient(
/* sessionInfoListener= */ internalListener,
/* playbackEventListener= */ internalListener,
/* userAgent= */ userAgent,
/* uri= */ uri);
rtspLoaderWrappers = new ArrayList<>();
selectedLoadInfos = new ArrayList<>();
for (int i = 0; i < rtspTracks.size(); i++) {
RtspMediaTrack rtspMediaTrack = rtspTracks.get(i);
rtspLoaderWrappers.add(
new RtspLoaderWrapper(rtspMediaTrack, /* trackId= */ i, rtpDataChannelFactory));
}
selectedLoadInfos = new ArrayList<>(rtspTracks.size());
pendingSeekPositionUs = C.TIME_UNSET;
}
@ -121,6 +133,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
rtspLoaderWrappers.get(i).release();
}
Util.closeQuietly(rtspClient);
released = true;
}
@ -128,8 +141,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public void prepare(Callback callback, long positionUs) {
this.callback = callback;
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
rtspLoaderWrappers.get(i).startLoading();
try {
rtspClient.start();
} catch (IOException e) {
preparationError = e;
Util.closeQuietly(rtspClient);
}
}
@ -233,6 +249,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return positionUs;
}
lastSeekPositionUs = positionUs;
pendingSeekPositionUs = positionUs;
rtspClient.seekToUs(positionUs);
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
@ -256,14 +273,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return pendingSeekPositionUs;
}
long bufferedPositionUs = rtspLoaderWrappers.get(0).sampleQueue.getLargestQueuedTimestampUs();
for (int i = 1; i < rtspLoaderWrappers.size(); i++) {
bufferedPositionUs =
min(
bufferedPositionUs,
checkNotNull(rtspLoaderWrappers.get(i)).sampleQueue.getLargestQueuedTimestampUs());
boolean allLoaderWrappersAreCanceled = true;
long bufferedPositionUs = Long.MAX_VALUE;
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
RtspLoaderWrapper loaderWrapper = rtspLoaderWrappers.get(i);
if (!loaderWrapper.canceled) {
bufferedPositionUs = min(bufferedPositionUs, loaderWrapper.getBufferedPositionUs());
allLoaderWrappersAreCanceled = false;
}
return bufferedPositionUs;
}
return allLoaderWrappersAreCanceled || bufferedPositionUs == Long.MIN_VALUE
? lastSeekPositionUs
: bufferedPositionUs;
}
@Override
@ -386,6 +408,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
implements ExtractorOutput,
Loader.Callback<RtpDataLoadable>,
UpstreamFormatChangedListener,
SessionInfoListener,
PlaybackEventListener {
// ExtractorOutput implementation.
@ -515,7 +538,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Handles the {@link Loadable} whose {@link RtpDataChannel} timed out. */
private LoadErrorAction handleSocketTimeout(RtpDataLoadable loadable) {
// TODO(b/172331505) Allow for retry when loading is not ending.
if (getBufferedPositionUs() == Long.MIN_VALUE) {
if (getBufferedPositionUs() == 0) {
if (!isUsingRtpTcp) {
// Retry playback with TCP if no sample has been received so far, and we are not already
// using TCP. Retrying will setup new loadables, so will not retry with the current
@ -533,9 +556,27 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
break;
}
}
playbackException = new RtspPlaybackException("Unknown loadable timed out.");
return Loader.DONT_RETRY;
}
@Override
public void onSessionTimelineUpdated(
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks) {
for (int i = 0; i < tracks.size(); i++) {
RtspMediaTrack rtspMediaTrack = tracks.get(i);
RtspLoaderWrapper loaderWrapper =
new RtspLoaderWrapper(rtspMediaTrack, /* trackId= */ i, rtpDataChannelFactory);
loaderWrapper.startLoading();
rtspLoaderWrappers.add(loaderWrapper);
}
listener.onSourceInfoRefreshed(timing);
}
@Override
public void onSessionTimelineRequestFailed(String message, @Nullable Throwable cause) {
preparationError = cause == null ? new IOException(message) : new IOException(message, cause);
}
}
private void retryWithRtpTcp() {
@ -632,6 +673,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
sampleQueue.setUpstreamFormatChangeListener(internalListener);
}
/**
* Returns the largest buffered position in microseconds; or {@link Long#MIN_VALUE} if no sample
* has been queued.
*/
public long getBufferedPositionUs() {
return sampleQueue.getLargestQueuedTimestampUs();
}
/** Starts loading. */
public void startLoading() {
loader.startLoading(

View File

@ -16,30 +16,27 @@
package com.google.android.exoplayer2.source.rtsp;
import static com.google.android.exoplayer2.ExoPlayerLibraryInfo.VERSION_SLASHY;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.net.Uri;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManagerProvider;
import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.ForwardingTimeline;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
import com.google.android.exoplayer2.source.rtsp.RtspClient.SessionInfoListener;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** An Rtsp {@link MediaSource} */
public final class RtspMediaSource extends BaseMediaSource {
@ -63,8 +60,13 @@ public final class RtspMediaSource extends BaseMediaSource {
*/
public static final class Factory implements MediaSourceFactory {
private String userAgent;
private boolean forceUseRtpTcp;
public Factory() {
userAgent = ExoPlayerLibraryInfo.VERSION_SLASHY;
}
/**
* Sets whether to force using TCP as the default RTP transport.
*
@ -81,6 +83,17 @@ public final class RtspMediaSource extends BaseMediaSource {
return this;
}
/**
* Sets the user agent, the default value is {@link ExoPlayerLibraryInfo#VERSION_SLASHY}.
*
* @param userAgent The user agent.
* @return This Factory, for convenience.
*/
public Factory setUserAgent(String userAgent) {
this.userAgent = userAgent;
return this;
}
/** Does nothing. {@link RtspMediaSource} does not support DRM. */
@Override
public Factory setDrmSessionManagerProvider(
@ -149,7 +162,8 @@ public final class RtspMediaSource extends BaseMediaSource {
mediaItem,
forceUseRtpTcp
? new TransferRtpDataChannelFactory()
: new UdpDataSourceRtpDataChannelFactory());
: new UdpDataSourceRtpDataChannelFactory(),
userAgent);
}
}
@ -170,34 +184,32 @@ public final class RtspMediaSource extends BaseMediaSource {
private final MediaItem mediaItem;
private final RtpDataChannel.Factory rtpDataChannelFactory;
private @MonotonicNonNull RtspClient rtspClient;
private final String userAgent;
private final Uri uri;
@Nullable private ImmutableList<RtspMediaTrack> rtspMediaTracks;
@Nullable private IOException sourcePrepareException;
private long timelineDurationUs;
private boolean timelineIsSeekable;
private boolean timelineIsLive;
private boolean timelineIsPlaceholder;
private RtspMediaSource(MediaItem mediaItem, RtpDataChannel.Factory rtpDataChannelFactory) {
private RtspMediaSource(
MediaItem mediaItem, RtpDataChannel.Factory rtpDataChannelFactory, String userAgent) {
this.mediaItem = mediaItem;
this.rtpDataChannelFactory = rtpDataChannelFactory;
this.userAgent = userAgent;
this.uri = checkNotNull(this.mediaItem.playbackProperties).uri;
this.timelineDurationUs = C.TIME_UNSET;
this.timelineIsPlaceholder = true;
}
@Override
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
checkNotNull(mediaItem.playbackProperties);
try {
rtspClient =
new RtspClient(
new SessionInfoListenerImpl(),
/* userAgent= */ VERSION_SLASHY,
mediaItem.playbackProperties.uri);
rtspClient.start();
} catch (IOException e) {
sourcePrepareException = new RtspPlaybackException("RtspClient not opened.", e);
}
notifySourceInfoRefreshed();
}
@Override
protected void releaseSourceInternal() {
Util.closeQuietly(rtspClient);
// Do nothing.
}
@Override
@ -206,16 +218,24 @@ public final class RtspMediaSource extends BaseMediaSource {
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
if (sourcePrepareException != null) {
throw sourcePrepareException;
}
public void maybeThrowSourceInfoRefreshError() {
// Do nothing.
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
return new RtspMediaPeriod(
allocator, checkNotNull(rtspMediaTracks), checkNotNull(rtspClient), rtpDataChannelFactory);
allocator,
rtpDataChannelFactory,
uri,
(timing) -> {
timelineDurationUs = C.msToUs(timing.getDurationMs());
timelineIsSeekable = !timing.isLive();
timelineIsLive = timing.isLive();
timelineIsPlaceholder = false;
notifySourceInfoRefreshed();
},
userAgent);
}
@Override
@ -223,28 +243,36 @@ public final class RtspMediaSource extends BaseMediaSource {
((RtspMediaPeriod) mediaPeriod).release();
}
private final class SessionInfoListenerImpl implements SessionInfoListener {
@Override
public void onSessionTimelineUpdated(
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks) {
rtspMediaTracks = tracks;
refreshSourceInfo(
// Internal methods.
private void notifySourceInfoRefreshed() {
Timeline timeline =
new SinglePeriodTimeline(
/* durationUs= */ C.msToUs(timing.getDurationMs()),
/* isSeekable= */ !timing.isLive(),
timelineDurationUs,
timelineIsSeekable,
/* isDynamic= */ false,
/* useLiveConfiguration= */ timing.isLive(),
/* useLiveConfiguration= */ timelineIsLive,
/* manifest= */ null,
mediaItem));
mediaItem);
if (timelineIsPlaceholder) {
timeline =
new ForwardingTimeline(timeline) {
@Override
public Window getWindow(
int windowIndex, Window window, long defaultPositionProjectionUs) {
super.getWindow(windowIndex, window, defaultPositionProjectionUs);
window.isPlaceholder = true;
return window;
}
@Override
public void onSessionTimelineRequestFailed(String message, @Nullable Throwable cause) {
if (cause == null) {
sourcePrepareException = new RtspPlaybackException(message);
} else {
sourcePrepareException = new RtspPlaybackException(message, castNonNull(cause));
}
}
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
super.getPeriod(periodIndex, period, setIds);
period.isPlaceholder = true;
return period;
}
};
}
refreshSourceInfo(timeline);
}
}

View File

@ -22,7 +22,9 @@ 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.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;
@ -76,6 +78,17 @@ public final class RtspClientTest {
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) {}
},
/* userAgent= */ "ExoPlayer:RtspClientTest",
/* uri= */ Uri.parse(
Util.formatInvariant("rtsp://localhost:%d/test", serverRtspPortNumber)));

View File

@ -1,105 +0,0 @@
/*
* 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.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.common.collect.ImmutableList;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.internal.DoNotInstrument;
/** Unit test for {@link RtspMediaPeriod}. */
@RunWith(AndroidJUnit4.class)
@DoNotInstrument
public class RtspMediaPeriodTest {
private static final RtspClient PLACEHOLDER_RTSP_CLIENT =
new RtspClient(
new RtspClient.SessionInfoListener() {
@Override
public void onSessionTimelineUpdated(
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks) {}
@Override
public void onSessionTimelineRequestFailed(String message, @Nullable Throwable cause) {}
},
/* userAgent= */ null,
Uri.EMPTY);
@Test
public void prepare_startsLoading() throws Exception {
RtspMediaPeriod rtspMediaPeriod =
new RtspMediaPeriod(
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
ImmutableList.of(
new RtspMediaTrack(
new MediaDescription.Builder(
/* mediaType= */ MediaDescription.MEDIA_TYPE_VIDEO,
/* port= */ 0,
/* transportProtocol= */ MediaDescription.RTP_AVP_PROFILE,
/* payloadType= */ 96)
.setConnection("IN IP4 0.0.0.0")
.setBitrate(500_000)
.addAttribute(SessionDescription.ATTR_RTPMAP, "96 H264/90000")
.addAttribute(
SessionDescription.ATTR_FMTP,
"96 packetization-mode=1;profile-level-id=64001F;sprop-parameter-sets=Z2QAH6zZQPARabIAAAMACAAAAwGcHjBjLA==,aOvjyyLA")
.addAttribute(SessionDescription.ATTR_CONTROL, "track1")
.build(),
Uri.parse("rtsp://localhost/test"))),
PLACEHOLDER_RTSP_CLIENT,
new UdpDataSourceRtpDataChannelFactory());
AtomicBoolean prepareCallbackCalled = new AtomicBoolean(false);
rtspMediaPeriod.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);
runMainLooperUntil(prepareCallbackCalled::get);
rtspMediaPeriod.release();
}
@Test
public void getBufferedPositionUs_withNoRtspMediaTracks_returnsEndOfSource() {
RtspMediaPeriod rtspMediaPeriod =
new RtspMediaPeriod(
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
ImmutableList.of(),
PLACEHOLDER_RTSP_CLIENT,
new UdpDataSourceRtpDataChannelFactory());
assertThat(rtspMediaPeriod.getBufferedPositionUs()).isEqualTo(C.TIME_END_OF_SOURCE);
}
}