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..d1c56b3176 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 @@ -37,6 +37,7 @@ import java.util.Map; public final class RtpPayloadFormat { private static final String RTP_MEDIA_AC3 = "AC3"; + private static final String RTP_MEDIA_OPUS = "OPUS"; 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"; @@ -45,6 +46,7 @@ public final class RtpPayloadFormat { public static boolean isFormatSupported(MediaDescription mediaDescription) { switch (Ascii.toUpperCase(mediaDescription.rtpMapAttribute.mediaEncoding)) { case RTP_MEDIA_AC3: + case RTP_MEDIA_OPUS: case RTP_MEDIA_H264: case RTP_MEDIA_H265: case RTP_MEDIA_MPEG4_GENERIC: @@ -71,6 +73,8 @@ public final class RtpPayloadFormat { return MimeTypes.VIDEO_H265; case RTP_MEDIA_MPEG4_GENERIC: return MimeTypes.AUDIO_AAC; + case RTP_MEDIA_OPUS: + return MimeTypes.AUDIO_OPUS; 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..b3e033c365 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 @@ -121,6 +121,14 @@ import com.google.common.collect.ImmutableMap; checkArgument(!fmtpParameters.isEmpty()); processAacFmtpAttribute(formatBuilder, fmtpParameters, channelCount, clockRate); break; + case MimeTypes.AUDIO_OPUS: + // RFC7587 Section 7 + checkArgument(channelCount == 2, "Invalid channel count"); + // RFC7587 Section 6.1 + // the RTP timestamp is incremented with a 48000 Hz clock rate + // for all modes of Opus and all sampling rates. + checkArgument(clockRate == 48000, "Invalid sampling rate"); + break; case MimeTypes.VIDEO_H264: checkArgument(!fmtpParameters.isEmpty()); processH264FmtpAttribute(formatBuilder, fmtpParameters); 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..cc78aaf1ec 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 @@ -36,6 +36,8 @@ import androidx.media3.exoplayer.rtsp.RtpPayloadFormat; return new RtpAc3Reader(payloadFormat); case MimeTypes.AUDIO_AAC: return new RtpAacReader(payloadFormat); + case MimeTypes.AUDIO_OPUS: + return new RtpOpusReader(payloadFormat); case MimeTypes.VIDEO_H264: return new RtpH264Reader(payloadFormat); case MimeTypes.VIDEO_H265: diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpOpusReader.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpOpusReader.java new file mode 100644 index 0000000000..de1c8af21b --- /dev/null +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpOpusReader.java @@ -0,0 +1,152 @@ +/* + * 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.checkArgument; +import static androidx.media3.common.util.Assertions.checkStateNotNull; + +import androidx.media3.common.C; +import androidx.media3.common.Format; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.rtsp.RtpPacket; +import androidx.media3.exoplayer.rtsp.RtpPayloadFormat; +import androidx.media3.extractor.ExtractorOutput; +import androidx.media3.extractor.OpusUtil; +import androidx.media3.extractor.TrackOutput; +import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Parses an OPUS byte stream carried on RTP packets and extracts individual samples. Refer to + * RFC7845 for more details. + */ +/* package */ final class RtpOpusReader implements RtpPayloadReader { + private static final String TAG = "RtpOpusReader"; + + private final RtpPayloadFormat payloadFormat; + private @MonotonicNonNull TrackOutput trackOutput; + private long firstReceivedTimestamp; + private long startTimeOffsetUs; + + private final int sampleRate; + private int previousSequenceNumber; + private boolean foundOpusIDHeader; + private boolean foundOpusCommentHeader; + + public RtpOpusReader(RtpPayloadFormat payloadFormat) { + this.payloadFormat = payloadFormat; + this.firstReceivedTimestamp = C.INDEX_UNSET; + this.sampleRate = this.payloadFormat.clockRate; + this.previousSequenceNumber = C.INDEX_UNSET; + this.foundOpusIDHeader = false; + this.foundOpusCommentHeader = false; + } + + // RtpPayloadReader implementation. + + @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) { + this.firstReceivedTimestamp = timestamp; + } + + @Override + public void consume( + ParsableByteArray data, long timestamp, int sequenceNumber, boolean rtpMarker) { + checkStateNotNull(trackOutput); + + /* RFC7845 Section 3 + * +---------+ +----------------+ +--------------------+ +----- + * |ID Header| | Comment Header | |Audio Data Packet 1 | | ... + * +---------+ +----------------+ +--------------------+ +----- + */ + if (!foundOpusIDHeader) { + int currPosition = data.getPosition(); + checkArgument(isOpusIDHeader(data), "ID Header missing"); + + data.setPosition(currPosition); + List initializationData = OpusUtil.buildInitializationData(data.getData()); + Format.Builder formatBuilder = payloadFormat.format.buildUpon(); + formatBuilder.setInitializationData(initializationData); + trackOutput.format(formatBuilder.build()); + foundOpusIDHeader = true; + } else if (!foundOpusCommentHeader) { + // Comment Header RFC7845 Section 5.2 + String header = data.readString(8); + checkArgument(header.equals("OpusTags"), "Comment Header should follow ID Header"); + foundOpusCommentHeader = true; + } else { + // Check that this packet is in the sequence of the previous packet. + int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber); + if (sequenceNumber != expectedSequenceNumber) { + Log.w( + TAG, + Util.formatInvariant( + "Received RTP packet with unexpected sequence number. Expected: %d; received: %d.", + expectedSequenceNumber, sequenceNumber)); + } + + // sending opus data + int size = data.bytesLeft(); + trackOutput.sampleData(data, size); + long timeUs = + toSampleTimeUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp, sampleRate); + trackOutput.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, size, 0, null); + } + previousSequenceNumber = sequenceNumber; + } + + @Override + public void seek(long nextRtpTimestamp, long timeUs) { + firstReceivedTimestamp = nextRtpTimestamp; + startTimeOffsetUs = timeUs; + } + + // Internal methods. + + private static boolean isOpusIDHeader(ParsableByteArray data) { + int sampleSize = data.limit(); + String header = data.readString(8); + // Identification header RFC7845 Section 5.1 + if (sampleSize < 19 || !header.equals("OpusHead")) { + Log.e( + TAG, + Util.formatInvariant( + "first data octet of the RTP packet is not the beginning of a OpusHeader " + + "Dropping current packet")); + return false; + } + checkArgument(data.readUnsignedByte() == 1, "version number must always be 1"); + return true; + } + + /** Returns the correct sample time from RTP timestamp, accounting for the OPUS sampling rate. */ + private static long toSampleTimeUs( + long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp, int sampleRate) { + return startTimeOffsetUs + + Util.scaleLargeTimestamp( + rtpTimestamp - firstReceivedRtpTimestamp, + /* multiplier= */ C.MICROS_PER_SECOND, + /* divisor= */ sampleRate); + } +}