From be108b9e5ee9a3596c8d9d7183074eeedc498cba Mon Sep 17 00:00:00 2001 From: Manisha Jajoo Date: Thu, 3 Mar 2022 18:57:38 +0530 Subject: [PATCH 1/4] Add support for RTSP Opus Added Opus RTP packet reader and added support for Opus playback through RTSP Change-Id: Ib6702bd8aafd0bd782e89127ab907061ff06ccb3 --- .../exoplayer/rtsp/RtpPayloadFormat.java | 4 + .../media3/exoplayer/rtsp/RtspMediaTrack.java | 8 + .../DefaultRtpPayloadReaderFactory.java | 2 + .../exoplayer/rtsp/reader/RtpOpusReader.java | 152 ++++++++++++++++++ 4 files changed, 166 insertions(+) create mode 100644 libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpOpusReader.java 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 297353167b9..d1c56b3176b 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 @@ 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 static String getMimeTypeFromRtpMediaType(String mediaType) { 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 7547f1ea188..b3e033c365d 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 @@ public int hashCode() { 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 888939b7e89..cc78aaf1ec8 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 @@ public RtpPayloadReader createPayloadReader(RtpPayloadFormat payloadFormat) { 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 00000000000..de1c8af21bf --- /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); + } +} From 3a87039ba19a72dc6671573036a1d11a873476fc Mon Sep 17 00:00:00 2001 From: Manisha Jajoo Date: Thu, 24 Mar 2022 01:39:10 +0530 Subject: [PATCH 2/4] Fixed review comments in RtpOpusReader --- .../exoplayer/rtsp/RtpPayloadFormat.java | 6 +-- .../media3/exoplayer/rtsp/RtspMediaTrack.java | 10 +++-- .../exoplayer/rtsp/reader/RtpOpusReader.java | 41 ++++++++----------- 3 files changed, 25 insertions(+), 32 deletions(-) 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 d1c56b3176b..2e97fca3d5d 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,19 +37,19 @@ 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"; + private static final String RTP_MEDIA_MPEG4_GENERIC = "MPEG4-GENERIC"; + private static final String RTP_MEDIA_OPUS = "OPUS"; /** Returns whether the format of a {@link MediaDescription} is supported. */ 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: + case RTP_MEDIA_OPUS: return true; default: return false; 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 b3e033c365d..87c8e798936 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 @@ -56,6 +56,9 @@ private static final String GENERIC_CONTROL_ATTR = "*"; + /** RFC7587 Section 6.1 Sampling rate for OPUS is fixed at 48KHz. */ + private static final int OPUS_SAMPLING_RATE = 48000; + /** The track's associated {@link RtpPayloadFormat}. */ public final RtpPayloadFormat payloadFormat; /** The track's URI. */ @@ -122,12 +125,11 @@ public int hashCode() { processAacFmtpAttribute(formatBuilder, fmtpParameters, channelCount, clockRate); break; case MimeTypes.AUDIO_OPUS: - // RFC7587 Section 7 - checkArgument(channelCount == 2, "Invalid channel count"); - // RFC7587 Section 6.1 + checkArgument(channelCount != C.INDEX_UNSET); + // 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"); + checkArgument(clockRate == OPUS_SAMPLING_RATE, "Invalid sampling rate"); break; case MimeTypes.VIDEO_H264: checkArgument(!fmtpParameters.isEmpty()); 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 index de1c8af21bf..460f074a8d7 100644 --- 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 @@ -39,11 +39,11 @@ private static final String TAG = "RtpOpusReader"; private final RtpPayloadFormat payloadFormat; + private static final long MEDIA_CLOCK_FREQUENCY = 48_000; + private @MonotonicNonNull TrackOutput trackOutput; private long firstReceivedTimestamp; private long startTimeOffsetUs; - - private final int sampleRate; private int previousSequenceNumber; private boolean foundOpusIDHeader; private boolean foundOpusCommentHeader; @@ -51,7 +51,6 @@ 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; @@ -75,23 +74,20 @@ public void consume( ParsableByteArray data, long timestamp, int sequenceNumber, boolean rtpMarker) { checkStateNotNull(trackOutput); - /* RFC7845 Section 3 + /* 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); + checkForOpusIdHeader(data); 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 + // Comment Header RFC7845 Section 5.2. String header = data.readString(8); checkArgument(header.equals("OpusTags"), "Comment Header should follow ID Header"); foundOpusCommentHeader = true; @@ -106,12 +102,13 @@ public void consume( expectedSequenceNumber, sequenceNumber)); } - // sending opus data + // 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); + toSampleTimeUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp); + trackOutput.sampleMetadata( + timeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset*/ 0, /* cryptoData*/ null); } previousSequenceNumber = sequenceNumber; } @@ -124,29 +121,23 @@ public void seek(long nextRtpTimestamp, long timeUs) { // Internal methods. - private static boolean isOpusIDHeader(ParsableByteArray data) { + private static void checkForOpusIdHeader(ParsableByteArray data) { + int currPosition = data.getPosition(); 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; - } + // Identification header RFC7845 Section 5.1. + checkArgument(sampleSize > 18 && header.equals("OpusHead"), "ID Header missing"); checkArgument(data.readUnsignedByte() == 1, "version number must always be 1"); - return true; + data.setPosition(currPosition); } /** 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) { + long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp) { return startTimeOffsetUs + Util.scaleLargeTimestamp( rtpTimestamp - firstReceivedRtpTimestamp, /* multiplier= */ C.MICROS_PER_SECOND, - /* divisor= */ sampleRate); + /* divisor= */ MEDIA_CLOCK_FREQUENCY); } } From 0bf197341db7a56f9ba8a3d2610e5dff04150438 Mon Sep 17 00:00:00 2001 From: manisha_jajoo Date: Thu, 21 Apr 2022 11:11:03 +0530 Subject: [PATCH 3/4] Add a missing break in RtspMediaTrack and update RtpPayloadFormat.java --- .../java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java | 2 +- .../java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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 f174b61aa4c..0419cac4bbf 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 @@ -46,9 +46,9 @@ public final class RtpPayloadFormat { private static final String RTP_MEDIA_MPEG4_VIDEO = "MP4V-ES"; private static final String RTP_MEDIA_H264 = "H264"; private static final String RTP_MEDIA_H265 = "H265"; + private static final String RTP_MEDIA_OPUS = "OPUS"; private static final String RTP_MEDIA_PCM_L8 = "L8"; private static final String RTP_MEDIA_PCM_L16 = "L16"; - private static final String RTP_MEDIA_OPUS = "OPUS"; private static final String RTP_MEDIA_PCMA = "PCMA"; private static final String RTP_MEDIA_PCMU = "PCMU"; private static final String RTP_MEDIA_VP8 = "VP8"; 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 c04ad071258..1b6fb6ce1ea 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 @@ -189,6 +189,7 @@ public int hashCode() { // the RTP timestamp is incremented with a 48000 Hz clock rate // for all modes of Opus and all sampling rates. checkArgument(clockRate == OPUS_SAMPLING_RATE, "Invalid sampling rate"); + break; case MimeTypes.VIDEO_MP4V: checkArgument(!fmtpParameters.isEmpty()); processMPEG4FmtpAttribute(formatBuilder, fmtpParameters); From 165e706aa954473f7b9d8079debd24b13f60678d Mon Sep 17 00:00:00 2001 From: Shraddha Basantwani Date: Fri, 8 Apr 2022 17:47:28 +0530 Subject: [PATCH 4/4] Add RTP Opus Reader Test Change-Id: I189811c9bef9d11e93472c755bc19dee5dc3ee7c --- .../exoplayer/rtsp/reader/RtpOpusReader.java | 5 +- .../rtsp/reader/RtpOpusReaderTest.java | 202 ++++++++++++++++++ 2 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpOpusReaderTest.java 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 index 460f074a8d7..17220c63ee7 100644 --- 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 @@ -88,6 +88,8 @@ public void consume( foundOpusIDHeader = true; } else if (!foundOpusCommentHeader) { // Comment Header RFC7845 Section 5.2. + int sampleSize = data.limit(); + checkArgument(sampleSize >= 8 , "Comment Header has insufficient data"); String header = data.readString(8); checkArgument(header.equals("OpusTags"), "Comment Header should follow ID Header"); foundOpusCommentHeader = true; @@ -124,9 +126,10 @@ public void seek(long nextRtpTimestamp, long timeUs) { private static void checkForOpusIdHeader(ParsableByteArray data) { int currPosition = data.getPosition(); int sampleSize = data.limit(); + checkArgument(sampleSize > 18, "ID Header has insufficient data"); String header = data.readString(8); // Identification header RFC7845 Section 5.1. - checkArgument(sampleSize > 18 && header.equals("OpusHead"), "ID Header missing"); + checkArgument(header.equals("OpusHead"), "ID Header missing"); checkArgument(data.readUnsignedByte() == 1, "version number must always be 1"); data.setPosition(currPosition); } diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpOpusReaderTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpOpusReaderTest.java new file mode 100644 index 00000000000..b72f40b4a27 --- /dev/null +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpOpusReaderTest.java @@ -0,0 +1,202 @@ +/* + * 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.Util.getBytesFromHexString; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; + +import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.exoplayer.rtsp.RtpPacket; +import androidx.media3.exoplayer.rtsp.RtpPayloadFormat; +import androidx.media3.extractor.ExtractorOutput; +import androidx.media3.test.utils.FakeTrackOutput; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableMap; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.function.ThrowingRunnable; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** + * Unit test for {@link RtpOpusReader}. + */ +@RunWith(AndroidJUnit4.class) +public final class RtpOpusReaderTest { + + private static final RtpPayloadFormat OPUS_FORMAT = + new RtpPayloadFormat( + new Format.Builder() + .setChannelCount(6) + .setSampleMimeType(MimeTypes.AUDIO_OPUS) + .setSampleRate(48_000) + .build(), + /* rtpPayloadType= */ 97, + /* clockRate= */ 48_000, + /* fmtpParameters= */ ImmutableMap.of()); + + private final RtpPacket opusHeader = + createRtpPacket( + /* timestamp= */ 2599168056L, + /* sequenceNumber= */ 40289, + /* payloadData= */ getBytesFromHexString("4F707573486561640102000000000000000000")); + private final RtpPacket opusTags = + createRtpPacket( + /* timestamp= */ 2599168056L, + /* sequenceNumber= */ 40290, + /* payloadData= */ getBytesFromHexString("4F707573546167730000000000000000000000")); + private final RtpPacket frame1 = + createRtpPacket( + /* timestamp= */ 2599168056L, + /* sequenceNumber= */ 40292, + /* payloadData= */ getBytesFromHexString("010203")); + private final RtpPacket frame2 = + createRtpPacket( + /* timestamp= */ 2599169592L, + /* sequenceNumber= */ 40293, + /* payloadData= */ getBytesFromHexString("04050607")); + + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + private ParsableByteArray packetData; + + private RtpOpusReader opusReader; + private FakeTrackOutput trackOutput; + @Mock + private ExtractorOutput extractorOutput; + + @Before + public void setUp() { + packetData = new ParsableByteArray(); + trackOutput = new FakeTrackOutput(/* deduplicateConsecutiveFormats= */ true); + when(extractorOutput.track(anyInt(), anyInt())).thenReturn(trackOutput); + opusReader = new RtpOpusReader(OPUS_FORMAT); + opusReader.createTracks(extractorOutput, /* trackId= */ 0); + } + + @Test + public void consume_validPackets() { + opusReader.onReceivingFirstPacket(opusHeader.timestamp, opusHeader.sequenceNumber); + consume(opusHeader); + consume(opusTags); + consume(frame1); + consume(frame2); + + assertThat(trackOutput.getSampleCount()).isEqualTo(2); + assertThat(trackOutput.getSampleData(0)).isEqualTo(getBytesFromHexString("010203")); + assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0); + assertThat(trackOutput.getSampleData(1)).isEqualTo(getBytesFromHexString("04050607")); + assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(32000); + } + + @Test + public void consume_OpusHeader_invalidHeader() { + opusReader.onReceivingFirstPacket(opusHeader.timestamp, opusHeader.sequenceNumber); + // Modify "OpusHead" -> "OrusHead" (First 8 bytes) + assertExceptionMessage( + () -> consume(opusHeader, getBytesFromHexString("4F727573486561640102000000000000000000")), + "ID Header missing"); + } + + @Test + public void consume_OpusHeader_invalidSampleSize() { + opusReader.onReceivingFirstPacket(opusHeader.timestamp, opusHeader.sequenceNumber); + // Truncate the opusHeader payload data + assertExceptionMessage( + () -> consume(opusHeader, getBytesFromHexString("4F707573486561640102")), + "ID Header has insufficient data"); + } + + @Test + public void consume_OpusHeader_invalidVersionNumber() { + opusReader.onReceivingFirstPacket(opusHeader.timestamp, opusHeader.sequenceNumber); + // Modify version 1 -> 2 (9th byte) + assertExceptionMessage( + () -> consume(opusHeader, getBytesFromHexString("4F707573486561640202000000000000000000")), + "version number must always be 1"); + } + + @Test + public void consume_invalidOpusTags() { + opusReader.onReceivingFirstPacket(opusHeader.timestamp, opusHeader.sequenceNumber); + consume(opusHeader); + // Modify "OpusTags" -> "OpusTggs" (First 8 bytes) + assertExceptionMessage( + () -> consume(opusTags, getBytesFromHexString("4F70757354676773")), + "Comment Header should follow ID Header"); + } + + @Test + public void consume_skipOpusTags() { + opusReader.onReceivingFirstPacket(opusHeader.timestamp, opusHeader.sequenceNumber); + consume(opusHeader); + assertExceptionMessage( + () -> consume(frame1), + "Comment Header has insufficient data"); + } + + @Test + public void consume_skipOpusHeader() { + opusReader.onReceivingFirstPacket(opusHeader.timestamp, opusHeader.sequenceNumber); + assertExceptionMessage( + () -> consume(opusTags), + "ID Header missing"); + } + + @Test + public void consume_skipOpusHeaderAndOpusTags() { + opusReader.onReceivingFirstPacket(opusHeader.timestamp, opusHeader.sequenceNumber); + assertExceptionMessage( + () -> consume(frame1), + "ID Header has insufficient data"); + } + + private static RtpPacket createRtpPacket(long timestamp, int sequenceNumber, byte[] payloadData) { + return new RtpPacket.Builder() + .setTimestamp((int) timestamp) + .setSequenceNumber(sequenceNumber) + .setMarker(false) + .setPayloadData(payloadData) + .build(); + } + + private void assertExceptionMessage(ThrowingRunnable runnable, String expectedExceptionMessage) { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, runnable); + assertThat(exception.getMessage()).isEqualTo(expectedExceptionMessage); + } + + private void consume(RtpPacket rtpPacket) { + consume(rtpPacket, rtpPacket.payloadData); + } + + private void consume(RtpPacket rtpPacket, byte[] payloadData) { + packetData.reset(payloadData); + opusReader.consume( + packetData, + rtpPacket.timestamp, + rtpPacket.sequenceNumber, + /* isFrameBoundary= */ rtpPacket.marker); + } +}