diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 365770ab631..df2116a0de1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -41,6 +41,8 @@ * HLS Extension: * Smooth Streaming Extension: * RTSP Extension: + * Skip invalid Media Descriptions in SDP parsing + ([#1087](https://github.com/androidx/media/issues/1472)). * Decoder Extensions (FFmpeg, VP9, AV1, etc.): * MIDI extension: * Leanback extension: diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/SessionDescriptionParser.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/SessionDescriptionParser.java index 8d536047afd..411fef6ffa1 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/SessionDescriptionParser.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/SessionDescriptionParser.java @@ -24,6 +24,7 @@ import android.net.Uri; import androidx.annotation.Nullable; import androidx.media3.common.ParserException; +import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; import java.util.Objects; import java.util.regex.Matcher; @@ -31,6 +32,8 @@ /** Parses a String based SDP message into {@link SessionDescription}. */ /* package */ final class SessionDescriptionParser { + private static final String TAG = "SDPParser"; + // SDP line always starts with an one letter tag, followed by an equal sign. The information // under the given tag follows an optional space. private static final Pattern SDP_LINE_PATTERN = Pattern.compile("([a-z])=\\s?(.+)"); @@ -74,6 +77,10 @@ public static SessionDescription parse(String sdpString) throws ParserException SessionDescription.Builder sessionDescriptionBuilder = new SessionDescription.Builder(); @Nullable MediaDescription.Builder mediaDescriptionBuilder = null; + // Tracks if currently parsing an invalid media description and should skip any parsed + // and related SDP lines until the next valid media description. + boolean isSkippingMediaDescription = false; + // Lines are separated by an CRLF. for (String line : RtspMessageUtil.splitRtspMessageBody(sdpString)) { if ("".equals(line)) { @@ -111,6 +118,9 @@ public static SessionDescription parse(String sdpString) throws ParserException break; case INFORMATION_TYPE: + if (isSkippingMediaDescription) { + continue; + } if (mediaDescriptionBuilder == null) { sessionDescriptionBuilder.setSessionInfo(sdpValue); } else { @@ -131,6 +141,9 @@ public static SessionDescription parse(String sdpString) throws ParserException break; case CONNECTION_TYPE: + if (isSkippingMediaDescription) { + continue; + } if (mediaDescriptionBuilder == null) { sessionDescriptionBuilder.setConnection(sdpValue); } else { @@ -139,6 +152,9 @@ public static SessionDescription parse(String sdpString) throws ParserException break; case BANDWIDTH_TYPE: + if (isSkippingMediaDescription) { + continue; + } String[] bandwidthComponents = Util.split(sdpValue, ":\\s?"); checkArgument(bandwidthComponents.length == 2); int bitrateKbps = Integer.parseInt(bandwidthComponents[1]); @@ -156,6 +172,9 @@ public static SessionDescription parse(String sdpString) throws ParserException break; case KEY_TYPE: + if (isSkippingMediaDescription) { + continue; + } if (mediaDescriptionBuilder == null) { sessionDescriptionBuilder.setKey(sdpValue); } else { @@ -164,6 +183,10 @@ public static SessionDescription parse(String sdpString) throws ParserException break; case ATTRIBUTE_TYPE: + // Parsing attribute + if (isSkippingMediaDescription) { + continue; + } matcher = ATTRIBUTE_PATTERN.matcher(sdpValue); if (!matcher.matches()) { throw ParserException.createForMalformedManifest( @@ -186,6 +209,7 @@ public static SessionDescription parse(String sdpString) throws ParserException addMediaDescriptionToSession(sessionDescriptionBuilder, mediaDescriptionBuilder); } mediaDescriptionBuilder = parseMediaDescriptionLine(sdpValue); + isSkippingMediaDescription = mediaDescriptionBuilder == null; break; case REPEAT_TYPE: case ZONE_TYPE: @@ -216,6 +240,18 @@ private static void addMediaDescriptionToSession( } } + /** + * Parses a Media Description SDP line. + * + *

Returns a {@link MediaDescription.Builder} from parsing a valid SDP {@code line} is parsed + * or {@code null} if {@code line} contains invalid port or format values. + * + * @param line representing a Media Description. + * @return valid {@link MediaDescription.Builder} or {@code null} if {@code line} contains invalid + * port or format values. + * @throws ParserException if malformed SDP media description line. + */ + @Nullable private static MediaDescription.Builder parseMediaDescriptionLine(String line) throws ParserException { Matcher matcher = MEDIA_DESCRIPTION_PATTERN.matcher(line); @@ -228,16 +264,18 @@ private static MediaDescription.Builder parseMediaDescriptionLine(String line) String transportProtocol = checkNotNull(matcher.group(3)); String payloadTypeString = checkNotNull(matcher.group(4)); + @Nullable MediaDescription.Builder mediaDescriptionBuilder = null; try { - return new MediaDescription.Builder( - mediaType, - Integer.parseInt(portString), - transportProtocol, - Integer.parseInt(payloadTypeString)); + mediaDescriptionBuilder = + new MediaDescription.Builder( + mediaType, + Integer.parseInt(portString), + transportProtocol, + Integer.parseInt(payloadTypeString)); } catch (NumberFormatException e) { - throw ParserException.createForMalformedManifest( - "Malformed SDP media description line: " + line, e); + Log.w(TAG, "Malformed SDP media description line: " + line, e); } + return mediaDescriptionBuilder; } /** Prevents initialization. */ diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/SessionDescriptionTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/SessionDescriptionTest.java index 060e5727006..cf97e79dbf0 100644 --- a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/SessionDescriptionTest.java +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/SessionDescriptionTest.java @@ -192,6 +192,58 @@ public void parse_sdpString2_succeeds() throws Exception { assertThat(sessionDescription).isEqualTo(expectedSession); } + @Test + public void parse_sdpStringWithInvalidMediaDescriptionFormatType_succeeds() throws Exception { + // SDP with invalid media description should skip and parse following media descriptions + String testMediaSdpInfo = + "v=0\r\n" + + "o=- 1600785369059721 1 IN IP4 192.168.2.176\r\n" + + "s=SDP Seminar\r\n" + + "a=control:*\r\n" + + "m=video 0 RTP/AVP 96\r\n" + + "c=IN IP4 0.0.0.0\r\n" + + "b=AS:500\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=fmtp:96" + + " packetization-mode=1;profile-level-id=64001F;sprop-parameter-sets=Z2QAH6zZQPARabIAAAMACAAAAwGcHjBjLA==,aOvjyyLAAA==\r\n" + + "a=control:track1\r\n" + + "m=application/T-Link 0 RTP/AVP smart/1/90000\r\n" + + "i=CustomStream\r\n" + + "c=IN IP4 0.0.0.0\r\n" + + "b=AS:500\r\n" + + "a=rtpmap:95 T-Link/90000 \r\n" + + "a=control:track2 \r\n" + + "m=audio 3456 RTP/AVP 97\r\n" + + "a=rtpmap:97 AC3/44100\r\n" + + "a=control:track3\r\n"; + + SessionDescription sessionDescription = SessionDescriptionParser.parse(testMediaSdpInfo); + + SessionDescription expectedSession = + new SessionDescription.Builder() + .setOrigin("- 1600785369059721 1 IN IP4 192.168.2.176") + .setSessionName("SDP Seminar") + .addAttribute(ATTR_CONTROL, "*") + .addMediaDescription( + new MediaDescription.Builder(MEDIA_TYPE_VIDEO, 0, RTP_AVP_PROFILE, 96) + .setConnection("IN IP4 0.0.0.0") + .setBitrate(500_000) + .addAttribute(ATTR_RTPMAP, "96 H264/90000") + .addAttribute( + ATTR_FMTP, + "96 packetization-mode=1;profile-level-id=64001F;sprop-parameter-sets=Z2QAH6zZQPARabIAAAMACAAAAwGcHjBjLA==,aOvjyyLAAA==") + .addAttribute(ATTR_CONTROL, "track1") + .build()) + .addMediaDescription( + new MediaDescription.Builder(MEDIA_TYPE_AUDIO, 3456, RTP_AVP_PROFILE, 97) + .addAttribute(ATTR_RTPMAP, "97 AC3/44100") + .addAttribute(ATTR_CONTROL, "track3") + .build()) + .build(); + + assertThat(sessionDescription).isEqualTo(expectedSession); + } + @Test public void parse_sdpStringWithDuplicatedMediaAttribute_recordsTheMostRecentValue() throws Exception {