diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f97db1cef60..16fe8852979 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -78,6 +78,8 @@ * RTSP: * Handle when additional spaces are in SDP's RTPMAP atrribute ([#9379](https://github.com/google/ExoPlayer/issues/9379)). + * Handle partial URIs in RTP-Info headers + ([#9346](https://github.com/google/ExoPlayer/issues/9346)). * Extractors: * ID3: Fix issue decoding ID3 tags containing UTF-16 encoded strings ([#9087](https://github.com/google/ExoPlayer/issues/9087)). diff --git a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspClient.java b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspClient.java index 882a090435c..a8003365319 100644 --- a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspClient.java +++ b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspClient.java @@ -545,7 +545,7 @@ private void handleRtspResponse(List message) { ImmutableList trackTimingList = rtpInfoString == null ? ImmutableList.of() - : RtspTrackTiming.parseTrackTiming(rtpInfoString); + : RtspTrackTiming.parseTrackTiming(rtpInfoString, uri); onPlayResponseReceived(new RtspPlayResponse(response.status, timing, trackTimingList)); break; diff --git a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspTrackTiming.java b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspTrackTiming.java index 3e6d34038e5..cf0fd5f691e 100644 --- a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspTrackTiming.java +++ b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/RtspTrackTiming.java @@ -15,10 +15,15 @@ */ package com.google.android.exoplayer2.source.rtsp; +import static com.google.android.exoplayer2.util.Assertions.checkArgument; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; + import android.net.Uri; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.util.UriUtil; import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; @@ -49,11 +54,12 @@ * * * @param rtpInfoString The value of the RTP-Info header, with header name (RTP-Info) removed. + * @param sessionUri The session URI, must include an {@code rtsp} scheme. * @return A list of parsed {@link RtspTrackTiming}. * @throws ParserException If parsing failed. */ - public static ImmutableList parseTrackTiming(String rtpInfoString) - throws ParserException { + public static ImmutableList parseTrackTiming( + String rtpInfoString, Uri sessionUri) throws ParserException { ImmutableList.Builder listBuilder = new ImmutableList.Builder<>(); for (String perTrackTimingString : Util.split(rtpInfoString, ",")) { @@ -69,7 +75,7 @@ public static ImmutableList parseTrackTiming(String rtpInfoStri switch (attributeName) { case "url": - uri = Uri.parse(attributeValue); + uri = resolveUri(/* urlString= */ attributeValue, sessionUri); break; case "seq": sequenceNumber = Integer.parseInt(attributeValue); @@ -96,6 +102,48 @@ public static ImmutableList parseTrackTiming(String rtpInfoStri return listBuilder.build(); } + /** + * Resolves the input string to always be an absolute URL with RTP-Info headers + * + *

Handles some servers do not send absolute URL in RTP-Info headers. This method takes in + * RTP-Info header's url string, and returns the correctly formatted {@link Uri url} for this + * track. The input url string could be + * + *

    + *
  • A correctly formatted URL, like "{@code rtsp://foo.bar/video}". + *
  • A correct URI that is missing the scheme, like "{@code foo.bar/video}". + *
  • A path to the resource, like "{@code video}" or "{@code /video}". + *
+ * + * @param urlString The URL included in the RTP-Info header, without the {@code url=} identifier. + * @param sessionUri The session URI, must include an {@code rtsp} scheme, or {@link + * IllegalArgumentException} is thrown. + * @return The formatted URL. + */ + @VisibleForTesting + /* package */ static Uri resolveUri(String urlString, Uri sessionUri) { + checkArgument(checkNotNull(sessionUri.getScheme()).equals("rtsp")); + + Uri uri = Uri.parse(urlString); + if (uri.isAbsolute()) { + return uri; + } + + // The urlString is at least missing the scheme. + uri = Uri.parse("rtsp://" + urlString); + String sessionUriString = sessionUri.toString(); + + String host = checkNotNull(uri.getHost()); + if (host.equals(sessionUri.getHost())) { + // Handles the case that the urlString is only missing the scheme. + return uri; + } + + return sessionUriString.endsWith("/") + ? UriUtil.resolveToUri(sessionUriString, urlString) + : UriUtil.resolveToUri(sessionUriString + "/", urlString); + } + /** * The timestamp of the next RTP packet, {@link C#TIME_UNSET} if not present. * diff --git a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspTrackTimingTest.java b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspTrackTimingTest.java index 1269811301b..5f50929fbab 100644 --- a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspTrackTimingTest.java +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspTrackTimingTest.java @@ -35,7 +35,7 @@ public void parseTiming_withSeqNumberAndRtpTime() throws Exception { "url=rtsp://video.example.com/twister/video;seq=12312232;rtptime=78712811"; ImmutableList trackTimingList = - RtspTrackTiming.parseTrackTiming(rtpInfoString); + RtspTrackTiming.parseTrackTiming(rtpInfoString, Uri.parse("rtsp://video.example.com")); assertThat(trackTimingList).hasSize(1); RtspTrackTiming trackTiming = trackTimingList.get(0); @@ -50,7 +50,7 @@ public void parseTiming_withSeqNumberOnly() throws Exception { "url=rtsp://foo.com/bar.avi/streamid=0;seq=45102,url=rtsp://foo.com/bar.avi/streamid=1;seq=30211"; ImmutableList trackTimingList = - RtspTrackTiming.parseTrackTiming(rtpInfoString); + RtspTrackTiming.parseTrackTiming(rtpInfoString, Uri.parse("rtsp://foo.com")); assertThat(trackTimingList).hasSize(2); RtspTrackTiming trackTiming = trackTimingList.get(0); @@ -67,27 +67,88 @@ public void parseTiming_withSeqNumberOnly() throws Exception { public void parseTiming_withInvalidParameter_throws() { String rtpInfoString = "url=rtsp://video.example.com/twister/video;seq=123abc"; - assertThrows(ParserException.class, () -> RtspTrackTiming.parseTrackTiming(rtpInfoString)); - } - - @Test - public void parseTiming_withInvalidUrl_throws() { - String rtpInfoString = "url=video.example.com/twister/video;seq=36192348"; - - assertThrows(ParserException.class, () -> RtspTrackTiming.parseTrackTiming(rtpInfoString)); + assertThrows( + ParserException.class, + () -> + RtspTrackTiming.parseTrackTiming( + rtpInfoString, Uri.parse("rtsp://video.example.com/twister"))); } @Test public void parseTiming_withNoParameter_throws() { String rtpInfoString = "url=rtsp://video.example.com/twister/video"; - assertThrows(ParserException.class, () -> RtspTrackTiming.parseTrackTiming(rtpInfoString)); + assertThrows( + ParserException.class, + () -> + RtspTrackTiming.parseTrackTiming( + rtpInfoString, Uri.parse("rtsp://video.example.com/twister"))); } @Test public void parseTiming_withNoUrl_throws() { String rtpInfoString = "seq=35421887"; - assertThrows(ParserException.class, () -> RtspTrackTiming.parseTrackTiming(rtpInfoString)); + assertThrows( + ParserException.class, + () -> + RtspTrackTiming.parseTrackTiming( + rtpInfoString, Uri.parse("rtsp://video.example.com/twister"))); + } + + @Test + public void resolveUri_withAbsoluteUri_succeeds() { + Uri uri = + RtspTrackTiming.resolveUri( + "rtsp://video.example.com/twister/video=1?a2bfc09887ce", + Uri.parse("rtsp://video.example.com/twister")); + + assertThat(uri).isEqualTo(Uri.parse("rtsp://video.example.com/twister/video=1?a2bfc09887ce")); + } + + @Test + public void resolveUri_withCompleteUriMissingScheme_succeeds() { + Uri uri = + RtspTrackTiming.resolveUri( + "video.example.com/twister/video=1", Uri.parse("rtsp://video.example.com/twister")); + + assertThat(uri).isEqualTo(Uri.parse("rtsp://video.example.com/twister/video=1")); + } + + @Test + public void resolveUri_withPartialUriMissingScheme_succeeds() { + Uri uri = RtspTrackTiming.resolveUri("video=1", Uri.parse("rtsp://video.example.com/twister")); + + assertThat(uri).isEqualTo(Uri.parse("rtsp://video.example.com/twister/video=1")); + } + + @Test + public void resolveUri_withMultipartPartialUriMissingScheme_succeeds() { + Uri uri = + RtspTrackTiming.resolveUri( + "container/video=1", Uri.parse("rtsp://video.example.com/twister")); + + assertThat(uri).isEqualTo(Uri.parse("rtsp://video.example.com/twister/container/video=1")); + } + + @Test + public void resolveUri_withPartialUriMissingSchemeWithIpBaseUri_succeeds() { + Uri uri = RtspTrackTiming.resolveUri("video=1", Uri.parse("rtsp://127.0.0.1:18888/test")); + + assertThat(uri).isEqualTo(Uri.parse("rtsp://127.0.0.1:18888/test/video=1")); + } + + @Test + public void resolveUri_withPartialUriMissingSchemeWithIpBaseUriWithSlash_succeeds() { + Uri uri = RtspTrackTiming.resolveUri("video=1", Uri.parse("rtsp://127.0.0.1:18888/test/")); + + assertThat(uri).isEqualTo(Uri.parse("rtsp://127.0.0.1:18888/test/video=1")); + } + + @Test + public void resolveUri_withSessionUriMissingScheme_throwsIllegalArgumentException() { + assertThrows( + IllegalArgumentException.class, + () -> RtspTrackTiming.resolveUri("video=1", Uri.parse("127.0.0.1:18888/test"))); } }