mirror of
https://github.com/androidx/media.git
synced 2025-05-09 00:20:45 +08:00
Move RtspClient creation into RtspPeriod.
RtspMediaSource uses the timeline update paradigm from ProgressiveMediaPeriod. #minor-release PiperOrigin-RevId: 378150758
This commit is contained in:
parent
22b126cac3
commit
1ca0efdd9b
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)));
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user