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 index f8b1cb2541..9d3402c94c 100644 --- 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 @@ -17,9 +17,11 @@ package androidx.media3.exoplayer.rtsp.reader; import static androidx.media3.common.util.Assertions.checkState; +import android.util.Log; import androidx.media3.common.C; 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.TrackOutput; @@ -31,15 +33,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; */ /* package */ public final class RtpPcmReader implements RtpPayloadReader { + private static final String TAG = "RtpPcmReader"; private final RtpPayloadFormat payloadFormat; private @MonotonicNonNull TrackOutput trackOutput; private long firstReceivedTimestamp; private long startTimeOffsetUs; + private int previousSequenceNumber; public RtpPcmReader(RtpPayloadFormat payloadFormat) { this.payloadFormat = payloadFormat; firstReceivedTimestamp = C.TIME_UNSET; startTimeOffsetUs = 0; + previousSequenceNumber = C.INDEX_UNSET; } @Override @@ -57,6 +62,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public void consume( ParsableByteArray data, long timestamp, int sequenceNumber, boolean rtpMarker) { + if (previousSequenceNumber != C.INDEX_UNSET) { + 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)); + } + } + long sampleTimeUs = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp, payloadFormat.clockRate); int size = data.bytesLeft(); @@ -68,6 +84,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; size, /* offset= */ 0, /* cryptoData= */ null); + previousSequenceNumber = sequenceNumber; } @Override diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpPcmReaderTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpPcmReaderTest.java new file mode 100644 index 0000000000..3118649acc --- /dev/null +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpPcmReaderTest.java @@ -0,0 +1,182 @@ +/* + * 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.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; + +import androidx.media3.common.C; +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.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** + * Unit test for {@link RtpPcmReader}. + */ +@RunWith(AndroidJUnit4.class) +public final class RtpPcmReaderTest { + + private static final String FRAMEDATA1 = "01020304"; + private static final String FRAMEDATA2 = "05060708"; + + private static final RtpPacket FRAME1 = + createRtpPacket(2599168056L, 40289, getBytesFromHexString(FRAMEDATA1)); + private static final RtpPacket FRAME2 = + createRtpPacket(2599169592L, 40290, getBytesFromHexString(FRAMEDATA2)); + + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + private ParsableByteArray packetData; + private FakeTrackOutput trackOutput; + private RtpPcmReader pcmReader; + @Mock + private ExtractorOutput extractorOutput; + + @Before + public void setUp() { + packetData = new ParsableByteArray(); + trackOutput = new FakeTrackOutput(/* deduplicateConsecutiveFormats= */ true); + when(extractorOutput.track(anyInt(), anyInt())).thenReturn(trackOutput); + } + + @Test + public void consume_AllPackets_8bit() { + pcmReader = new RtpPcmReader( + new RtpPayloadFormat( + new Format.Builder() + .setChannelCount(2) + .setSampleMimeType(MimeTypes.AUDIO_WAV) + .setPcmEncoding(C.ENCODING_PCM_8BIT) + .setSampleRate(48_000) + .build(), + /* rtpPayloadType= */ 97, + /* clockRate= */ 48_000, + /* fmtpParameters= */ ImmutableMap.of())); + pcmReader.createTracks(extractorOutput, /* trackId= */ 0); + pcmReader.onReceivingFirstPacket(FRAME1.timestamp, FRAME1.sequenceNumber); + consume(FRAME1); + consume(FRAME2); + + assertThat(trackOutput.getSampleCount()).isEqualTo(2); + assertThat(trackOutput.getSampleData(0)).isEqualTo(getBytesFromHexString(FRAMEDATA1)); + assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0); + assertThat(trackOutput.getSampleData(1)).isEqualTo(getBytesFromHexString(FRAMEDATA2)); + assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(32000); + } + + @Test + public void consume_AllPackets_16bit() { + pcmReader = new RtpPcmReader( + new RtpPayloadFormat( + new Format.Builder() + .setChannelCount(1) + .setSampleMimeType(MimeTypes.AUDIO_WAV) + .setPcmEncoding(C.ENCODING_PCM_16BIT_BIG_ENDIAN) + .setSampleRate(60_000) + .build(), + /* rtpPayloadType= */ 97, + /* clockRate= */ 60_000, + /* fmtpParameters= */ ImmutableMap.of())); + pcmReader.createTracks(extractorOutput, /* trackId= */ 0); + pcmReader.onReceivingFirstPacket(FRAME1.timestamp, FRAME1.sequenceNumber); + consume(FRAME1); + consume(FRAME2); + + assertThat(trackOutput.getSampleCount()).isEqualTo(2); + assertThat(trackOutput.getSampleData(0)).isEqualTo(getBytesFromHexString(FRAMEDATA1)); + assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0); + assertThat(trackOutput.getSampleData(1)).isEqualTo(getBytesFromHexString(FRAMEDATA2)); + assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(25600); + } + + @Test + public void consume_AllPackets_ALAW() { + pcmReader = new RtpPcmReader( + new RtpPayloadFormat( + new Format.Builder() + .setChannelCount(2) + .setSampleMimeType(MimeTypes.AUDIO_ALAW) + .setSampleRate(16_000) + .build(), + /* rtpPayloadType= */ 97, + /* clockRate= */ 16_000, + /* fmtpParameters= */ ImmutableMap.of())); + pcmReader.createTracks(extractorOutput, /* trackId= */ 0); + pcmReader.onReceivingFirstPacket(FRAME1.timestamp, FRAME1.sequenceNumber); + consume(FRAME1); + consume(FRAME2); + + assertThat(trackOutput.getSampleCount()).isEqualTo(2); + assertThat(trackOutput.getSampleData(0)).isEqualTo(getBytesFromHexString(FRAMEDATA1)); + assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0); + assertThat(trackOutput.getSampleData(1)).isEqualTo(getBytesFromHexString(FRAMEDATA2)); + assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(96000); + } + + @Test + public void consume_AllPackets_MLAW() { + pcmReader = new RtpPcmReader( + new RtpPayloadFormat( + new Format.Builder() + .setChannelCount(2) + .setSampleMimeType(MimeTypes.AUDIO_MLAW) + .setSampleRate(24_000) + .build(), + /* rtpPayloadType= */ 97, + /* clockRate= */ 24_000, + /* fmtpParameters= */ ImmutableMap.of())); + pcmReader.createTracks(extractorOutput, /* trackId= */ 0); + pcmReader.onReceivingFirstPacket(FRAME1.timestamp, FRAME1.sequenceNumber); + consume(FRAME1); + consume(FRAME2); + + assertThat(trackOutput.getSampleCount()).isEqualTo(2); + assertThat(trackOutput.getSampleData(0)).isEqualTo(getBytesFromHexString(FRAMEDATA1)); + assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0); + assertThat(trackOutput.getSampleData(1)).isEqualTo(getBytesFromHexString(FRAMEDATA2)); + assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(64000); + } + + 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 consume(RtpPacket frame) { + packetData.reset(frame.payloadData); + pcmReader.consume(packetData, frame.timestamp, frame.sequenceNumber, frame.marker); + } +}