Catch IllegalArgumentExceptions in RTSP Response parsing

In parsing Describe RTSP response messages, IllegalArgumentExceptions are thrown for invalid parameters and values. These exceptions were not caught and crashed the Playback thread. Now these exceptions will be caught and their errors forwarded to the proper error handling listeners.

Issue: google/ExoPlayer#10971
PiperOrigin-RevId: 509207881
(cherry picked from commit a8c87453db02658a21293b44b017a70d5ae1125d)
This commit is contained in:
michaelkatz 2023-02-13 14:18:53 +00:00 committed by tonihei
parent f2753e2e27
commit cfe861ed89
4 changed files with 86 additions and 11 deletions

View File

@ -84,6 +84,12 @@ This release corresponds to the
([#233](https://github.com/androidx/media/issues/233)). ([#233](https://github.com/androidx/media/issues/233)).
* Make `QueueTimeline` more robust in case of a shady legacy session state * Make `QueueTimeline` more robust in case of a shady legacy session state
([#241](https://github.com/androidx/media/issues/241)). ([#241](https://github.com/androidx/media/issues/241)).
* Fix a bug where notification play/pause button doesn't update with
player state ([#192](https://github.com/androidx/media/issues/192)).
* RTSP:
* Catch the IllegalArgumentException thrown in parsing of invalid RTSP
Describe response messages
([#10971](https://github.com/google/ExoPlayer/issues/10971)).
* Metadata: * Metadata:
* Parse multiple null-separated values from ID3 frames, as permitted by * Parse multiple null-separated values from ID3 frames, as permitted by
ID3 v2.4. ID3 v2.4.

View File

@ -652,7 +652,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
default: default:
throw new IllegalStateException(); throw new IllegalStateException();
} }
} catch (ParserException e) { } catch (ParserException | IllegalArgumentException e) {
dispatchRtspError(new RtspPlaybackException(e)); dispatchRtspError(new RtspPlaybackException(e));
} }
} }

View File

@ -159,7 +159,8 @@ import com.google.common.collect.ImmutableMap;
* @param sessionUri The {@link Uri} of the RTSP playback session. * @param sessionUri The {@link Uri} of the RTSP playback session.
*/ */
public RtspMediaTrack(MediaDescription mediaDescription, Uri sessionUri) { public RtspMediaTrack(MediaDescription mediaDescription, Uri sessionUri) {
checkArgument(mediaDescription.attributes.containsKey(ATTR_CONTROL)); checkArgument(
mediaDescription.attributes.containsKey(ATTR_CONTROL), "missing attribute control");
payloadFormat = generatePayloadFormat(mediaDescription); payloadFormat = generatePayloadFormat(mediaDescription);
uri = extractTrackUri(sessionUri, castNonNull(mediaDescription.attributes.get(ATTR_CONTROL))); uri = extractTrackUri(sessionUri, castNonNull(mediaDescription.attributes.get(ATTR_CONTROL)));
} }
@ -210,7 +211,7 @@ import com.google.common.collect.ImmutableMap;
switch (mimeType) { switch (mimeType) {
case MimeTypes.AUDIO_AAC: case MimeTypes.AUDIO_AAC:
checkArgument(channelCount != C.INDEX_UNSET); checkArgument(channelCount != C.INDEX_UNSET);
checkArgument(!fmtpParameters.isEmpty()); checkArgument(!fmtpParameters.isEmpty(), "missing attribute fmtp");
if (mediaEncoding.equals(RtpPayloadFormat.RTP_MEDIA_MPEG4_LATM_AUDIO)) { if (mediaEncoding.equals(RtpPayloadFormat.RTP_MEDIA_MPEG4_LATM_AUDIO)) {
// cpresent is defined in RFC3016 Section 5.3. cpresent=0 means the config fmtp parameter // cpresent is defined in RFC3016 Section 5.3. cpresent=0 means the config fmtp parameter
// must exist. // must exist.
@ -259,11 +260,11 @@ import com.google.common.collect.ImmutableMap;
formatBuilder.setWidth(DEFAULT_H263_WIDTH).setHeight(DEFAULT_H263_HEIGHT); formatBuilder.setWidth(DEFAULT_H263_WIDTH).setHeight(DEFAULT_H263_HEIGHT);
break; break;
case MimeTypes.VIDEO_H264: case MimeTypes.VIDEO_H264:
checkArgument(!fmtpParameters.isEmpty()); checkArgument(!fmtpParameters.isEmpty(), "missing attribute fmtp");
processH264FmtpAttribute(formatBuilder, fmtpParameters); processH264FmtpAttribute(formatBuilder, fmtpParameters);
break; break;
case MimeTypes.VIDEO_H265: case MimeTypes.VIDEO_H265:
checkArgument(!fmtpParameters.isEmpty()); checkArgument(!fmtpParameters.isEmpty(), "missing attribute fmtp");
processH265FmtpAttribute(formatBuilder, fmtpParameters); processH265FmtpAttribute(formatBuilder, fmtpParameters);
break; break;
case MimeTypes.VIDEO_VP8: case MimeTypes.VIDEO_VP8:
@ -312,7 +313,8 @@ import com.google.common.collect.ImmutableMap;
ImmutableMap<String, String> fmtpAttributes, ImmutableMap<String, String> fmtpAttributes,
int channelCount, int channelCount,
int sampleRate) { int sampleRate) {
checkArgument(fmtpAttributes.containsKey(PARAMETER_PROFILE_LEVEL_ID)); checkArgument(
fmtpAttributes.containsKey(PARAMETER_PROFILE_LEVEL_ID), "missing profile-level-id param");
String profileLevel = checkNotNull(fmtpAttributes.get(PARAMETER_PROFILE_LEVEL_ID)); String profileLevel = checkNotNull(fmtpAttributes.get(PARAMETER_PROFILE_LEVEL_ID));
formatBuilder.setCodecs(AAC_CODECS_PREFIX + profileLevel); formatBuilder.setCodecs(AAC_CODECS_PREFIX + profileLevel);
formatBuilder.setInitializationData( formatBuilder.setInitializationData(
@ -380,10 +382,10 @@ import com.google.common.collect.ImmutableMap;
private static void processH264FmtpAttribute( private static void processH264FmtpAttribute(
Format.Builder formatBuilder, ImmutableMap<String, String> fmtpAttributes) { Format.Builder formatBuilder, ImmutableMap<String, String> fmtpAttributes) {
checkArgument(fmtpAttributes.containsKey(PARAMETER_SPROP_PARAMS)); checkArgument(fmtpAttributes.containsKey(PARAMETER_SPROP_PARAMS), "missing sprop parameter");
String spropParameterSets = checkNotNull(fmtpAttributes.get(PARAMETER_SPROP_PARAMS)); String spropParameterSets = checkNotNull(fmtpAttributes.get(PARAMETER_SPROP_PARAMS));
String[] parameterSets = Util.split(spropParameterSets, ","); String[] parameterSets = Util.split(spropParameterSets, ",");
checkArgument(parameterSets.length == 2); checkArgument(parameterSets.length == 2, "empty sprop value");
ImmutableList<byte[]> initializationData = ImmutableList<byte[]> initializationData =
ImmutableList.of( ImmutableList.of(
getInitializationDataFromParameterSet(parameterSets[0]), getInitializationDataFromParameterSet(parameterSets[0]),
@ -418,11 +420,14 @@ import com.google.common.collect.ImmutableMap;
maxDonDiff == 0, "non-zero sprop-max-don-diff " + maxDonDiff + " is not supported"); maxDonDiff == 0, "non-zero sprop-max-don-diff " + maxDonDiff + " is not supported");
} }
checkArgument(fmtpAttributes.containsKey(PARAMETER_H265_SPROP_VPS)); checkArgument(
fmtpAttributes.containsKey(PARAMETER_H265_SPROP_VPS), "missing sprop-vps parameter");
String spropVPS = checkNotNull(fmtpAttributes.get(PARAMETER_H265_SPROP_VPS)); String spropVPS = checkNotNull(fmtpAttributes.get(PARAMETER_H265_SPROP_VPS));
checkArgument(fmtpAttributes.containsKey(PARAMETER_H265_SPROP_SPS)); checkArgument(
fmtpAttributes.containsKey(PARAMETER_H265_SPROP_SPS), "missing sprop-sps parameter");
String spropSPS = checkNotNull(fmtpAttributes.get(PARAMETER_H265_SPROP_SPS)); String spropSPS = checkNotNull(fmtpAttributes.get(PARAMETER_H265_SPROP_SPS));
checkArgument(fmtpAttributes.containsKey(PARAMETER_H265_SPROP_PPS)); checkArgument(
fmtpAttributes.containsKey(PARAMETER_H265_SPROP_PPS), "missing sprop-pps parameter");
String spropPPS = checkNotNull(fmtpAttributes.get(PARAMETER_H265_SPROP_PPS)); String spropPPS = checkNotNull(fmtpAttributes.get(PARAMETER_H265_SPROP_PPS));
ImmutableList<byte[]> initializationData = ImmutableList<byte[]> initializationData =
ImmutableList.of( ImmutableList.of(

View File

@ -389,4 +389,68 @@ public final class RtspClientTest {
RobolectricUtil.runMainLooperUntil(timelineRequestFailed::get); RobolectricUtil.runMainLooperUntil(timelineRequestFailed::get);
assertThat(rtspClient.getState()).isEqualTo(RtspClient.RTSP_STATE_UNINITIALIZED); assertThat(rtspClient.getState()).isEqualTo(RtspClient.RTSP_STATE_UNINITIALIZED);
} }
@Test
public void connectServerAndClient_sdpInDescribeResponseHasInvalidFmtpAttr_doesNotUpdateTimeline()
throws Exception {
class ResponseProvider implements RtspServer.ResponseProvider {
@Override
public RtspResponse getOptionsResponse() {
return new RtspResponse(
/* status= */ 200,
new RtspHeaders.Builder().add(RtspHeaders.PUBLIC, "OPTIONS, DESCRIBE").build());
}
@Override
public RtspResponse getDescribeResponse(Uri requestedUri, RtspHeaders headers) {
String testMediaSdpInfo =
"v=0\r\n"
+ "o=- 1600785369059721 1 IN IP4 192.168.2.176\r\n"
+ "s=video, streamed by ExoPlayer\r\n"
+ "i=test.mkv\r\n"
+ "t=0 0\r\n"
+ "a=tool:ExoPlayer\r\n"
+ "a=type:broadcast\r\n"
+ "a=control:*\r\n"
+ "a=range:npt=0-30.102\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=\r\n"
+ "a=control:track1\r\n";
return RtspTestUtils.newDescribeResponseWithSdpMessage(
/* sessionDescription= */ testMediaSdpInfo,
// This session description has no tracks.
/* rtpPacketStreamDumps= */ ImmutableList.of(),
requestedUri);
}
}
rtspServer = new RtspServer(new ResponseProvider());
AtomicBoolean timelineRequestFailed = new AtomicBoolean();
rtspClient =
new RtspClient(
new SessionInfoListener() {
@Override
public void onSessionTimelineUpdated(
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks) {}
@Override
public void onSessionTimelineRequestFailed(
String message, @Nullable Throwable cause) {
timelineRequestFailed.set(true);
}
},
EMPTY_PLAYBACK_LISTENER,
/* userAgent= */ "ExoPlayer:RtspClientTest",
RtspTestUtils.getTestUri(rtspServer.startAndGetPortNumber()),
SocketFactory.getDefault(),
/* debugLoggingEnabled= */ false);
rtspClient.start();
RobolectricUtil.runMainLooperUntil(timelineRequestFailed::get);
assertThat(rtspClient.getState()).isEqualTo(RtspClient.RTSP_STATE_UNINITIALIZED);
}
} }