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 SessionInfoListener sessionInfoListener;
private final PlaybackEventListener playbackEventListener;
private final Uri uri; private final Uri uri;
@Nullable private final RtspAuthUserInfo rtspAuthUserInfo; @Nullable private final RtspAuthUserInfo rtspAuthUserInfo;
@Nullable private final String userAgent; private final String userAgent;
private final ArrayDeque<RtpLoadInfo> pendingSetupRtpLoadInfos; private final ArrayDeque<RtpLoadInfo> pendingSetupRtpLoadInfos;
// TODO(b/172331505) Add a timeout monitor for pending requests. // TODO(b/172331505) Add a timeout monitor for pending requests.
private final SparseArray<RtspRequest> pendingRequests; private final SparseArray<RtspRequest> pendingRequests;
private final MessageSender messageSender; private final MessageSender messageSender;
private RtspMessageChannel messageChannel; private RtspMessageChannel messageChannel;
private @MonotonicNonNull PlaybackEventListener playbackEventListener;
@Nullable private String sessionId; @Nullable private String sessionId;
@Nullable private KeepAliveMonitor keepAliveMonitor; @Nullable private KeepAliveMonitor keepAliveMonitor;
@Nullable private RtspAuthenticationInfo rtspAuthenticationInfo; @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. * <p>Note: all method invocations must be made from the playback thread.
* *
* @param sessionInfoListener The {@link SessionInfoListener}. * @param sessionInfoListener The {@link SessionInfoListener}.
* @param userAgent The user agent that will be used if needed, or {@code null} for the fallback * @param playbackEventListener The {@link PlaybackEventListener}.
* to use the default user agent of the underlying platform. * @param userAgent The user agent.
* @param uri The RTSP playback URI. * @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.sessionInfoListener = sessionInfoListener;
this.playbackEventListener = playbackEventListener;
this.uri = RtspMessageUtil.removeUserInfo(uri); this.uri = RtspMessageUtil.removeUserInfo(uri);
this.rtspAuthUserInfo = RtspMessageUtil.parseUserInfo(uri); this.rtspAuthUserInfo = RtspMessageUtil.parseUserInfo(uri);
this.userAgent = userAgent; this.userAgent = userAgent;
@ -157,17 +162,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
messageSender.sendOptionsRequest(uri, sessionId); 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. * Triggers RTSP SETUP requests after track selection.
* *
* <p>A {@link PlaybackEventListener} must be set via {@link #setPlaybackEventListener} before * <p>All selected tracks (represented by {@link RtpLoadInfo}) must have valid transport.
* calling this method. All selected tracks (represented by {@link RtpLoadInfo}) must have valid
* transport.
* *
* @param loadInfos A list of selected tracks represented by {@link RtpLoadInfo}. * @param loadInfos A list of selected tracks represented by {@link RtpLoadInfo}.
*/ */
@ -224,7 +222,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
receivedAuthorizationRequest = false; receivedAuthorizationRequest = false;
rtspAuthenticationInfo = null; rtspAuthenticationInfo = null;
} catch (IOException e) { } 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() { private void continueSetupRtspTrack() {
@Nullable RtpLoadInfo loadInfo = pendingSetupRtpLoadInfos.pollFirst(); @Nullable RtpLoadInfo loadInfo = pendingSetupRtpLoadInfos.pollFirst();
if (loadInfo == null) { if (loadInfo == null) {
checkNotNull(playbackEventListener).onRtspSetupCompleted(); playbackEventListener.onRtspSetupCompleted();
return; return;
} }
messageSender.sendSetupRequest(loadInfo.getTrackUri(), loadInfo.getTransport(), sessionId); messageSender.sendSetupRequest(loadInfo.getTrackUri(), loadInfo.getTransport(), sessionId);
@ -258,7 +256,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
if (hasUpdatedTimelineAndTracks) { if (hasUpdatedTimelineAndTracks) {
// Playback event listener must be non-null after timeline has been updated. // Playback event listener must be non-null after timeline has been updated.
checkNotNull(playbackEventListener).onPlaybackError(playbackException); playbackEventListener.onPlaybackError(playbackException);
} else { } else {
sessionInfoListener.onSessionTimelineRequestFailed(nullToEmpty(error.getMessage()), error); sessionInfoListener.onSessionTimelineRequestFailed(nullToEmpty(error.getMessage()), error);
} }
@ -373,9 +371,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
Uri uri) { Uri uri) {
RtspHeaders.Builder headersBuilder = new RtspHeaders.Builder(); RtspHeaders.Builder headersBuilder = new RtspHeaders.Builder();
headersBuilder.add(RtspHeaders.CSEQ, String.valueOf(cSeq++)); headersBuilder.add(RtspHeaders.CSEQ, String.valueOf(cSeq++));
if (userAgent != null) {
headersBuilder.add(RtspHeaders.USER_AGENT, userAgent); headersBuilder.add(RtspHeaders.USER_AGENT, userAgent);
}
if (sessionId != null) { if (sessionId != null) {
headersBuilder.add(RtspHeaders.SESSION, sessionId); headersBuilder.add(RtspHeaders.SESSION, sessionId);
@ -574,8 +570,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
keepAliveMonitor.start(); keepAliveMonitor.start();
} }
checkNotNull(playbackEventListener) playbackEventListener.onPlaybackStarted(
.onPlaybackStarted(
C.msToUs(response.sessionTiming.startTimeMs), response.trackTimingList); C.msToUs(response.sessionTiming.startTimeMs), response.trackTimingList);
pendingSeekPositionUs = C.TIME_UNSET; 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.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.rtsp.RtspClient.PlaybackEventListener; 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.source.rtsp.RtspMediaSource.RtspPlaybackException;
import com.google.android.exoplayer2.trackselection.ExoTrackSelection; import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection; 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. */ /** A {@link MediaPeriod} that loads an RTSP stream. */
/* package */ final class RtspMediaPeriod implements MediaPeriod { /* 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. */ /** The maximum times to retry if the underlying data channel failed to bind. */
private static final int PORT_BINDING_MAX_RETRY_COUNT = 3; private static final int PORT_BINDING_MAX_RETRY_COUNT = 3;
private final Allocator allocator; private final Allocator allocator;
private final Handler handler; private final Handler handler;
private final InternalListener internalListener; private final InternalListener internalListener;
private final RtspClient rtspClient; private final RtspClient rtspClient;
private final List<RtspLoaderWrapper> rtspLoaderWrappers; private final List<RtspLoaderWrapper> rtspLoaderWrappers;
private final List<RtpLoadInfo> selectedLoadInfos; private final List<RtpLoadInfo> selectedLoadInfos;
private final Listener listener;
private final RtpDataChannel.Factory rtpDataChannelFactory;
private @MonotonicNonNull Callback callback; private @MonotonicNonNull Callback callback;
private @MonotonicNonNull ImmutableList<TrackGroup> trackGroups; private @MonotonicNonNull ImmutableList<TrackGroup> trackGroups;
@Nullable private IOException preparationError; @Nullable private IOException preparationError;
@Nullable private RtspPlaybackException playbackException; @Nullable private RtspPlaybackException playbackException;
private long lastSeekPositionUs;
private long pendingSeekPositionUs; private long pendingSeekPositionUs;
private boolean loadingFinished; private boolean loadingFinished;
private boolean released; private boolean released;
@ -90,29 +100,31 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* Creates an RTSP media period. * Creates an RTSP media period.
* *
* @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @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 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( public RtspMediaPeriod(
Allocator allocator, Allocator allocator,
List<RtspMediaTrack> rtspTracks, RtpDataChannel.Factory rtpDataChannelFactory,
RtspClient rtspClient, Uri uri,
RtpDataChannel.Factory rtpDataChannelFactory) { Listener listener,
String userAgent) {
this.allocator = allocator; this.allocator = allocator;
this.rtpDataChannelFactory = rtpDataChannelFactory;
this.listener = listener;
handler = Util.createHandlerForCurrentLooper(); handler = Util.createHandlerForCurrentLooper();
internalListener = new InternalListener(); internalListener = new InternalListener();
rtspLoaderWrappers = new ArrayList<>(rtspTracks.size()); rtspClient =
this.rtspClient = rtspClient; new RtspClient(
this.rtspClient.setPlaybackEventListener(internalListener); /* 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; pendingSeekPositionUs = C.TIME_UNSET;
} }
@ -121,6 +133,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
for (int i = 0; i < rtspLoaderWrappers.size(); i++) { for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
rtspLoaderWrappers.get(i).release(); rtspLoaderWrappers.get(i).release();
} }
Util.closeQuietly(rtspClient);
released = true; released = true;
} }
@ -128,8 +141,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public void prepare(Callback callback, long positionUs) { public void prepare(Callback callback, long positionUs) {
this.callback = callback; this.callback = callback;
for (int i = 0; i < rtspLoaderWrappers.size(); i++) { try {
rtspLoaderWrappers.get(i).startLoading(); rtspClient.start();
} catch (IOException e) {
preparationError = e;
Util.closeQuietly(rtspClient);
} }
} }
@ -233,6 +249,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return positionUs; return positionUs;
} }
lastSeekPositionUs = positionUs;
pendingSeekPositionUs = positionUs; pendingSeekPositionUs = positionUs;
rtspClient.seekToUs(positionUs); rtspClient.seekToUs(positionUs);
for (int i = 0; i < rtspLoaderWrappers.size(); i++) { for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
@ -256,14 +273,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return pendingSeekPositionUs; return pendingSeekPositionUs;
} }
long bufferedPositionUs = rtspLoaderWrappers.get(0).sampleQueue.getLargestQueuedTimestampUs(); boolean allLoaderWrappersAreCanceled = true;
for (int i = 1; i < rtspLoaderWrappers.size(); i++) { long bufferedPositionUs = Long.MAX_VALUE;
bufferedPositionUs = for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
min( RtspLoaderWrapper loaderWrapper = rtspLoaderWrappers.get(i);
bufferedPositionUs, if (!loaderWrapper.canceled) {
checkNotNull(rtspLoaderWrappers.get(i)).sampleQueue.getLargestQueuedTimestampUs()); bufferedPositionUs = min(bufferedPositionUs, loaderWrapper.getBufferedPositionUs());
allLoaderWrappersAreCanceled = false;
} }
return bufferedPositionUs; }
return allLoaderWrappersAreCanceled || bufferedPositionUs == Long.MIN_VALUE
? lastSeekPositionUs
: bufferedPositionUs;
} }
@Override @Override
@ -386,6 +408,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
implements ExtractorOutput, implements ExtractorOutput,
Loader.Callback<RtpDataLoadable>, Loader.Callback<RtpDataLoadable>,
UpstreamFormatChangedListener, UpstreamFormatChangedListener,
SessionInfoListener,
PlaybackEventListener { PlaybackEventListener {
// ExtractorOutput implementation. // ExtractorOutput implementation.
@ -515,7 +538,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Handles the {@link Loadable} whose {@link RtpDataChannel} timed out. */ /** Handles the {@link Loadable} whose {@link RtpDataChannel} timed out. */
private LoadErrorAction handleSocketTimeout(RtpDataLoadable loadable) { private LoadErrorAction handleSocketTimeout(RtpDataLoadable loadable) {
// TODO(b/172331505) Allow for retry when loading is not ending. // TODO(b/172331505) Allow for retry when loading is not ending.
if (getBufferedPositionUs() == Long.MIN_VALUE) { if (getBufferedPositionUs() == 0) {
if (!isUsingRtpTcp) { if (!isUsingRtpTcp) {
// Retry playback with TCP if no sample has been received so far, and we are not already // 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 // 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; break;
} }
} }
playbackException = new RtspPlaybackException("Unknown loadable timed out.");
return Loader.DONT_RETRY; 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() { private void retryWithRtpTcp() {
@ -632,6 +673,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
sampleQueue.setUpstreamFormatChangeListener(internalListener); 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. */ /** Starts loading. */
public void startLoading() { public void startLoading() {
loader.startLoading( loader.startLoading(

View File

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

View File

@ -22,7 +22,9 @@ import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider; 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.PlaybackEventListener;
import com.google.android.exoplayer2.source.rtsp.RtspClient.SessionInfoListener; 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.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;
@ -76,6 +78,17 @@ public final class RtspClientTest {
public void onSessionTimelineRequestFailed( public void onSessionTimelineRequestFailed(
String message, @Nullable Throwable cause) {} 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", /* userAgent= */ "ExoPlayer:RtspClientTest",
/* uri= */ Uri.parse( /* uri= */ Uri.parse(
Util.formatInvariant("rtsp://localhost:%d/test", serverRtspPortNumber))); 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);
}
}