diff --git a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/MediaDescription.java b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/MediaDescription.java index 6ef06c1a4f..2b57e92ea8 100644 --- a/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/MediaDescription.java +++ b/library/rtsp/src/main/java/com/google/android/exoplayer2/source/rtsp/MediaDescription.java @@ -20,7 +20,6 @@ import static com.google.android.exoplayer2.source.rtsp.RtspMessageUtil.parseInt import static com.google.android.exoplayer2.source.rtsp.SessionDescription.ATTR_FMTP; import static com.google.android.exoplayer2.source.rtsp.SessionDescription.ATTR_RTPMAP; import static com.google.android.exoplayer2.util.Assertions.checkArgument; -import static com.google.android.exoplayer2.util.Assertions.checkState; import static com.google.android.exoplayer2.util.Util.castNonNull; import androidx.annotation.Nullable; @@ -103,6 +102,17 @@ import java.util.HashMap; /** Builder class for {@link MediaDescription}. */ public static final class Builder { + + /** + * RTPMAP attribute format: {@code //}. + */ + private static final String RTP_MAP_ATTR_AUDIO_FMT = "%d %s/%d/%d"; + + private static final int RTP_STATIC_PAYLOAD_TYPE_PCMU = 0; + private static final int RTP_STATIC_PAYLOAD_TYPE_PCMA = 8; + private static final int RTP_STATIC_PAYLOAD_TYPE_L16_STEREO = 10; + private static final int RTP_STATIC_PAYLOAD_TYPE_L16_MONO = 11; + private final String mediaType; private final int port; private final String transportProtocol; @@ -197,15 +207,55 @@ import java.util.HashMap; */ public MediaDescription build() { try { - // rtpmap attribute is mandatory in RTSP (RFC2326 Section C.1.3). - checkState(attributes.containsKey(ATTR_RTPMAP)); RtpMapAttribute rtpMapAttribute = - RtpMapAttribute.parse(castNonNull(attributes.get(ATTR_RTPMAP))); + attributes.containsKey(ATTR_RTPMAP) + ? RtpMapAttribute.parse(castNonNull(attributes.get(ATTR_RTPMAP))) + : RtpMapAttribute.parse(getRtpMapStringByPayloadType(payloadType)); return new MediaDescription(this, ImmutableMap.copyOf(attributes), rtpMapAttribute); } catch (ParserException e) { throw new IllegalStateException(e); } } + + private static String getRtpMapStringByPayloadType(int rtpPayloadType) { + checkArgument(rtpPayloadType < 96); + + switch (rtpPayloadType) { + // See RFC3551 Section 6. + case RTP_STATIC_PAYLOAD_TYPE_PCMU: + return constructAudioRtpMap( + RTP_STATIC_PAYLOAD_TYPE_PCMU, + /* mediaEncoding= */ "PCMU", + /* clockRate= */ 8_000, + /* channelCount= */ 1); + case RTP_STATIC_PAYLOAD_TYPE_PCMA: + return constructAudioRtpMap( + RTP_STATIC_PAYLOAD_TYPE_PCMA, + /* mediaEncoding= */ "PCMA", + /* clockRate= */ 8_000, + /* channelCount= */ 1); + case RTP_STATIC_PAYLOAD_TYPE_L16_STEREO: + return constructAudioRtpMap( + RTP_STATIC_PAYLOAD_TYPE_L16_STEREO, + /* mediaEncoding= */ "L16", + /* clockRate= */ 44_100, + /* channelCount= */ 2); + case RTP_STATIC_PAYLOAD_TYPE_L16_MONO: + return constructAudioRtpMap( + RTP_STATIC_PAYLOAD_TYPE_L16_MONO, + /* mediaEncoding= */ "L16", + /* clockRate= */ 44_100, + /* channelCount= */ 1); + default: + throw new IllegalStateException("Unsupported static paylod type " + rtpPayloadType); + } + } + + private static String constructAudioRtpMap( + int payloadType, String mediaEncoding, int clockRate, int channelCount) { + return Util.formatInvariant( + RTP_MAP_ATTR_AUDIO_FMT, payloadType, mediaEncoding, clockRate, channelCount); + } } /** The media types allowed in a SDP media description. */ diff --git a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMediaTrackTest.java b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMediaTrackTest.java index 33ef70eaf0..c2b88db37f 100644 --- a/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMediaTrackTest.java +++ b/library/rtsp/src/test/java/com/google/android/exoplayer2/source/rtsp/RtspMediaTrackTest.java @@ -26,6 +26,7 @@ import static org.junit.Assert.assertThrows; import android.net.Uri; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AacUtil; import com.google.android.exoplayer2.util.MimeTypes; @@ -80,6 +81,132 @@ public class RtspMediaTrackTest { assertThat(format).isEqualTo(expectedFormat); } + @Test + public void generatePayloadFormat_withPcmuMediaDescription_succeeds() { + + MediaDescription mediaDescription = + new MediaDescription.Builder( + MEDIA_TYPE_AUDIO, /* port= */ 0, RTP_AVP_PROFILE, /* payloadType= */ 0) + .setConnection("IN IP4 0.0.0.0") + .addAttribute(ATTR_CONTROL, "track2") + .build(); + + RtpPayloadFormat format = RtspMediaTrack.generatePayloadFormat(mediaDescription); + RtpPayloadFormat expectedFormat = + new RtpPayloadFormat( + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_MLAW) + .setChannelCount(1) + .setSampleRate(8_000) + .build(), + /* rtpPayloadType= */ 0, + /* clockRate= */ 8_000, + /* fmtpParameters= */ ImmutableMap.of()); + + assertThat(format).isEqualTo(expectedFormat); + } + + @Test + public void generatePayloadFormat_withPcmaMediaDescription_succeeds() { + // m=audio 0 RTP/AVP 0 + // c=IN IP4 0.0.0.0 + // a=control:track2 + int pcmaPayloadType = 8; + int pcmaClockRate = 8_000; + + MediaDescription mediaDescription = + new MediaDescription.Builder( + MEDIA_TYPE_AUDIO, + /* port= */ 0, + RTP_AVP_PROFILE, + /* payloadType= */ pcmaPayloadType) + .setConnection("IN IP4 0.0.0.0") + .addAttribute(ATTR_CONTROL, "track2") + .build(); + + RtpPayloadFormat format = RtspMediaTrack.generatePayloadFormat(mediaDescription); + RtpPayloadFormat expectedFormat = + new RtpPayloadFormat( + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_ALAW) + .setChannelCount(1) + .setSampleRate(pcmaClockRate) + .build(), + /* rtpPayloadType= */ pcmaPayloadType, + /* clockRate= */ pcmaClockRate, + /* fmtpParameters= */ ImmutableMap.of()); + + assertThat(format).isEqualTo(expectedFormat); + } + + @Test + public void generatePayloadFormat_withL16StereoMediaDescription_succeeds() { + // m=audio 0 RTP/AVP 0 + // c=IN IP4 0.0.0.0 + // a=control:track2 + int l16StereoPayloadType = 10; + int l16StereoClockRate = 44_100; + + MediaDescription mediaDescription = + new MediaDescription.Builder( + MEDIA_TYPE_AUDIO, + /* port= */ 0, + RTP_AVP_PROFILE, + /* payloadType= */ l16StereoPayloadType) + .setConnection("IN IP4 0.0.0.0") + .addAttribute(ATTR_CONTROL, "track2") + .build(); + + RtpPayloadFormat format = RtspMediaTrack.generatePayloadFormat(mediaDescription); + RtpPayloadFormat expectedFormat = + new RtpPayloadFormat( + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_RAW) + .setChannelCount(2) + .setSampleRate(l16StereoClockRate) + .setPcmEncoding(C.ENCODING_PCM_16BIT_BIG_ENDIAN) + .build(), + /* rtpPayloadType= */ l16StereoPayloadType, + /* clockRate= */ l16StereoClockRate, + /* fmtpParameters= */ ImmutableMap.of()); + + assertThat(format).isEqualTo(expectedFormat); + } + + @Test + public void generatePayloadFormat_withL16MonoMediaDescription_succeeds() { + // m=audio 0 RTP/AVP 0 + // c=IN IP4 0.0.0.0 + // a=control:track2 + int l16MonoPayloadType = 11; + int l16MonoClockRate = 44_100; + + MediaDescription mediaDescription = + new MediaDescription.Builder( + MEDIA_TYPE_AUDIO, + /* port= */ 0, + RTP_AVP_PROFILE, + /* payloadType= */ l16MonoPayloadType) + .setConnection("IN IP4 0.0.0.0") + .addAttribute(ATTR_CONTROL, "track2") + .build(); + + RtpPayloadFormat format = RtspMediaTrack.generatePayloadFormat(mediaDescription); + RtpPayloadFormat expectedFormat = + new RtpPayloadFormat( + new Format.Builder() + .setSampleMimeType(MimeTypes.AUDIO_RAW) + .setChannelCount(1) + .setSampleRate(l16MonoClockRate) + .setPcmEncoding(C.ENCODING_PCM_16BIT_BIG_ENDIAN) + .build(), + /* rtpPayloadType= */ l16MonoPayloadType, + /* clockRate= */ l16MonoClockRate, + /* fmtpParameters= */ ImmutableMap.of()); + + assertThat(format).isEqualTo(expectedFormat); + } + @Test public void generatePayloadFormat_withFmtpTrailingSemicolon_succeeds() { MediaDescription mediaDescription =