Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RTSP VP8 Reader test #101

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -15,6 +15,7 @@
*/
package androidx.media3.exoplayer.rtsp.reader;

import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull;

import androidx.media3.common.C;
Expand Down Expand Up @@ -50,6 +51,7 @@
private int previousSequenceNumber;
/** The combined size of a sample that is fragmented into multiple RTP packets. */
private int fragmentedSampleSizeBytes;
private long sampleTimeUsOfFragmentedSample;

private long startTimeOffsetUs;
/**
Expand All @@ -66,7 +68,8 @@ public RtpVp8Reader(RtpPayloadFormat payloadFormat) {
this.payloadFormat = payloadFormat;
firstReceivedTimestamp = C.TIME_UNSET;
previousSequenceNumber = C.INDEX_UNSET;
fragmentedSampleSizeBytes = C.LENGTH_UNSET;
sampleTimeUsOfFragmentedSample = C.TIME_UNSET;
fragmentedSampleSizeBytes = 0;
// The start time offset must be 0 until the first seek.
startTimeOffsetUs = 0;
gotFirstPacketOfVp8Frame = false;
Expand All @@ -81,7 +84,10 @@ public void createTracks(ExtractorOutput extractorOutput, int trackId) {
}

@Override
public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {}
public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {
checkState(firstReceivedTimestamp == C.TIME_UNSET);
firstReceivedTimestamp = timestamp;
}

@Override
public void consume(
Expand All @@ -91,7 +97,7 @@ public void consume(
boolean isValidVP8Descriptor = validateVp8Descriptor(data, sequenceNumber);
if (isValidVP8Descriptor) {
// VP8 Payload Header is defined in RFC7741 Section 4.3.
if (fragmentedSampleSizeBytes == C.LENGTH_UNSET && gotFirstPacketOfVp8Frame) {
if (fragmentedSampleSizeBytes == 0 && gotFirstPacketOfVp8Frame) {
isKeyFrame = (data.peekUnsignedByte() & 0x01) == 0;
}
if (!isOutputFormatSet) {
Expand All @@ -114,20 +120,11 @@ public void consume(
int fragmentSize = data.bytesLeft();
trackOutput.sampleData(data, fragmentSize);
fragmentedSampleSizeBytes += fragmentSize;
sampleTimeUsOfFragmentedSample =
toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp);

if (rtpMarker) {
if (firstReceivedTimestamp == C.TIME_UNSET) {
firstReceivedTimestamp = timestamp;
}
long timeUs = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp);
trackOutput.sampleMetadata(
timeUs,
isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0,
fragmentedSampleSizeBytes,
/* offset= */ 0,
/* cryptoData= */ null);
fragmentedSampleSizeBytes = C.LENGTH_UNSET;
gotFirstPacketOfVp8Frame = false;
outputSampleMetadataForFragmentedPackets();
}
previousSequenceNumber = sequenceNumber;
}
Expand All @@ -136,7 +133,7 @@ public void consume(
@Override
public void seek(long nextRtpTimestamp, long timeUs) {
firstReceivedTimestamp = nextRtpTimestamp;
fragmentedSampleSizeBytes = C.LENGTH_UNSET;
fragmentedSampleSizeBytes = 0;
startTimeOffsetUs = timeUs;
}

Expand All @@ -147,18 +144,17 @@ public void seek(long nextRtpTimestamp, long timeUs) {
private boolean validateVp8Descriptor(ParsableByteArray payload, int packetSequenceNumber) {
// VP8 Payload Descriptor is defined in RFC7741 Section 4.2.
int header = payload.readUnsignedByte();
if (!gotFirstPacketOfVp8Frame) {
// TODO(b/198620566) Consider using ParsableBitArray.
// For start of VP8 partition S=1 and PID=0 as per RFC7741 Section 4.2.
if ((header & 0x10) != 0x1 || (header & 0x07) != 0) {
Log.w(TAG, "RTP packet is not the start of a new VP8 partition, skipping.");
return false;
// TODO(b/198620566) Consider using ParsableBitArray.
// For start of VP8 partition S=1 and PID=0 as per RFC7741 Section 4.2.
if ((header & 0x10) == 0x10 && (header & 0x07) == 0) {
if (gotFirstPacketOfVp8Frame && fragmentedSampleSizeBytes > 0) {
outputSampleMetadataForFragmentedPackets();
}
gotFirstPacketOfVp8Frame = true;
} else {
} else if (gotFirstPacketOfVp8Frame) {
// Check that this packet is in the sequence of the previous packet.
int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber);
if (packetSequenceNumber != expectedSequenceNumber) {
if (packetSequenceNumber < expectedSequenceNumber) {
Log.w(
TAG,
Util.formatInvariant(
Expand All @@ -167,6 +163,9 @@ private boolean validateVp8Descriptor(ParsableByteArray payload, int packetSeque
expectedSequenceNumber, packetSequenceNumber));
return false;
}
} else {
Log.w(TAG, "RTP packet is not the start of a new VP8 partition, skipping.");
return false;
}

// Check if optional X header is present.
Expand Down Expand Up @@ -195,6 +194,23 @@ private boolean validateVp8Descriptor(ParsableByteArray payload, int packetSeque
return true;
}

/**
* Outputs sample metadata.
*
* <p>Call this method only when receiving a end of VP8 partition
*/
private void outputSampleMetadataForFragmentedPackets() {
trackOutput.sampleMetadata(
sampleTimeUsOfFragmentedSample,
isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0,
fragmentedSampleSizeBytes,
/* offset= */ 0,
/* cryptoData= */ null);
fragmentedSampleSizeBytes = 0;
sampleTimeUsOfFragmentedSample = C.TIME_UNSET;
gotFirstPacketOfVp8Frame = false;
}

private static long toSampleUs(
long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp) {
return startTimeOffsetUs
Expand Down
@@ -0,0 +1,180 @@
/*
* 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.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 RtpVp8Reader}.
*/
@RunWith(AndroidJUnit4.class)
public final class RtpVp8ReaderTest {

private final RtpPacket frame1fragment1 =
createRtpPacket(
/* timestamp= */ 2599168056L,
/* sequenceNumber= */ 40289,
/* marker= */ false,
/* payloadData= */ getBytesFromHexString("10000102030405060708090A"));
private final RtpPacket frame1fragment2 =
createRtpPacket(
/* timestamp= */ 2599168056L,
/* sequenceNumber= */ 40290,
/* marker= */ true,
/* payloadData= */ getBytesFromHexString("000B0C0D0E"));
private final byte[] frame1Data = getBytesFromHexString("000102030405060708090A0B0C0D0E");
private final RtpPacket frame2fragment1 =
createRtpPacket(
/* timestamp= */ 2599168344L,
/* sequenceNumber= */ 40291,
/* marker= */ false,
/* payloadData= */ getBytesFromHexString("100D0C0B0A090807060504"));
// Add optional headers
private final RtpPacket frame2fragment2 =
createRtpPacket(
/* timestamp= */ 2599168344L,
/* sequenceNumber= */ 40292,
/* marker= */ true,
/* payloadData= */ getBytesFromHexString("80D6AA95396103020100"));
private final byte[] frame2Data = getBytesFromHexString("0D0C0B0A09080706050403020100");

private static final RtpPayloadFormat VP8_FORMAT =
new RtpPayloadFormat(
new Format.Builder()
.setSampleMimeType(MimeTypes.VIDEO_VP8)
.setSampleRate(500000)
.build(),
/* rtpPayloadType= */ 97,
/* clockRate= */ 48_000,
/* fmtpParameters= */ ImmutableMap.of());

@Rule
public final MockitoRule mockito = MockitoJUnit.rule();

private ParsableByteArray packetData;

private RtpVp8Reader vp8Reader;
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);
vp8Reader = new RtpVp8Reader(VP8_FORMAT);
vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
}

@Test
public void consume_validPackets() {
vp8Reader.onReceivingFirstPacket(frame1fragment1.timestamp, frame1fragment1.sequenceNumber);
consume(frame1fragment1);
consume(frame1fragment2);
consume(frame2fragment1);
consume(frame2fragment2);

assertThat(trackOutput.getSampleCount()).isEqualTo(2);
assertThat(trackOutput.getSampleData(0)).isEqualTo(frame1Data);
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
assertThat(trackOutput.getSampleData(1)).isEqualTo(frame2Data);
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(3200);
}

@Test
public void consume_fragmentedFrameMissingFirstFragment() {
// First packet timing information is transmitted over RTSP, not RTP.
vp8Reader.onReceivingFirstPacket(frame1fragment1.timestamp, frame1fragment1.sequenceNumber);
consume(frame1fragment2);
consume(frame2fragment1);
consume(frame2fragment2);

assertThat(trackOutput.getSampleCount()).isEqualTo(1);
assertThat(trackOutput.getSampleData(0)).isEqualTo(frame2Data);
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(3200);
}

@Test
public void consume_fragmentedFrameMissingBoundaryFragment() {
vp8Reader.onReceivingFirstPacket(frame1fragment1.timestamp, frame1fragment1.sequenceNumber);
consume(frame1fragment1);
consume(frame2fragment1);
consume(frame2fragment2);

assertThat(trackOutput.getSampleCount()).isEqualTo(2);
assertThat(trackOutput.getSampleData(0))
.isEqualTo(getBytesFromHexString("000102030405060708090A"));
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
assertThat(trackOutput.getSampleData(1)).isEqualTo(frame2Data);
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(3200);
}

@Test
public void consume_outOfOrderFragmentedFrame() {
vp8Reader.onReceivingFirstPacket(frame1fragment1.timestamp, frame1fragment1.sequenceNumber);
consume(frame1fragment1);
consume(frame2fragment1);
consume(frame1fragment2);
consume(frame2fragment2);

assertThat(trackOutput.getSampleCount()).isEqualTo(2);
assertThat(trackOutput.getSampleData(0))
.isEqualTo(getBytesFromHexString("000102030405060708090A"));
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
assertThat(trackOutput.getSampleData(1)).isEqualTo(frame2Data);
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(3200);
}

private static RtpPacket createRtpPacket(
long timestamp, int sequenceNumber, boolean marker, byte[] payloadData) {
return new RtpPacket.Builder()
.setTimestamp((int) timestamp)
.setSequenceNumber(sequenceNumber)
.setMarker(marker)
.setPayloadData(payloadData)
.build();
}

private void consume(RtpPacket rtpPacket) {
packetData.reset(rtpPacket.payloadData);
vp8Reader.consume(
packetData,
rtpPacket.timestamp,
rtpPacket.sequenceNumber,
/* isFrameBoundary= */ rtpPacket.marker);
}
}