Skip invalid media description in SessionDescriptionParser

Some RTSP servers may provide media descriptions for custom streams that are not supported. ExoPlayer should skip the invalid media description and continues parsing the following media descriptions.

To start, ExoPlayer will still error on malformed SDP lines for media descriptions, but will now skip media descriptions with "non-parsable" formats as described by [RFC 8866 Section 5.14](https://datatracker.ietf.org/doc/html/rfc8866#section-5.14).

Issue: androidx/media#1472
PiperOrigin-RevId: 660826116
This commit is contained in:
michaelkatz 2024-08-08 07:19:15 -07:00 committed by Copybara-Service
parent c1078e3cfa
commit 8b33ad5811
3 changed files with 99 additions and 7 deletions

View File

@ -108,6 +108,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:

View File

@ -24,6 +24,7 @@ import static com.google.common.base.Strings.nullToEmpty;
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 @@ import java.util.regex.Pattern;
/** 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 @@ import java.util.regex.Pattern;
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 @@ import java.util.regex.Pattern;
break;
case INFORMATION_TYPE:
if (isSkippingMediaDescription) {
continue;
}
if (mediaDescriptionBuilder == null) {
sessionDescriptionBuilder.setSessionInfo(sdpValue);
} else {
@ -131,6 +141,9 @@ import java.util.regex.Pattern;
break;
case CONNECTION_TYPE:
if (isSkippingMediaDescription) {
continue;
}
if (mediaDescriptionBuilder == null) {
sessionDescriptionBuilder.setConnection(sdpValue);
} else {
@ -139,6 +152,9 @@ import java.util.regex.Pattern;
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 @@ import java.util.regex.Pattern;
break;
case KEY_TYPE:
if (isSkippingMediaDescription) {
continue;
}
if (mediaDescriptionBuilder == null) {
sessionDescriptionBuilder.setKey(sdpValue);
} else {
@ -164,6 +183,10 @@ import java.util.regex.Pattern;
break;
case ATTRIBUTE_TYPE:
// Parsing attribute
if (isSkippingMediaDescription) {
continue;
}
matcher = ATTRIBUTE_PATTERN.matcher(sdpValue);
if (!matcher.matches()) {
throw ParserException.createForMalformedManifest(
@ -186,6 +209,7 @@ import java.util.regex.Pattern;
addMediaDescriptionToSession(sessionDescriptionBuilder, mediaDescriptionBuilder);
}
mediaDescriptionBuilder = parseMediaDescriptionLine(sdpValue);
isSkippingMediaDescription = mediaDescriptionBuilder == null;
break;
case REPEAT_TYPE:
case ZONE_TYPE:
@ -216,6 +240,18 @@ import java.util.regex.Pattern;
}
}
/**
* Parses a Media Description SDP line.
*
* <p>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 @@ import java.util.regex.Pattern;
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. */

View File

@ -192,6 +192,58 @@ public class SessionDescriptionTest {
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 {