Commit 5a60db33 by claincly Committed by Ian Baker

Handle when RTSP track timing is not available.

Issue: google/ExoPlayer#9775

We got a few issues for this on GH already. Some RTSP servers do not provide
track timing in PLAY responses, or the timings are invalid.

Missing timing means the RTSP stream is not seekable. Added method to
1. Update the timeline that seek is not possible
2. Report read discontinuity so that playback can start from the beginning.

PiperOrigin-RevId: 423281439
parent 4cf91065
...@@ -106,8 +106,10 @@ ...@@ -106,8 +106,10 @@
* RTSP: * RTSP:
* Provide a client API to override the `SocketFactory` used for any server * Provide a client API to override the `SocketFactory` used for any server
connection ([#9606](https://github.com/google/ExoPlayer/pull/9606)). connection ([#9606](https://github.com/google/ExoPlayer/pull/9606)).
* Prefers DIGEST authentication method over BASIC if both are present. * Prefers DIGEST authentication method over BASIC if both are present
([#9800](https://github.com/google/ExoPlayer/issues/9800)). ([#9800](https://github.com/google/ExoPlayer/issues/9800)).
* Handle when RTSP track timing is not available
([#9775](https://github.com/google/ExoPlayer/issues/9775)).
* Cast extension * Cast extension
* Fix bug that prevented `CastPlayer` from calling `onIsPlayingChanged` * Fix bug that prevented `CastPlayer` from calling `onIsPlayingChanged`
correctly ([#9792](https://github.com/google/ExoPlayer/issues/9792)). correctly ([#9792](https://github.com/google/ExoPlayer/issues/9792)).
......
...@@ -67,6 +67,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -67,6 +67,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Called when the {@link RtspSessionTiming} is available. */ /** Called when the {@link RtspSessionTiming} is available. */
void onSourceInfoRefreshed(RtspSessionTiming timing); void onSourceInfoRefreshed(RtspSessionTiming timing);
/** Called when the RTSP server does not support seeking. */
default void onSeekingUnsupported() {}
} }
/** 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. */
...@@ -90,6 +93,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -90,6 +93,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private long pendingSeekPositionUs; private long pendingSeekPositionUs;
private long pendingSeekPositionUsForTcpRetry; private long pendingSeekPositionUsForTcpRetry;
private boolean loadingFinished; private boolean loadingFinished;
private boolean notifyDiscontinuity;
private boolean released; private boolean released;
private boolean prepared; private boolean prepared;
private boolean trackSelected; private boolean trackSelected;
...@@ -243,6 +247,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -243,6 +247,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override @Override
public long readDiscontinuity() { public long readDiscontinuity() {
// Discontinuity only happens in RTSP when seeking an unexpectedly un-seekable RTSP server (a
// server that doesn't include the required RTP-Info header in its PLAY responses). This only
// applies to seeks made before receiving the first RTSP PLAY response. The playback can only
// start from time zero in this case.
if (notifyDiscontinuity) {
notifyDiscontinuity = false;
return 0;
}
return C.TIME_UNSET; return C.TIME_UNSET;
} }
...@@ -355,7 +367,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -355,7 +367,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// SampleStream methods. // SampleStream methods.
/* package */ boolean isReady(int trackGroupIndex) { /* package */ boolean isReady(int trackGroupIndex) {
return rtspLoaderWrappers.get(trackGroupIndex).isSampleQueueReady(); return !suppressRead() && rtspLoaderWrappers.get(trackGroupIndex).isSampleQueueReady();
} }
@ReadDataResult @ReadDataResult
...@@ -364,9 +376,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -364,9 +376,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
FormatHolder formatHolder, FormatHolder formatHolder,
DecoderInputBuffer buffer, DecoderInputBuffer buffer,
@ReadFlags int readFlags) { @ReadFlags int readFlags) {
if (suppressRead()) {
return C.RESULT_NOTHING_READ;
}
return rtspLoaderWrappers.get(sampleQueueIndex).read(formatHolder, buffer, readFlags); return rtspLoaderWrappers.get(sampleQueueIndex).read(formatHolder, buffer, readFlags);
} }
/* package */ int skipData(int sampleQueueIndex, long positionUs) {
if (suppressRead()) {
return C.RESULT_NOTHING_READ;
}
return rtspLoaderWrappers.get(sampleQueueIndex).skipData(positionUs);
}
private boolean suppressRead() {
return notifyDiscontinuity;
}
// Internal methods. // Internal methods.
@Nullable @Nullable
...@@ -549,7 +575,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -549,7 +575,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override @Override
public void onPlaybackStarted( public void onPlaybackStarted(
long startPositionUs, ImmutableList<RtspTrackTiming> trackTimingList) { long startPositionUs, ImmutableList<RtspTrackTiming> trackTimingList) {
// Validate that the trackTimingList contains timings for the selected tracks.
// Validate that the trackTimingList contains timings for the selected tracks, and notify the
// listener.
ArrayList<String> trackUrisWithTiming = new ArrayList<>(trackTimingList.size()); ArrayList<String> trackUrisWithTiming = new ArrayList<>(trackTimingList.size());
for (int i = 0; i < trackTimingList.size(); i++) { for (int i = 0; i < trackTimingList.size(); i++) {
trackUrisWithTiming.add(checkNotNull(trackTimingList.get(i).uri.getPath())); trackUrisWithTiming.add(checkNotNull(trackTimingList.get(i).uri.getPath()));
...@@ -557,10 +585,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -557,10 +585,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
for (int i = 0; i < selectedLoadInfos.size(); i++) { for (int i = 0; i < selectedLoadInfos.size(); i++) {
RtpLoadInfo loadInfo = selectedLoadInfos.get(i); RtpLoadInfo loadInfo = selectedLoadInfos.get(i);
if (!trackUrisWithTiming.contains(loadInfo.getTrackUri().getPath())) { if (!trackUrisWithTiming.contains(loadInfo.getTrackUri().getPath())) {
playbackException = listener.onSeekingUnsupported();
new RtspPlaybackException( if (isSeekPending()) {
"Server did not provide timing for track " + loadInfo.getTrackUri()); notifyDiscontinuity = true;
return; pendingSeekPositionUs = C.TIME_UNSET;
requestedSeekPositionUs = C.TIME_UNSET;
pendingSeekPositionUsForTcpRetry = C.TIME_UNSET;
}
} }
} }
...@@ -697,7 +728,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -697,7 +728,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override @Override
public int skipData(long positionUs) { public int skipData(long positionUs) {
return 0; return RtspMediaPeriod.this.skipData(track, positionUs);
} }
} }
...@@ -748,6 +779,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -748,6 +779,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return sampleQueue.read(formatHolder, buffer, readFlags, /* loadingFinished= */ canceled); return sampleQueue.read(formatHolder, buffer, readFlags, /* loadingFinished= */ canceled);
} }
public int skipData(long positionUs) {
int skipCount = sampleQueue.getSkipCount(positionUs, /* allowEndOfQueue= */ canceled);
sampleQueue.skip(skipCount);
return skipCount;
}
/** Cancels loading. */ /** Cancels loading. */
public void cancelLoad() { public void cancelLoad() {
if (!canceled) { if (!canceled) {
......
...@@ -255,12 +255,21 @@ public final class RtspMediaSource extends BaseMediaSource { ...@@ -255,12 +255,21 @@ public final class RtspMediaSource extends BaseMediaSource {
allocator, allocator,
rtpDataChannelFactory, rtpDataChannelFactory,
uri, uri,
/* listener= */ timing -> { new RtspMediaPeriod.Listener() {
@Override
public void onSourceInfoRefreshed(RtspSessionTiming timing) {
timelineDurationUs = Util.msToUs(timing.getDurationMs()); timelineDurationUs = Util.msToUs(timing.getDurationMs());
timelineIsSeekable = !timing.isLive(); timelineIsSeekable = !timing.isLive();
timelineIsLive = timing.isLive(); timelineIsLive = timing.isLive();
timelineIsPlaceholder = false; timelineIsPlaceholder = false;
notifySourceInfoRefreshed(); notifySourceInfoRefreshed();
}
@Override
public void onSeekingUnsupported() {
timelineIsSeekable = false;
notifySourceInfoRefreshed();
}
}, },
userAgent, userAgent,
socketFactory, socketFactory,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment