diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d57f9d0683..07e3504a4f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -66,6 +66,9 @@ * HLS Extension: * Smooth Streaming Extension: * RTSP Extension: + * Use base Uri for relative path resolution from the RTSP session if + present in DESCRIBE response header + ([#11160](https://github.com/google/ExoPlayer/issues/11160)). * Decoder Extensions (FFmpeg, VP9, AV1, etc.): * Cast Extension: * Test Utilities: diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspClient.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspClient.java index 8056d5687e..f04b1a62c5 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspClient.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspClient.java @@ -337,19 +337,22 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } /** - * Gets the included {@link RtspMediaTrack RtspMediaTracks} from a {@link SessionDescription}. + * Returns the included {@link RtspMediaTrack RtspMediaTracks} from parsing the {@link + * SessionDescription} within the {@link RtspDescribeResponse}. * - * @param sessionDescription The {@link SessionDescription}. + * @param rtspDescribeResponse The {@link RtspDescribeResponse} from which to retrieve the tracks. * @param uri The RTSP playback URI. */ private static ImmutableList buildTrackList( - SessionDescription sessionDescription, Uri uri) { + RtspDescribeResponse rtspDescribeResponse, Uri uri) { ImmutableList.Builder trackListBuilder = new ImmutableList.Builder<>(); - for (int i = 0; i < sessionDescription.mediaDescriptionList.size(); i++) { - MediaDescription mediaDescription = sessionDescription.mediaDescriptionList.get(i); + for (int i = 0; i < rtspDescribeResponse.sessionDescription.mediaDescriptionList.size(); i++) { + MediaDescription mediaDescription = + rtspDescribeResponse.sessionDescription.mediaDescriptionList.get(i); // Includes tracks with supported formats only. if (RtpPayloadFormat.isFormatSupported(mediaDescription)) { - trackListBuilder.add(new RtspMediaTrack(mediaDescription, uri)); + trackListBuilder.add( + new RtspMediaTrack(rtspDescribeResponse.headers, mediaDescription, uri)); } } return trackListBuilder.build(); @@ -618,7 +621,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; case METHOD_DESCRIBE: onDescribeResponseReceived( new RtspDescribeResponse( - response.status, SessionDescriptionParser.parse(response.messageBody))); + response.headers, + response.status, + SessionDescriptionParser.parse(response.messageBody))); break; case METHOD_SETUP: @@ -708,7 +713,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } - ImmutableList tracks = buildTrackList(response.sessionDescription, uri); + ImmutableList tracks = buildTrackList(response, uri); if (tracks.isEmpty()) { sessionInfoListener.onSessionTimelineRequestFailed("No playable track.", /* cause= */ null); return; diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspDescribeResponse.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspDescribeResponse.java index 3902cc3f7f..209e072a40 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspDescribeResponse.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspDescribeResponse.java @@ -20,6 +20,8 @@ import androidx.media3.common.util.UnstableApi; /** Represents an RTSP DESCRIBE response. */ @UnstableApi /* package */ final class RtspDescribeResponse { + /** The response's headers. */ + public final RtspHeaders headers; /** The response's status code. */ public final int status; /** The {@link SessionDescription} (see RFC2327) in the DESCRIBE response. */ @@ -28,10 +30,13 @@ import androidx.media3.common.util.UnstableApi; /** * Creates a new instance. * + * @param headers The response's headers. * @param status The response's status code. * @param sessionDescription The {@link SessionDescription} in the DESCRIBE response. */ - public RtspDescribeResponse(int status, SessionDescription sessionDescription) { + public RtspDescribeResponse( + RtspHeaders headers, int status, SessionDescription sessionDescription) { + this.headers = headers; this.status = status; this.sessionDescription = sessionDescription; } diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java index 92d1d4b78e..6bcf3dcf30 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java @@ -21,9 +21,12 @@ import static androidx.media3.common.util.Util.castNonNull; import static androidx.media3.container.NalUnitUtil.NAL_START_CODE; import static androidx.media3.exoplayer.rtsp.MediaDescription.MEDIA_TYPE_AUDIO; import static androidx.media3.exoplayer.rtsp.RtpPayloadFormat.getMimeTypeFromRtpMediaType; +import static androidx.media3.exoplayer.rtsp.RtspHeaders.CONTENT_BASE; +import static androidx.media3.exoplayer.rtsp.RtspHeaders.CONTENT_LOCATION; import static androidx.media3.exoplayer.rtsp.SessionDescription.ATTR_CONTROL; import android.net.Uri; +import android.text.TextUtils; import android.util.Base64; import android.util.Pair; import androidx.annotation.Nullable; @@ -155,14 +158,18 @@ import com.google.common.collect.ImmutableMap; /** * Creates a new instance from a {@link MediaDescription}. * + * @param rtspHeaders The {@link RtspHeaders} from the session's DESCRIBE response. * @param mediaDescription The {@link MediaDescription} of this track. * @param sessionUri The {@link Uri} of the RTSP playback session. */ - public RtspMediaTrack(MediaDescription mediaDescription, Uri sessionUri) { + public RtspMediaTrack( + RtspHeaders rtspHeaders, MediaDescription mediaDescription, Uri sessionUri) { checkArgument( mediaDescription.attributes.containsKey(ATTR_CONTROL), "missing attribute control"); payloadFormat = generatePayloadFormat(mediaDescription); - uri = extractTrackUri(sessionUri, castNonNull(mediaDescription.attributes.get(ATTR_CONTROL))); + uri = + extractTrackUri( + rtspHeaders, sessionUri, castNonNull(mediaDescription.attributes.get(ATTR_CONTROL))); } @Override @@ -466,15 +473,25 @@ import com.google.common.collect.ImmutableMap; * *

The processing logic is specified in RFC2326 Section C.1.1. * + * @param rtspHeaders The {@link RtspHeaders} from the session's DESCRIBE response. * @param sessionUri The session URI. * @param controlAttributeString The control attribute from the track's {@link MediaDescription}. * @return The extracted track URI. */ - private static Uri extractTrackUri(Uri sessionUri, String controlAttributeString) { + private static Uri extractTrackUri( + RtspHeaders rtspHeaders, Uri sessionUri, String controlAttributeString) { Uri controlAttributeUri = Uri.parse(controlAttributeString); if (controlAttributeUri.isAbsolute()) { return controlAttributeUri; - } else if (controlAttributeString.equals(GENERIC_CONTROL_ATTR)) { + } + + if (!TextUtils.isEmpty(rtspHeaders.get(CONTENT_BASE))) { + sessionUri = Uri.parse(rtspHeaders.get(CONTENT_BASE)); + } else if (!TextUtils.isEmpty(rtspHeaders.get(CONTENT_LOCATION))) { + sessionUri = Uri.parse(rtspHeaders.get(CONTENT_LOCATION)); + } + + if (controlAttributeString.equals(GENERIC_CONTROL_ATTR)) { return sessionUri; } else { return sessionUri.buildUpon().appendEncodedPath(controlAttributeString).build(); diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspMediaTrackTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspMediaTrackTest.java index 5f0eeb627e..11fac5a101 100644 --- a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspMediaTrackTest.java +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspMediaTrackTest.java @@ -39,6 +39,16 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class RtspMediaTrackTest { + private static final RtspHeaders RTSP_DESCRIBE_RESPONSE_HEADERS = + new RtspHeaders.Builder() + .addAll( + ImmutableList.of( + "Accept: application/sdp", + "CSeq: 3", + "Content-Length: 707", + "Transport: RTP/AVP;unicast;client_port=65458-65459\r\n")) + .build(); + @Test public void generatePayloadFormat_withH264MediaDescription_succeeds() { MediaDescription mediaDescription = @@ -361,7 +371,9 @@ public class RtspMediaTrackTest { MediaDescription mediaDescription = createGenericMediaDescriptionWithControlAttribute("path1/track2"); - RtspMediaTrack mediaTrack = new RtspMediaTrack(mediaDescription, Uri.parse("rtsp://test.com")); + RtspMediaTrack mediaTrack = + new RtspMediaTrack( + RTSP_DESCRIBE_RESPONSE_HEADERS, mediaDescription, Uri.parse("rtsp://test.com")); assertThat(mediaTrack.uri).isEqualTo(Uri.parse("rtsp://test.com/path1/track2")); } @@ -371,7 +383,9 @@ public class RtspMediaTrackTest { MediaDescription mediaDescription = createGenericMediaDescriptionWithControlAttribute("rtsp://test.com/foo"); - RtspMediaTrack mediaTrack = new RtspMediaTrack(mediaDescription, Uri.parse("rtsp://test.com")); + RtspMediaTrack mediaTrack = + new RtspMediaTrack( + RTSP_DESCRIBE_RESPONSE_HEADERS, mediaDescription, Uri.parse("rtsp://test.com")); assertThat(mediaTrack.uri).isEqualTo(Uri.parse("rtsp://test.com/foo")); } @@ -380,11 +394,113 @@ public class RtspMediaTrackTest { public void rtspMediaTrack_mediaDescriptionContainsGenericUri_setsCorrectTrackUri() { MediaDescription mediaDescription = createGenericMediaDescriptionWithControlAttribute("*"); - RtspMediaTrack mediaTrack = new RtspMediaTrack(mediaDescription, Uri.parse("rtsp://test.com")); + RtspMediaTrack mediaTrack = + new RtspMediaTrack( + RTSP_DESCRIBE_RESPONSE_HEADERS, mediaDescription, Uri.parse("rtsp://test.com")); assertThat(mediaTrack.uri).isEqualTo(Uri.parse("rtsp://test.com")); } + @Test + public void rtspMediaTrack_withContentBaseAndRelativeUri_setsCorrectTrackUri() { + RtspHeaders rtspHeaders = + RTSP_DESCRIBE_RESPONSE_HEADERS + .buildUpon() + .addAll(ImmutableList.of("Content-Base: rtsp://test.com/path1")) + .build(); + MediaDescription mediaDescription = + createGenericMediaDescriptionWithControlAttribute("path2/track3"); + + RtspMediaTrack mediaTrack = + new RtspMediaTrack(rtspHeaders, mediaDescription, Uri.parse("rtsp://test.com")); + + assertThat(mediaTrack.uri).isEqualTo(Uri.parse("rtsp://test.com/path1/path2/track3")); + } + + @Test + public void rtspMediaTrack_withContentLocationAndRelativeUri_setsCorrectTrackUri() { + RtspHeaders rtspHeaders = + RTSP_DESCRIBE_RESPONSE_HEADERS + .buildUpon() + .addAll(ImmutableList.of("Content-Location: rtsp://test.com/path1")) + .build(); + MediaDescription mediaDescription = + createGenericMediaDescriptionWithControlAttribute("path2/track3"); + + RtspMediaTrack mediaTrack = + new RtspMediaTrack(rtspHeaders, mediaDescription, Uri.parse("rtsp://test.com")); + + assertThat(mediaTrack.uri).isEqualTo(Uri.parse("rtsp://test.com/path1/path2/track3")); + } + + @Test + public void rtspMediaTrack_withBothContentBaseAndLocation_setsCorrectTrackUri() { + RtspHeaders rtspHeaders = + RTSP_DESCRIBE_RESPONSE_HEADERS + .buildUpon() + .addAll( + ImmutableList.of( + "Content-Base: rtsp://test.com/path1", + "Content-Location: rtsp://test.com/path2")) + .build(); + MediaDescription mediaDescription = + createGenericMediaDescriptionWithControlAttribute("path2/track3"); + + RtspMediaTrack mediaTrack = + new RtspMediaTrack(rtspHeaders, mediaDescription, Uri.parse("rtsp://test.com")); + + assertThat(mediaTrack.uri).isEqualTo(Uri.parse("rtsp://test.com/path1/path2/track3")); + } + + @Test + public void + rtspMediaTrack_withContentLocationAndEmptyContentBaseAndRelativeUri_setsCorrectTrackUri() { + RtspHeaders rtspHeaders = + RTSP_DESCRIBE_RESPONSE_HEADERS + .buildUpon() + .addAll(ImmutableList.of("Content-Base:", "Content-Location: rtsp://test.com/path1")) + .build(); + MediaDescription mediaDescription = + createGenericMediaDescriptionWithControlAttribute("path2/track3"); + + RtspMediaTrack mediaTrack = + new RtspMediaTrack(rtspHeaders, mediaDescription, Uri.parse("rtsp://test.com")); + + assertThat(mediaTrack.uri).isEqualTo(Uri.parse("rtsp://test.com/path1/path2/track3")); + } + + @Test + public void rtspMediaTrack_withEmptyContentLocationAndRelativeUri_setsCorrectTrackUri() { + RtspHeaders rtspHeaders = + RTSP_DESCRIBE_RESPONSE_HEADERS + .buildUpon() + .addAll(ImmutableList.of("Content-Location:")) + .build(); + MediaDescription mediaDescription = + createGenericMediaDescriptionWithControlAttribute("path2/track3"); + + RtspMediaTrack mediaTrack = + new RtspMediaTrack(rtspHeaders, mediaDescription, Uri.parse("rtsp://test.com")); + + assertThat(mediaTrack.uri).isEqualTo(Uri.parse("rtsp://test.com/path2/track3")); + } + + @Test + public void rtspMediaTrack_withContentBaseAndAbsoluteUri_setsCorrectTrackUri() { + RtspHeaders rtspHeaders = + RTSP_DESCRIBE_RESPONSE_HEADERS + .buildUpon() + .addAll(ImmutableList.of("Content-Base: rtsp://test.com/path1")) + .build(); + MediaDescription mediaDescription = + createGenericMediaDescriptionWithControlAttribute("rtsp://test.com/foo"); + + RtspMediaTrack mediaTrack = + new RtspMediaTrack(rtspHeaders, mediaDescription, Uri.parse("rtsp://test.com")); + + assertThat(mediaTrack.uri).isEqualTo(Uri.parse("rtsp://test.com/foo")); + } + @Test public void generatePayloadFormat_withH264MediaDescriptionMissingProfileLevel_generatesCorrectProfileLevel() {