Skip to content

Commit 9d9bbe3

Browse files
committed
Merge pull request #110 from ittiam-systems:rtp_vp8_test
PiperOrigin-RevId: 460513413
2 parents e56219f + 1de4ee3 commit 9d9bbe3

3 files changed

Lines changed: 251 additions & 22 deletions

File tree

RELEASENOTES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
* RTSP:
3636
* Add RTP reader for H263
3737
([#63](https://github.com/androidx/media/pull/63)).
38+
* Add VP8 fragmented packet handling
39+
([#110](https://github.com/androidx/media/pull/110)).
3840
* Leanback extension:
3941
* Listen to `playWhenReady` changes in `LeanbackAdapter`
4042
([10420](https://github.com/google/ExoPlayer/issues/10420)).

libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8Reader.java

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package androidx.media3.exoplayer.rtsp.reader;
1717

18+
import static androidx.media3.common.util.Assertions.checkNotNull;
19+
import static androidx.media3.common.util.Assertions.checkState;
1820
import static androidx.media3.common.util.Assertions.checkStateNotNull;
1921

2022
import androidx.media3.common.C;
@@ -51,6 +53,8 @@
5153
/** The combined size of a sample that is fragmented into multiple RTP packets. */
5254
private int fragmentedSampleSizeBytes;
5355

56+
private long fragmentedSampleTimeUs;
57+
5458
private long startTimeOffsetUs;
5559
/**
5660
* Whether the first packet of one VP8 frame is received. A VP8 frame can be split into two RTP
@@ -67,6 +71,7 @@ public RtpVp8Reader(RtpPayloadFormat payloadFormat) {
6771
firstReceivedTimestamp = C.TIME_UNSET;
6872
previousSequenceNumber = C.INDEX_UNSET;
6973
fragmentedSampleSizeBytes = C.LENGTH_UNSET;
74+
fragmentedSampleTimeUs = C.TIME_UNSET;
7075
// The start time offset must be 0 until the first seek.
7176
startTimeOffsetUs = 0;
7277
gotFirstPacketOfVp8Frame = false;
@@ -81,7 +86,10 @@ public void createTracks(ExtractorOutput extractorOutput, int trackId) {
8186
}
8287

8388
@Override
84-
public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {}
89+
public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {
90+
checkState(firstReceivedTimestamp == C.TIME_UNSET);
91+
firstReceivedTimestamp = timestamp;
92+
}
8593

8694
@Override
8795
public void consume(
@@ -113,21 +121,16 @@ public void consume(
113121

114122
int fragmentSize = data.bytesLeft();
115123
trackOutput.sampleData(data, fragmentSize);
116-
fragmentedSampleSizeBytes += fragmentSize;
124+
if (fragmentedSampleSizeBytes == C.LENGTH_UNSET) {
125+
fragmentedSampleSizeBytes = fragmentSize;
126+
} else {
127+
fragmentedSampleSizeBytes += fragmentSize;
128+
}
129+
130+
fragmentedSampleTimeUs = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp);
117131

118132
if (rtpMarker) {
119-
if (firstReceivedTimestamp == C.TIME_UNSET) {
120-
firstReceivedTimestamp = timestamp;
121-
}
122-
long timeUs = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp);
123-
trackOutput.sampleMetadata(
124-
timeUs,
125-
isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0,
126-
fragmentedSampleSizeBytes,
127-
/* offset= */ 0,
128-
/* cryptoData= */ null);
129-
fragmentedSampleSizeBytes = C.LENGTH_UNSET;
130-
gotFirstPacketOfVp8Frame = false;
133+
outputSampleMetadataForFragmentedPackets();
131134
}
132135
previousSequenceNumber = sequenceNumber;
133136
}
@@ -147,18 +150,18 @@ public void seek(long nextRtpTimestamp, long timeUs) {
147150
private boolean validateVp8Descriptor(ParsableByteArray payload, int packetSequenceNumber) {
148151
// VP8 Payload Descriptor is defined in RFC7741 Section 4.2.
149152
int header = payload.readUnsignedByte();
150-
if (!gotFirstPacketOfVp8Frame) {
151-
// TODO(b/198620566) Consider using ParsableBitArray.
152-
// For start of VP8 partition S=1 and PID=0 as per RFC7741 Section 4.2.
153-
if ((header & 0x10) != 0x1 || (header & 0x07) != 0) {
154-
Log.w(TAG, "RTP packet is not the start of a new VP8 partition, skipping.");
155-
return false;
153+
// TODO(b/198620566) Consider using ParsableBitArray.
154+
// For start of VP8 partition S=1 and PID=0 as per RFC7741 Section 4.2.
155+
if ((header & 0x10) == 0x10 && (header & 0x07) == 0) {
156+
if (gotFirstPacketOfVp8Frame && fragmentedSampleSizeBytes > 0) {
157+
// Received new VP8 fragment, output data of previous fragment to decoder.
158+
outputSampleMetadataForFragmentedPackets();
156159
}
157160
gotFirstPacketOfVp8Frame = true;
158-
} else {
161+
} else if (gotFirstPacketOfVp8Frame) {
159162
// Check that this packet is in the sequence of the previous packet.
160163
int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber);
161-
if (packetSequenceNumber != expectedSequenceNumber) {
164+
if (packetSequenceNumber < expectedSequenceNumber) {
162165
Log.w(
163166
TAG,
164167
Util.formatInvariant(
@@ -167,6 +170,9 @@ private boolean validateVp8Descriptor(ParsableByteArray payload, int packetSeque
167170
expectedSequenceNumber, packetSequenceNumber));
168171
return false;
169172
}
173+
} else {
174+
Log.w(TAG, "RTP packet is not the start of a new VP8 partition, skipping.");
175+
return false;
170176
}
171177

172178
// Check if optional X header is present.
@@ -195,6 +201,24 @@ private boolean validateVp8Descriptor(ParsableByteArray payload, int packetSeque
195201
return true;
196202
}
197203

204+
/**
205+
* Outputs sample metadata of the received fragmented packets.
206+
*
207+
* <p>Call this method only after receiving an end of a VP8 partition.
208+
*/
209+
private void outputSampleMetadataForFragmentedPackets() {
210+
checkNotNull(trackOutput)
211+
.sampleMetadata(
212+
fragmentedSampleTimeUs,
213+
isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0,
214+
fragmentedSampleSizeBytes,
215+
/* offset= */ 0,
216+
/* cryptoData= */ null);
217+
fragmentedSampleSizeBytes = 0;
218+
fragmentedSampleTimeUs = C.TIME_UNSET;
219+
gotFirstPacketOfVp8Frame = false;
220+
}
221+
198222
private static long toSampleUs(
199223
long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp) {
200224
return startTimeOffsetUs
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/*
2+
* Copyright 2022 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package androidx.media3.exoplayer.rtsp.reader;
17+
18+
import static androidx.media3.common.util.Util.getBytesFromHexString;
19+
import static com.google.common.truth.Truth.assertThat;
20+
21+
import androidx.media3.common.C;
22+
import androidx.media3.common.Format;
23+
import androidx.media3.common.MimeTypes;
24+
import androidx.media3.common.util.ParsableByteArray;
25+
import androidx.media3.common.util.Util;
26+
import androidx.media3.exoplayer.rtsp.RtpPacket;
27+
import androidx.media3.exoplayer.rtsp.RtpPayloadFormat;
28+
import androidx.media3.test.utils.FakeExtractorOutput;
29+
import androidx.media3.test.utils.FakeTrackOutput;
30+
import androidx.test.ext.junit.runners.AndroidJUnit4;
31+
import com.google.common.collect.ImmutableMap;
32+
import com.google.common.primitives.Bytes;
33+
import java.util.Arrays;
34+
import org.junit.Before;
35+
import org.junit.Test;
36+
import org.junit.runner.RunWith;
37+
38+
/** Unit test for {@link RtpVp8Reader}. */
39+
@RunWith(AndroidJUnit4.class)
40+
public final class RtpVp8ReaderTest {
41+
42+
/** VP9 uses a 90 KHz media clock (RFC7741 Section 4.1). */
43+
private static final long MEDIA_CLOCK_FREQUENCY = 90_000;
44+
45+
private static final byte[] PARTITION_1 = getBytesFromHexString("000102030405060708090A0B0C0D0E");
46+
// 000102030405060708090A
47+
private static final byte[] PARTITION_1_FRAGMENT_1 =
48+
Arrays.copyOf(PARTITION_1, /* newLength= */ 11);
49+
// 0B0C0D0E
50+
private static final byte[] PARTITION_1_FRAGMENT_2 =
51+
Arrays.copyOfRange(PARTITION_1, /* from= */ 11, /* to= */ 15);
52+
private static final long PARTITION_1_RTP_TIMESTAMP = 2599168056L;
53+
private static final RtpPacket PACKET_PARTITION_1_FRAGMENT_1 =
54+
new RtpPacket.Builder()
55+
.setTimestamp(PARTITION_1_RTP_TIMESTAMP)
56+
.setSequenceNumber(40289)
57+
.setMarker(false)
58+
.setPayloadData(Bytes.concat(getBytesFromHexString("10"), PARTITION_1_FRAGMENT_1))
59+
.build();
60+
private static final RtpPacket PACKET_PARTITION_1_FRAGMENT_2 =
61+
new RtpPacket.Builder()
62+
.setTimestamp(PARTITION_1_RTP_TIMESTAMP)
63+
.setSequenceNumber(40290)
64+
.setMarker(false)
65+
.setPayloadData(Bytes.concat(getBytesFromHexString("00"), PARTITION_1_FRAGMENT_2))
66+
.build();
67+
68+
private static final byte[] PARTITION_2 = getBytesFromHexString("0D0C0B0A09080706050403020100");
69+
// 0D0C0B0A090807060504
70+
private static final byte[] PARTITION_2_FRAGMENT_1 =
71+
Arrays.copyOf(PARTITION_2, /* newLength= */ 10);
72+
// 03020100
73+
private static final byte[] PARTITION_2_FRAGMENT_2 =
74+
Arrays.copyOfRange(PARTITION_2, /* from= */ 10, /* to= */ 14);
75+
private static final long PARTITION_2_RTP_TIMESTAMP = 2599168344L;
76+
private static final RtpPacket PACKET_PARTITION_2_FRAGMENT_1 =
77+
new RtpPacket.Builder()
78+
.setTimestamp(PARTITION_2_RTP_TIMESTAMP)
79+
.setSequenceNumber(40291)
80+
.setMarker(false)
81+
.setPayloadData(Bytes.concat(getBytesFromHexString("10"), PARTITION_2_FRAGMENT_1))
82+
.build();
83+
private static final RtpPacket PACKET_PARTITION_2_FRAGMENT_2 =
84+
new RtpPacket.Builder()
85+
.setTimestamp(PARTITION_2_RTP_TIMESTAMP)
86+
.setSequenceNumber(40292)
87+
.setMarker(true)
88+
.setPayloadData(
89+
Bytes.concat(
90+
getBytesFromHexString("80"),
91+
// Optional header.
92+
getBytesFromHexString("D6AA953961"),
93+
PARTITION_2_FRAGMENT_2))
94+
.build();
95+
private static final long PARTITION_2_PRESENTATION_TIMESTAMP_US =
96+
Util.scaleLargeTimestamp(
97+
(PARTITION_2_RTP_TIMESTAMP - PARTITION_1_RTP_TIMESTAMP),
98+
/* multiplier= */ C.MICROS_PER_SECOND,
99+
/* divisor= */ MEDIA_CLOCK_FREQUENCY);
100+
101+
private FakeExtractorOutput extractorOutput;
102+
103+
@Before
104+
public void setUp() {
105+
extractorOutput =
106+
new FakeExtractorOutput(
107+
(id, type) -> new FakeTrackOutput(/* deduplicateConsecutiveFormats= */ true));
108+
}
109+
110+
@Test
111+
public void consume_validPackets() {
112+
RtpVp8Reader vp8Reader = createVp8Reader();
113+
114+
vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
115+
vp8Reader.onReceivingFirstPacket(
116+
PACKET_PARTITION_1_FRAGMENT_1.timestamp, PACKET_PARTITION_1_FRAGMENT_1.sequenceNumber);
117+
consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_1);
118+
consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_2);
119+
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_1);
120+
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_2);
121+
122+
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
123+
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
124+
assertThat(trackOutput.getSampleData(0)).isEqualTo(PARTITION_1);
125+
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
126+
assertThat(trackOutput.getSampleData(1)).isEqualTo(PARTITION_2);
127+
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
128+
}
129+
130+
@Test
131+
public void consume_fragmentedFrameMissingFirstFragment() {
132+
RtpVp8Reader vp8Reader = createVp8Reader();
133+
134+
vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
135+
// First packet timing information is transmitted over RTSP, not RTP.
136+
vp8Reader.onReceivingFirstPacket(
137+
PACKET_PARTITION_1_FRAGMENT_1.timestamp, PACKET_PARTITION_1_FRAGMENT_1.sequenceNumber);
138+
consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_2);
139+
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_1);
140+
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_2);
141+
142+
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
143+
assertThat(trackOutput.getSampleCount()).isEqualTo(1);
144+
assertThat(trackOutput.getSampleData(0)).isEqualTo(PARTITION_2);
145+
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
146+
}
147+
148+
@Test
149+
public void consume_fragmentedFrameMissingBoundaryFragment() {
150+
RtpVp8Reader vp8Reader = createVp8Reader();
151+
152+
vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
153+
vp8Reader.onReceivingFirstPacket(
154+
PACKET_PARTITION_1_FRAGMENT_1.timestamp, PACKET_PARTITION_1_FRAGMENT_1.sequenceNumber);
155+
consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_1);
156+
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_1);
157+
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_2);
158+
159+
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
160+
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
161+
assertThat(trackOutput.getSampleData(0)).isEqualTo(PARTITION_1_FRAGMENT_1);
162+
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
163+
assertThat(trackOutput.getSampleData(1)).isEqualTo(PARTITION_2);
164+
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
165+
}
166+
167+
@Test
168+
public void consume_outOfOrderFragmentedFrame() {
169+
RtpVp8Reader vp8Reader = createVp8Reader();
170+
171+
vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
172+
vp8Reader.onReceivingFirstPacket(
173+
PACKET_PARTITION_1_FRAGMENT_1.timestamp, PACKET_PARTITION_1_FRAGMENT_1.sequenceNumber);
174+
consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_1);
175+
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_1);
176+
consume(vp8Reader, PACKET_PARTITION_1_FRAGMENT_2);
177+
consume(vp8Reader, PACKET_PARTITION_2_FRAGMENT_2);
178+
179+
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
180+
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
181+
assertThat(trackOutput.getSampleData(0)).isEqualTo(PARTITION_1_FRAGMENT_1);
182+
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
183+
assertThat(trackOutput.getSampleData(1)).isEqualTo(PARTITION_2);
184+
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
185+
}
186+
187+
private static RtpVp8Reader createVp8Reader() {
188+
return new RtpVp8Reader(
189+
new RtpPayloadFormat(
190+
new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_VP8).build(),
191+
/* rtpPayloadType= */ 96,
192+
/* clockRate= */ (int) MEDIA_CLOCK_FREQUENCY,
193+
/* fmtpParameters= */ ImmutableMap.of()));
194+
}
195+
196+
private static void consume(RtpVp8Reader vp8Reader, RtpPacket rtpPacket) {
197+
vp8Reader.consume(
198+
new ParsableByteArray(rtpPacket.payloadData),
199+
rtpPacket.timestamp,
200+
rtpPacket.sequenceNumber,
201+
rtpPacket.marker);
202+
}
203+
}

0 commit comments

Comments
 (0)