Merge pull request #110 from ittiam-systems:rtp_vp8_test

PiperOrigin-RevId: 460513413
This commit is contained in:
Rohit Singh 2022-07-13 17:40:18 +00:00
commit 9d9bbe3d33
3 changed files with 251 additions and 22 deletions

View File

@ -35,6 +35,8 @@
* RTSP: * RTSP:
* Add RTP reader for H263 * Add RTP reader for H263
([#63](https://github.com/androidx/media/pull/63)). ([#63](https://github.com/androidx/media/pull/63)).
* Add VP8 fragmented packet handling
([#110](https://github.com/androidx/media/pull/110)).
* Leanback extension: * Leanback extension:
* Listen to `playWhenReady` changes in `LeanbackAdapter` * Listen to `playWhenReady` changes in `LeanbackAdapter`
([10420](https://github.com/google/ExoPlayer/issues/10420)). ([10420](https://github.com/google/ExoPlayer/issues/10420)).

View File

@ -15,6 +15,8 @@
*/ */
package androidx.media3.exoplayer.rtsp.reader; package androidx.media3.exoplayer.rtsp.reader;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull;
import androidx.media3.common.C; import androidx.media3.common.C;
@ -51,6 +53,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** The combined size of a sample that is fragmented into multiple RTP packets. */ /** The combined size of a sample that is fragmented into multiple RTP packets. */
private int fragmentedSampleSizeBytes; private int fragmentedSampleSizeBytes;
private long fragmentedSampleTimeUs;
private long startTimeOffsetUs; private long startTimeOffsetUs;
/** /**
* Whether the first packet of one VP8 frame is received. A VP8 frame can be split into two RTP * Whether the first packet of one VP8 frame is received. A VP8 frame can be split into two RTP
@ -67,6 +71,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
firstReceivedTimestamp = C.TIME_UNSET; firstReceivedTimestamp = C.TIME_UNSET;
previousSequenceNumber = C.INDEX_UNSET; previousSequenceNumber = C.INDEX_UNSET;
fragmentedSampleSizeBytes = C.LENGTH_UNSET; fragmentedSampleSizeBytes = C.LENGTH_UNSET;
fragmentedSampleTimeUs = C.TIME_UNSET;
// The start time offset must be 0 until the first seek. // The start time offset must be 0 until the first seek.
startTimeOffsetUs = 0; startTimeOffsetUs = 0;
gotFirstPacketOfVp8Frame = false; gotFirstPacketOfVp8Frame = false;
@ -81,7 +86,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
@Override @Override
public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {} public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {
checkState(firstReceivedTimestamp == C.TIME_UNSET);
firstReceivedTimestamp = timestamp;
}
@Override @Override
public void consume( public void consume(
@ -113,21 +121,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
int fragmentSize = data.bytesLeft(); int fragmentSize = data.bytesLeft();
trackOutput.sampleData(data, fragmentSize); trackOutput.sampleData(data, fragmentSize);
if (fragmentedSampleSizeBytes == C.LENGTH_UNSET) {
fragmentedSampleSizeBytes = fragmentSize;
} else {
fragmentedSampleSizeBytes += fragmentSize; fragmentedSampleSizeBytes += fragmentSize;
}
fragmentedSampleTimeUs = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp);
if (rtpMarker) { if (rtpMarker) {
if (firstReceivedTimestamp == C.TIME_UNSET) { outputSampleMetadataForFragmentedPackets();
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;
} }
previousSequenceNumber = sequenceNumber; previousSequenceNumber = sequenceNumber;
} }
@ -147,18 +150,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private boolean validateVp8Descriptor(ParsableByteArray payload, int packetSequenceNumber) { private boolean validateVp8Descriptor(ParsableByteArray payload, int packetSequenceNumber) {
// VP8 Payload Descriptor is defined in RFC7741 Section 4.2. // VP8 Payload Descriptor is defined in RFC7741 Section 4.2.
int header = payload.readUnsignedByte(); int header = payload.readUnsignedByte();
if (!gotFirstPacketOfVp8Frame) {
// TODO(b/198620566) Consider using ParsableBitArray. // TODO(b/198620566) Consider using ParsableBitArray.
// For start of VP8 partition S=1 and PID=0 as per RFC7741 Section 4.2. // For start of VP8 partition S=1 and PID=0 as per RFC7741 Section 4.2.
if ((header & 0x10) != 0x1 || (header & 0x07) != 0) { if ((header & 0x10) == 0x10 && (header & 0x07) == 0) {
Log.w(TAG, "RTP packet is not the start of a new VP8 partition, skipping."); if (gotFirstPacketOfVp8Frame && fragmentedSampleSizeBytes > 0) {
return false; // Received new VP8 fragment, output data of previous fragment to decoder.
outputSampleMetadataForFragmentedPackets();
} }
gotFirstPacketOfVp8Frame = true; gotFirstPacketOfVp8Frame = true;
} else { } else if (gotFirstPacketOfVp8Frame) {
// Check that this packet is in the sequence of the previous packet. // Check that this packet is in the sequence of the previous packet.
int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber); int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber);
if (packetSequenceNumber != expectedSequenceNumber) { if (packetSequenceNumber < expectedSequenceNumber) {
Log.w( Log.w(
TAG, TAG,
Util.formatInvariant( Util.formatInvariant(
@ -167,6 +170,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
expectedSequenceNumber, packetSequenceNumber)); expectedSequenceNumber, packetSequenceNumber));
return false; 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. // Check if optional X header is present.
@ -195,6 +201,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return true; return true;
} }
/**
* Outputs sample metadata of the received fragmented packets.
*
* <p>Call this method only after receiving an end of a VP8 partition.
*/
private void outputSampleMetadataForFragmentedPackets() {
checkNotNull(trackOutput)
.sampleMetadata(
fragmentedSampleTimeUs,
isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0,
fragmentedSampleSizeBytes,
/* offset= */ 0,
/* cryptoData= */ null);
fragmentedSampleSizeBytes = 0;
fragmentedSampleTimeUs = C.TIME_UNSET;
gotFirstPacketOfVp8Frame = false;
}
private static long toSampleUs( private static long toSampleUs(
long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp) { long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp) {
return startTimeOffsetUs return startTimeOffsetUs

View File

@ -0,0 +1,203 @@
/*
* 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 androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
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.test.utils.FakeExtractorOutput;
import androidx.media3.test.utils.FakeTrackOutput;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableMap;
import com.google.common.primitives.Bytes;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link RtpVp8Reader}. */
@RunWith(AndroidJUnit4.class)
public final class RtpVp8ReaderTest {
/** VP9 uses a 90 KHz media clock (RFC7741 Section 4.1). */
private static final long MEDIA_CLOCK_FREQUENCY = 90_000;
private static final byte[] PARTITION_1 = getBytesFromHexString("000102030405060708090A0B0C0D0E");
// 000102030405060708090A
private static final byte[] PARTITION_1_FRAGMENT_1 =
Arrays.copyOf(PARTITION_1, /* newLength= */ 11);
// 0B0C0D0E
private static final byte[] PARTITION_1_FRAGMENT_2 =
Arrays.copyOfRange(PARTITION_1, /* from= */ 11, /* to= */ 15);
private static final long PARTITION_1_RTP_TIMESTAMP = 2599168056L;
private static final RtpPacket PACKET_PARTITION_1_FRAGMENT_1 =
new RtpPacket.Builder()
.setTimestamp(PARTITION_1_RTP_TIMESTAMP)
.setSequenceNumber(40289)
.setMarker(false)
.setPayloadData(Bytes.concat(getBytesFromHexString("10"), PARTITION_1_FRAGMENT_1))
.build();
private static final RtpPacket PACKET_PARTITION_1_FRAGMENT_2 =
new RtpPacket.Builder()
.setTimestamp(PARTITION_1_RTP_TIMESTAMP)
.setSequenceNumber(40290)
.setMarker(false)
.setPayloadData(Bytes.concat(getBytesFromHexString("00"), PARTITION_1_FRAGMENT_2))
.build();
private static final byte[] PARTITION_2 = getBytesFromHexString("0D0C0B0A09080706050403020100");
// 0D0C0B0A090807060504
private static final byte[] PARTITION_2_FRAGMENT_1 =
Arrays.copyOf(PARTITION_2, /* newLength= */ 10);
// 03020100
private static final byte[] PARTITION_2_FRAGMENT_2 =
Arrays.copyOfRange(PARTITION_2, /* from= */ 10, /* to= */ 14);
private static final long PARTITION_2_RTP_TIMESTAMP = 2599168344L;
private static final RtpPacket PACKET_PARTITION_2_FRAGMENT_1 =
new RtpPacket.Builder()
.setTimestamp(PARTITION_2_RTP_TIMESTAMP)
.setSequenceNumber(40291)
.setMarker(false)
.setPayloadData(Bytes.concat(getBytesFromHexString("10"), PARTITION_2_FRAGMENT_1))
.build();
private static final RtpPacket PACKET_PARTITION_2_FRAGMENT_2 =
new RtpPacket.Builder()
.setTimestamp(PARTITION_2_RTP_TIMESTAMP)
.setSequenceNumber(40292)
.setMarker(true)
.setPayloadData(
Bytes.concat(
getBytesFromHexString("80"),
// Optional header.
getBytesFromHexString("D6AA953961"),
PARTITION_2_FRAGMENT_2))
.build();
private static final long PARTITION_2_PRESENTATION_TIMESTAMP_US =
Util.scaleLargeTimestamp(
(PARTITION_2_RTP_TIMESTAMP - PARTITION_1_RTP_TIMESTAMP),
/* multiplier= */ C.MICROS_PER_SECOND,
/* divisor= */ MEDIA_CLOCK_FREQUENCY);
private FakeExtractorOutput extractorOutput;
@Before
public void setUp() {
extractorOutput =
new FakeExtractorOutput(
(id, type) -> new FakeTrackOutput(/* deduplicateConsecutiveFormats= */ true));
}
@Test
public void consume_validPackets() {
RtpVp8Reader vp8Reader = createVp8Reader();
vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
vp8Reader.onReceivingFirstPacket(
PACKET_PARTITION_1_FRAGMENT_1.timestamp, PACKET_PARTITION_1_FRAGMENT_1.sequenceNumber);
consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_1);
consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_2);
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_1);
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_2);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
assertThat(trackOutput.getSampleData(0)).isEqualTo(PARTITION_1);
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
assertThat(trackOutput.getSampleData(1)).isEqualTo(PARTITION_2);
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
}
@Test
public void consume_fragmentedFrameMissingFirstFragment() {
RtpVp8Reader vp8Reader = createVp8Reader();
vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
// First packet timing information is transmitted over RTSP, not RTP.
vp8Reader.onReceivingFirstPacket(
PACKET_PARTITION_1_FRAGMENT_1.timestamp, PACKET_PARTITION_1_FRAGMENT_1.sequenceNumber);
consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_2);
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_1);
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_2);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
assertThat(trackOutput.getSampleCount()).isEqualTo(1);
assertThat(trackOutput.getSampleData(0)).isEqualTo(PARTITION_2);
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
}
@Test
public void consume_fragmentedFrameMissingBoundaryFragment() {
RtpVp8Reader vp8Reader = createVp8Reader();
vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
vp8Reader.onReceivingFirstPacket(
PACKET_PARTITION_1_FRAGMENT_1.timestamp, PACKET_PARTITION_1_FRAGMENT_1.sequenceNumber);
consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_1);
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_1);
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_2);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
assertThat(trackOutput.getSampleData(0)).isEqualTo(PARTITION_1_FRAGMENT_1);
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
assertThat(trackOutput.getSampleData(1)).isEqualTo(PARTITION_2);
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
}
@Test
public void consume_outOfOrderFragmentedFrame() {
RtpVp8Reader vp8Reader = createVp8Reader();
vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
vp8Reader.onReceivingFirstPacket(
PACKET_PARTITION_1_FRAGMENT_1.timestamp, PACKET_PARTITION_1_FRAGMENT_1.sequenceNumber);
consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_1);
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_1);
consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_2);
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_2);
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
assertThat(trackOutput.getSampleData(0)).isEqualTo(PARTITION_1_FRAGMENT_1);
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
assertThat(trackOutput.getSampleData(1)).isEqualTo(PARTITION_2);
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
}
private static RtpVp8Reader createVp8Reader() {
return new RtpVp8Reader(
new RtpPayloadFormat(
new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_VP8).build(),
/* rtpPayloadType= */ 96,
/* clockRate= */ (int) MEDIA_CLOCK_FREQUENCY,
/* fmtpParameters= */ ImmutableMap.of()));
}
private static void consume(RtpVp8Reader vp8Reader, RtpPacket rtpPacket) {
vp8Reader.consume(
new ParsableByteArray(rtpPacket.payloadData),
rtpPacket.timestamp,
rtpPacket.sequenceNumber,
rtpPacket.marker);
}
}