diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java index 297353167b..d835c64751 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java @@ -16,6 +16,7 @@ package androidx.media3.exoplayer.rtsp; import androidx.annotation.Nullable; +import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.UnstableApi; @@ -40,6 +41,10 @@ public final class RtpPayloadFormat { private static final String RTP_MEDIA_MPEG4_GENERIC = "MPEG4-GENERIC"; private static final String RTP_MEDIA_H264 = "H264"; private static final String RTP_MEDIA_H265 = "H265"; + private static final String RTP_MEDIA_PCM_L8 = "L8"; + private static final String RTP_MEDIA_PCM_L16 = "L16"; + private static final String RTP_MEDIA_PCMA = "PCMA"; + private static final String RTP_MEDIA_PCMU = "PCMU"; /** Returns whether the format of a {@link MediaDescription} is supported. */ public static boolean isFormatSupported(MediaDescription mediaDescription) { @@ -48,6 +53,10 @@ public final class RtpPayloadFormat { case RTP_MEDIA_H264: case RTP_MEDIA_H265: case RTP_MEDIA_MPEG4_GENERIC: + case RTP_MEDIA_PCM_L8: + case RTP_MEDIA_PCM_L16: + case RTP_MEDIA_PCMA: + case RTP_MEDIA_PCMU: return true; default: return false; @@ -71,6 +80,33 @@ public final class RtpPayloadFormat { return MimeTypes.VIDEO_H265; case RTP_MEDIA_MPEG4_GENERIC: return MimeTypes.AUDIO_AAC; + case RTP_MEDIA_PCM_L8: + case RTP_MEDIA_PCM_L16: + return MimeTypes.AUDIO_RAW; + case RTP_MEDIA_PCMA: + return MimeTypes.AUDIO_ALAW; + case RTP_MEDIA_PCMU: + return MimeTypes.AUDIO_MLAW; + default: + throw new IllegalArgumentException(mediaType); + } + } + + /** + * Gets the PCM Encoding type for RAW track that is associated with the RTP media type. + * + *

For instance, RTP media type "L8" maps to {@link C.PcmEncoding#ENCODING_PCM_8BIT}. + * + * @throws IllegalArgumentException When the media type is not supported/recognized. + */ + public static int getPCMEncodingFromRtpMediaType(String mediaType) { + switch (mediaType) { + case RTP_MEDIA_PCM_L8: + // Refer to RFC3551#section-4.5.10 + return C.ENCODING_PCM_8BIT; + case RTP_MEDIA_PCM_L16: + // Refer to RFC3551#section-4.5.11 + return C.ENCODING_PCM_16BIT_BIG_ENDIAN; default: throw new IllegalArgumentException(mediaType); } diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java index 7547f1ea18..a702260f28 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java @@ -20,6 +20,7 @@ import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Util.castNonNull; import static androidx.media3.exoplayer.rtsp.MediaDescription.MEDIA_TYPE_AUDIO; import static androidx.media3.exoplayer.rtsp.RtpPayloadFormat.getMimeTypeFromRtpMediaType; +import static androidx.media3.exoplayer.rtsp.RtpPayloadFormat.getPCMEncodingFromRtpMediaType; import static androidx.media3.exoplayer.rtsp.SessionDescription.ATTR_CONTROL; import static androidx.media3.extractor.NalUnitUtil.NAL_START_CODE; @@ -129,8 +130,15 @@ import com.google.common.collect.ImmutableMap; checkArgument(!fmtpParameters.isEmpty()); processH265FmtpAttribute(formatBuilder, fmtpParameters); break; + case MimeTypes.AUDIO_RAW: + int pcmEncoding = + getPCMEncodingFromRtpMediaType(mediaDescription.rtpMapAttribute.mediaEncoding); + formatBuilder.setPcmEncoding(pcmEncoding); + break; case MimeTypes.AUDIO_AC3: - // AC3 does not require a FMTP attribute. Fall through. + case MimeTypes.AUDIO_ALAW: + case MimeTypes.AUDIO_MLAW: + // Does not require a FMTP attribute. Fall through. default: // Do nothing. } diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/DefaultRtpPayloadReaderFactory.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/DefaultRtpPayloadReaderFactory.java index 888939b7e8..5c563e31a8 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/DefaultRtpPayloadReaderFactory.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/DefaultRtpPayloadReaderFactory.java @@ -40,6 +40,10 @@ import androidx.media3.exoplayer.rtsp.RtpPayloadFormat; return new RtpH264Reader(payloadFormat); case MimeTypes.VIDEO_H265: return new RtpH265Reader(payloadFormat); + case MimeTypes.AUDIO_RAW: + case MimeTypes.AUDIO_ALAW: + case MimeTypes.AUDIO_MLAW: + return new RtpPCMReader(payloadFormat); default: // No supported reader, returning null. } diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpPCMReader.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpPCMReader.java new file mode 100644 index 0000000000..a80d1d9717 --- /dev/null +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpPCMReader.java @@ -0,0 +1,89 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.exoplayer.rtsp.reader; + +import static androidx.media3.common.util.Assertions.checkState; + +import androidx.media3.common.C; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.rtsp.RtpPayloadFormat; +import androidx.media3.extractor.ExtractorOutput; +import androidx.media3.extractor.TrackOutput; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Parses byte stream carried on RTP packets, and extracts PCM frames. Refer to RFC3551 for more + * details. + */ +/* package */ public final class RtpPCMReader implements RtpPayloadReader { + + private final RtpPayloadFormat payloadFormat; + private @MonotonicNonNull TrackOutput trackOutput; + private long firstReceivedTimestamp; + private long startTimeOffsetUs; + + public RtpPCMReader(RtpPayloadFormat payloadFormat) { + this.payloadFormat = payloadFormat; + firstReceivedTimestamp = C.TIME_UNSET; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, int trackId) { + trackOutput = extractorOutput.track(trackId, C.TRACK_TYPE_AUDIO); + trackOutput.format(payloadFormat.format); + } + + @Override + public void onReceivingFirstPacket(long timestamp, int sequenceNumber) { + checkState(firstReceivedTimestamp == C.TIME_UNSET); + firstReceivedTimestamp = timestamp; + } + + @Override + public void consume( + ParsableByteArray data, long timestamp, int sequenceNumber, boolean rtpMarker) { + long sampleTimeUs = + toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp, payloadFormat.clockRate); + int size = data.bytesLeft(); + trackOutput.sampleData(data, size); + trackOutput + .sampleMetadata( + /* timeUs= */ sampleTimeUs, + /* flags= */ C.BUFFER_FLAG_KEY_FRAME, + /* size= */ size, + /* offset= */ 0, + /* cryptoData= */ null); + } + + @Override + public void seek(long nextRtpTimestamp, long timeUs) { + firstReceivedTimestamp = nextRtpTimestamp; + startTimeOffsetUs = timeUs; + } + + /** + * Returns the correct sample time from RTP timestamp, accounting for the given sampling rate. + */ + private static long toSampleUs( + long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp, int sampleRate) { + return startTimeOffsetUs + + Util.scaleLargeTimestamp( + rtpTimestamp - firstReceivedRtpTimestamp, + /* multiplier= */ C.MICROS_PER_SECOND, + /* divisor= */ sampleRate); + } +}