From e6b5392e6316160b1b1726b8b9a195af4f06cbb7 Mon Sep 17 00:00:00 2001 From: claincly Date: Wed, 8 Sep 2021 13:47:20 +0100 Subject: [PATCH] Handle malformed URL in RTP-Info header. Some server will send partial URIs in the RTP-Info header, while the RTSP spec requires absolute URLs. Issue: #9346 #exofixit PiperOrigin-RevId: 395452741 --- RELEASENOTES.md | 2 + .../exoplayer2/source/rtsp/RtspClient.java | 2 +- .../source/rtsp/RtspTrackTiming.java | 54 +++++++++++- .../source/rtsp/RtspTrackTimingTest.java | 85 ++++++++++++++++--- 4 files changed, 127 insertions(+), 16 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f97db1cef6..16fe885297 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 882a090435..a800336531 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 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 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 3e6d34038e..cf0fd5f691 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 @@ import com.google.common.collect.ImmutableList; * * * @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 @@ import com.google.common.collect.ImmutableList; switch (attributeName) { case "url": - uri = Uri.parse(attributeValue); + uri = resolveUri(/* urlString= */ attributeValue, sessionUri); break; case "seq": sequenceNumber = Integer.parseInt(attributeValue); @@ -96,6 +102,48 @@ import com.google.common.collect.ImmutableList; 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 1269811301..5f50929fba 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 class RtspTrackTimingTest { "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 class RtspTrackTimingTest { "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 class RtspTrackTimingTest { 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"))); } }