Added condition to RtspMediaPeriod.isLoading to check RtspState

If a RtspMediaSource is used within a combining source like MergingMediaSource, then it could become stuck in a loop of calling continueLoading when the source has not actually started loading yet.

Issue: androidx/media#577
PiperOrigin-RevId: 561322900
This commit is contained in:
michaelkatz 2023-08-30 06:27:26 -07:00 committed by Copybara-Service
parent b4ee896847
commit cf3fd1f4dd
3 changed files with 214 additions and 110 deletions

View File

@ -45,6 +45,9 @@
* RTSP Extension: * RTSP Extension:
* Fix a race condition that could lead to `IndexOutOfBoundsException` when * Fix a race condition that could lead to `IndexOutOfBoundsException` when
falling back to TCP, or playback hanging in some situations. falling back to TCP, or playback hanging in some situations.
* Check state in RTSP setup when returning loading state of
`RtspMediaPeriod`
([#577](https://github.com/androidx/media/issues/577)).
* Decoder Extensions (FFmpeg, VP9, AV1, etc.): * Decoder Extensions (FFmpeg, VP9, AV1, etc.):
* MIDI extension: * MIDI extension:
* Cast Extension: * Cast Extension:

View File

@ -380,7 +380,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override @Override
public boolean isLoading() { public boolean isLoading() {
return !loadingFinished; return !loadingFinished
&& (rtspClient.getState() == RtspClient.RTSP_STATE_PLAYING
|| rtspClient.getState() == RtspClient.RTSP_STATE_READY);
} }
@Override @Override

View File

@ -18,18 +18,26 @@ package androidx.media3.exoplayer.rtsp;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.LoadingInfo; import androidx.media3.exoplayer.LoadingInfo;
import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.SampleStream;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.trackselection.FixedTrackSelection;
import androidx.media3.exoplayer.upstream.DefaultAllocator; import androidx.media3.exoplayer.upstream.DefaultAllocator;
import androidx.media3.test.utils.robolectric.RobolectricUtil; import androidx.media3.test.utils.robolectric.RobolectricUtil;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import javax.net.SocketFactory; import javax.net.SocketFactory;
import org.junit.After; import org.junit.After;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -39,46 +47,44 @@ public final class RtspMediaPeriodTest {
private static final long DEFAULT_TIMEOUT_MS = 8000; private static final long DEFAULT_TIMEOUT_MS = 8000;
private RtspMediaPeriod mediaPeriod; private final AtomicReference<TrackGroup> trackGroupAtomicReference = new AtomicReference<>();
;
private final MediaPeriod.Callback mediaPeriodCallback =
new MediaPeriod.Callback() {
@Override
public void onPrepared(MediaPeriod mediaPeriod) {
trackGroupAtomicReference.set(mediaPeriod.getTrackGroups().get(0));
}
@Override
public void onContinueLoadingRequested(MediaPeriod source) {
source.continueLoading(new LoadingInfo.Builder().setPlaybackPositionUs(0).build());
}
};
private RtpPacketStreamDump rtpPacketStreamDump;
private RtspServer rtspServer; private RtspServer rtspServer;
private RtspMediaPeriod mediaPeriod;
@Before
public void setUp() throws IOException {
rtpPacketStreamDump = RtspTestUtils.readRtpPacketStreamDump("media/rtsp/aac-dump.json");
}
@After @After
public void tearDown() { public void tearDown() {
mediaPeriod.release();
Util.closeQuietly(rtspServer); Util.closeQuietly(rtspServer);
} }
@Test @Test
public void prepareMediaPeriod_refreshesSourceInfoAndCallsOnPrepared() throws Exception { public void prepareMediaPeriod_refreshesSourceInfoAndCallsOnPrepared() throws Exception {
RtpPacketStreamDump rtpPacketStreamDump = AtomicLong refreshedSourceDurationMs = new AtomicLong();
RtspTestUtils.readRtpPacketStreamDump("media/rtsp/aac-dump.json");
rtspServer = rtspServer =
new RtspServer( new RtspServer(
new RtspServer.ResponseProvider() { new TestResponseProvider(
@Override rtpPacketStreamDump,
public RtspResponse getOptionsResponse() { /* getPlayResponseReference= */ null,
return new RtspResponse( /* isWwwAuthenticationMode= */ false));
/* status= */ 200,
new RtspHeaders.Builder().add(RtspHeaders.PUBLIC, "OPTIONS, DESCRIBE").build());
}
@Override
public RtspResponse getDescribeResponse(Uri requestedUri, RtspHeaders headers) {
return RtspTestUtils.newDescribeResponseWithSdpMessage(
"v=0\r\n"
+ "o=- 1606776316530225 1 IN IP4 127.0.0.1\r\n"
+ "s=Exoplayer test\r\n"
+ "t=0 0\r\n"
// The session is 50.46s long.
+ "a=range:npt=0-50.46\r\n",
ImmutableList.of(rtpPacketStreamDump),
requestedUri);
}
});
AtomicBoolean prepareCallbackCalled = new AtomicBoolean();
AtomicLong refreshedSourceDurationMs = new AtomicLong();
mediaPeriod = mediaPeriod =
new RtspMediaPeriod( new RtspMediaPeriod(
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE), new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
@ -89,21 +95,8 @@ public final class RtspMediaPeriodTest {
/* socketFactory= */ SocketFactory.getDefault(), /* socketFactory= */ SocketFactory.getDefault(),
/* debugLoggingEnabled= */ false); /* debugLoggingEnabled= */ false);
mediaPeriod.prepare( mediaPeriod.prepare(mediaPeriodCallback, /* positionUs= */ 0);
new MediaPeriod.Callback() { RobolectricUtil.runMainLooperUntil(() -> trackGroupAtomicReference.get() != null);
@Override
public void onPrepared(MediaPeriod mediaPeriod) {
prepareCallbackCalled.set(true);
}
@Override
public void onContinueLoadingRequested(MediaPeriod source) {
source.continueLoading(new LoadingInfo.Builder().setPlaybackPositionUs(0).build());
}
},
/* positionUs= */ 0);
RobolectricUtil.runMainLooperUntil(prepareCallbackCalled::get);
mediaPeriod.release();
assertThat(refreshedSourceDurationMs.get()).isEqualTo(50_460); assertThat(refreshedSourceDurationMs.get()).isEqualTo(50_460);
} }
@ -111,21 +104,139 @@ public final class RtspMediaPeriodTest {
@Test @Test
public void prepareMediaPeriod_withWwwAuthentication_refreshesSourceInfoAndCallsOnPrepared() public void prepareMediaPeriod_withWwwAuthentication_refreshesSourceInfoAndCallsOnPrepared()
throws Exception { throws Exception {
RtpPacketStreamDump rtpPacketStreamDump = AtomicLong refreshedSourceDurationMs = new AtomicLong();
RtspTestUtils.readRtpPacketStreamDump("media/rtsp/aac-dump.json");
rtspServer = rtspServer =
new RtspServer( new RtspServer(
new RtspServer.ResponseProvider() { new TestResponseProvider(
rtpPacketStreamDump,
/* getPlayResponseReference= */ null,
/* isWwwAuthenticationMode= */ true));
mediaPeriod =
new RtspMediaPeriod(
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
new TransferRtpDataChannelFactory(DEFAULT_TIMEOUT_MS),
RtspTestUtils.getTestUriWithUserInfo(
"username", "password", rtspServer.startAndGetPortNumber()),
/* listener= */ timing -> refreshedSourceDurationMs.set(timing.getDurationMs()),
/* userAgent= */ "ExoPlayer:RtspPeriodTest",
/* socketFactory= */ SocketFactory.getDefault(),
/* debugLoggingEnabled= */ false);
mediaPeriod.prepare(mediaPeriodCallback, /* positionUs= */ 0);
RobolectricUtil.runMainLooperUntil(() -> trackGroupAtomicReference.get() != null);
assertThat(refreshedSourceDurationMs.get()).isEqualTo(50_460);
}
@Test
public void isLoading_afterInit_returnsFalse() throws Exception {
rtspServer =
new RtspServer(
() ->
new RtspResponse(
/* status= */ 200,
new RtspHeaders.Builder().add(RtspHeaders.PUBLIC, "OPTIONS").build()));
mediaPeriod =
new RtspMediaPeriod(
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
new TransferRtpDataChannelFactory(DEFAULT_TIMEOUT_MS),
RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber()),
/* listener= */ timing -> {},
/* userAgent= */ "ExoPlayer:RtspPeriodTest",
/* socketFactory= */ SocketFactory.getDefault(),
/* debugLoggingEnabled= */ false);
assertThat(mediaPeriod.isLoading()).isFalse();
}
@Test
public void isLoading_afterPrepare_returnsFalse() throws Exception {
rtspServer =
new RtspServer(
new TestResponseProvider(
rtpPacketStreamDump,
/* getPlayResponseReference= */ null,
/* isWwwAuthenticationMode= */ false));
mediaPeriod =
new RtspMediaPeriod(
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
new TransferRtpDataChannelFactory(DEFAULT_TIMEOUT_MS),
RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber()),
/* listener= */ timing -> {},
/* userAgent= */ "ExoPlayer:RtspPeriodTest",
/* socketFactory= */ SocketFactory.getDefault(),
/* debugLoggingEnabled= */ false);
mediaPeriod.prepare(mediaPeriodCallback, /* positionUs= */ 0);
RobolectricUtil.runMainLooperUntil(() -> trackGroupAtomicReference.get() != null);
assertThat(mediaPeriod.isLoading()).isFalse();
}
@Test
public void isLoading_afterSelectTracksAndSendPlayRequest_returnsTrue() throws Exception {
AtomicBoolean getPlayResponseReference = new AtomicBoolean();
rtspServer =
new RtspServer(
new TestResponseProvider(
rtpPacketStreamDump,
/* getPlayResponseReference= */ getPlayResponseReference,
/* isWwwAuthenticationMode= */ false));
mediaPeriod =
new RtspMediaPeriod(
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
new TransferRtpDataChannelFactory(DEFAULT_TIMEOUT_MS),
RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber()),
/* listener= */ timing -> {},
/* userAgent= */ "ExoPlayer:RtspPeriodTest",
/* socketFactory= */ SocketFactory.getDefault(),
/* debugLoggingEnabled= */ false);
mediaPeriod.prepare(mediaPeriodCallback, /* positionUs= */ 0);
RobolectricUtil.runMainLooperUntil(() -> trackGroupAtomicReference.get() != null);
SampleStream[] sampleStreams = new SampleStream[1];
// Call selectTracks to initiate RTSP SETUP Request
mediaPeriod.selectTracks(
new ExoTrackSelection[] {
new FixedTrackSelection(trackGroupAtomicReference.get(), /* track= */ 0)
},
/* mayRetainStreamFlags= */ new boolean[] {false},
sampleStreams,
/* streamResetFlags= */ new boolean[] {true},
/* positionUs= */ 0);
RobolectricUtil.runMainLooperUntil(getPlayResponseReference::get);
assertThat(mediaPeriod.isLoading()).isTrue();
}
private static class TestResponseProvider implements RtspServer.ResponseProvider {
private static final String SESSION_ID = "00000000";
private final ImmutableList<RtpPacketStreamDump> rtpPacketStreamDumps;
@Nullable private final AtomicBoolean getPlayResponseReference;
private final boolean isWwwAuthenticationMode;
private TestResponseProvider(
RtpPacketStreamDump rtpPacketStreamDump,
@Nullable AtomicBoolean getPlayResponseReference,
boolean isWwwAuthenticationMode) {
this.rtpPacketStreamDumps = ImmutableList.of(rtpPacketStreamDump);
this.getPlayResponseReference = getPlayResponseReference;
this.isWwwAuthenticationMode = isWwwAuthenticationMode;
}
@Override @Override
public RtspResponse getOptionsResponse() { public RtspResponse getOptionsResponse() {
return new RtspResponse( return new RtspResponse(
/* status= */ 200, /* status= */ 200,
new RtspHeaders.Builder().add(RtspHeaders.PUBLIC, "OPTIONS, DESCRIBE").build()); new RtspHeaders.Builder()
.add(RtspHeaders.PUBLIC, "OPTIONS, DESCRIBE, SETUP, PLAY")
.build());
} }
@Override @Override
public RtspResponse getDescribeResponse(Uri requestedUri, RtspHeaders headers) { public RtspResponse getDescribeResponse(Uri requestedUri, RtspHeaders headers) {
if (isWwwAuthenticationMode) {
String authorizationHeader = headers.get(RtspHeaders.AUTHORIZATION); String authorizationHeader = headers.get(RtspHeaders.AUTHORIZATION);
if (authorizationHeader == null) { if (authorizationHeader == null) {
return new RtspResponse( return new RtspResponse(
@ -148,6 +259,7 @@ public final class RtspMediaPeriodTest {
.add(RtspHeaders.CSEQ, headers.get(RtspHeaders.CSEQ)) .add(RtspHeaders.CSEQ, headers.get(RtspHeaders.CSEQ))
.build()); .build());
} }
}
return RtspTestUtils.newDescribeResponseWithSdpMessage( return RtspTestUtils.newDescribeResponseWithSdpMessage(
"v=0\r\n" "v=0\r\n"
@ -156,40 +268,27 @@ public final class RtspMediaPeriodTest {
+ "t=0 0\r\n" + "t=0 0\r\n"
// The session is 50.46s long. // The session is 50.46s long.
+ "a=range:npt=0-50.46\r\n", + "a=range:npt=0-50.46\r\n",
ImmutableList.of(rtpPacketStreamDump), rtpPacketStreamDumps,
requestedUri); requestedUri);
} }
});
AtomicBoolean prepareCallbackCalled = new AtomicBoolean();
AtomicLong refreshedSourceDurationMs = new AtomicLong();
mediaPeriod =
new RtspMediaPeriod(
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
new TransferRtpDataChannelFactory(DEFAULT_TIMEOUT_MS),
RtspTestUtils.getTestUriWithUserInfo(
"username", "password", rtspServer.startAndGetPortNumber()),
/* listener= */ timing -> refreshedSourceDurationMs.set(timing.getDurationMs()),
/* userAgent= */ "ExoPlayer:RtspPeriodTest",
/* socketFactory= */ SocketFactory.getDefault(),
/* debugLoggingEnabled= */ false);
mediaPeriod.prepare(
new MediaPeriod.Callback() {
@Override @Override
public void onPrepared(MediaPeriod mediaPeriod) { public RtspResponse getSetupResponse(Uri requestedUri, RtspHeaders headers) {
prepareCallbackCalled.set(true); return new RtspResponse(
/* status= */ 200, headers.buildUpon().add(RtspHeaders.SESSION, SESSION_ID).build());
} }
@Override @Override
public void onContinueLoadingRequested(MediaPeriod source) { public RtspResponse getPlayResponse() {
source.continueLoading(new LoadingInfo.Builder().setPlaybackPositionUs(0).build()); if (getPlayResponseReference != null) {
getPlayResponseReference.set(true);
} }
},
/* positionUs= */ 0);
RobolectricUtil.runMainLooperUntil(prepareCallbackCalled::get);
mediaPeriod.release();
assertThat(refreshedSourceDurationMs.get()).isEqualTo(50_460); return new RtspResponse(
/* status= */ 200,
new RtspHeaders.Builder()
.add(RtspHeaders.RTP_INFO, RtspTestUtils.getRtpInfoForDumps(rtpPacketStreamDumps))
.build());
}
} }
} }