Skip to content

Commit 1de4ee3

Browse files
Add RTP VP8 Reader Test
Update VP8 Reader to handle missing frames/fragments. Change-Id: I9eede8f1e3a20fb0ff2e7add0dfc60f0780ec769
1 parent ff3d7df commit 1de4ee3

File tree

2 files changed

+219
-21
lines changed

2 files changed

+219
-21
lines changed

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

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

18+
import static androidx.media3.common.util.Assertions.checkState;
1819
import static androidx.media3.common.util.Assertions.checkStateNotNull;
1920

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

5456
private long startTimeOffsetUs;
5557
/**
@@ -67,6 +69,7 @@ public RtpVp8Reader(RtpPayloadFormat payloadFormat) {
6769
firstReceivedTimestamp = C.TIME_UNSET;
6870
previousSequenceNumber = C.INDEX_UNSET;
6971
fragmentedSampleSizeBytes = C.LENGTH_UNSET;
72+
sampleTimeUsOfFragmentedSample = C.TIME_UNSET;
7073
// The start time offset must be 0 until the first seek.
7174
startTimeOffsetUs = 0;
7275
gotFirstPacketOfVp8Frame = false;
@@ -81,7 +84,10 @@ public void createTracks(ExtractorOutput extractorOutput, int trackId) {
8184
}
8285

8386
@Override
84-
public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {}
87+
public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {
88+
checkState(firstReceivedTimestamp == C.TIME_UNSET);
89+
firstReceivedTimestamp = timestamp;
90+
}
8591

8692
@Override
8793
public void consume(
@@ -119,19 +125,11 @@ public void consume(
119125
fragmentedSampleSizeBytes += fragmentSize;
120126
}
121127

128+
sampleTimeUsOfFragmentedSample =
129+
toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp);
130+
122131
if (rtpMarker) {
123-
if (firstReceivedTimestamp == C.TIME_UNSET) {
124-
firstReceivedTimestamp = timestamp;
125-
}
126-
long timeUs = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp);
127-
trackOutput.sampleMetadata(
128-
timeUs,
129-
isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0,
130-
fragmentedSampleSizeBytes,
131-
/* offset= */ 0,
132-
/* cryptoData= */ null);
133-
fragmentedSampleSizeBytes = C.LENGTH_UNSET;
134-
gotFirstPacketOfVp8Frame = false;
132+
outputSampleMetadataForFragmentedPackets();
135133
}
136134
previousSequenceNumber = sequenceNumber;
137135
}
@@ -151,18 +149,18 @@ public void seek(long nextRtpTimestamp, long timeUs) {
151149
private boolean validateVp8Descriptor(ParsableByteArray payload, int packetSequenceNumber) {
152150
// VP8 Payload Descriptor is defined in RFC7741 Section 4.2.
153151
int header = payload.readUnsignedByte();
154-
if (!gotFirstPacketOfVp8Frame) {
155-
// TODO(b/198620566) Consider using ParsableBitArray.
156-
// For start of VP8 partition S=1 and PID=0 as per RFC7741 Section 4.2.
157-
if ((header & 0x10) != 0x10 || (header & 0x07) != 0) {
158-
Log.w(TAG, "RTP packet is not the start of a new VP8 partition, skipping.");
159-
return false;
152+
// TODO(b/198620566) Consider using ParsableBitArray.
153+
// For start of VP8 partition S=1 and PID=0 as per RFC7741 Section 4.2.
154+
if ((header & 0x10) == 0x10 && (header & 0x07) == 0) {
155+
if (gotFirstPacketOfVp8Frame && fragmentedSampleSizeBytes > 0) {
156+
// Received new VP8 fragment, output data of previous fragment to decoder.
157+
outputSampleMetadataForFragmentedPackets();
160158
}
161159
gotFirstPacketOfVp8Frame = true;
162-
} else {
160+
} else if (gotFirstPacketOfVp8Frame) {
163161
// Check that this packet is in the sequence of the previous packet.
164162
int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber);
165-
if (packetSequenceNumber != expectedSequenceNumber) {
163+
if (packetSequenceNumber < expectedSequenceNumber) {
166164
Log.w(
167165
TAG,
168166
Util.formatInvariant(
@@ -171,6 +169,9 @@ private boolean validateVp8Descriptor(ParsableByteArray payload, int packetSeque
171169
expectedSequenceNumber, packetSequenceNumber));
172170
return false;
173171
}
172+
} else {
173+
Log.w(TAG, "RTP packet is not the start of a new VP8 partition, skipping.");
174+
return false;
174175
}
175176

176177
// Check if optional X header is present.
@@ -199,6 +200,23 @@ private boolean validateVp8Descriptor(ParsableByteArray payload, int packetSeque
199200
return true;
200201
}
201202

203+
/**
204+
* Outputs sample metadata.
205+
*
206+
* <p>Call this method only when receiving a end of VP8 partition
207+
*/
208+
private void outputSampleMetadataForFragmentedPackets() {
209+
trackOutput.sampleMetadata(
210+
sampleTimeUsOfFragmentedSample,
211+
isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0,
212+
fragmentedSampleSizeBytes,
213+
/* offset= */ 0,
214+
/* cryptoData= */ null);
215+
fragmentedSampleSizeBytes = 0;
216+
sampleTimeUsOfFragmentedSample = C.TIME_UNSET;
217+
gotFirstPacketOfVp8Frame = false;
218+
}
219+
202220
private static long toSampleUs(
203221
long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp) {
204222
return startTimeOffsetUs
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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+
import static org.mockito.ArgumentMatchers.anyInt;
21+
import static org.mockito.Mockito.when;
22+
23+
import androidx.media3.common.Format;
24+
import androidx.media3.common.MimeTypes;
25+
import androidx.media3.common.util.ParsableByteArray;
26+
import androidx.media3.exoplayer.rtsp.RtpPacket;
27+
import androidx.media3.exoplayer.rtsp.RtpPayloadFormat;
28+
import androidx.media3.extractor.ExtractorOutput;
29+
import androidx.media3.test.utils.FakeTrackOutput;
30+
import androidx.test.ext.junit.runners.AndroidJUnit4;
31+
import com.google.common.collect.ImmutableMap;
32+
import org.junit.Before;
33+
import org.junit.Rule;
34+
import org.junit.Test;
35+
import org.junit.runner.RunWith;
36+
import org.mockito.Mock;
37+
import org.mockito.junit.MockitoJUnit;
38+
import org.mockito.junit.MockitoRule;
39+
40+
/**
41+
* Unit test for {@link RtpVp8Reader}.
42+
*/
43+
@RunWith(AndroidJUnit4.class)
44+
public final class RtpVp8ReaderTest {
45+
46+
private final RtpPacket frame1fragment1 =
47+
createRtpPacket(
48+
/* timestamp= */ 2599168056L,
49+
/* sequenceNumber= */ 40289,
50+
/* marker= */ false,
51+
/* payloadData= */ getBytesFromHexString("10000102030405060708090A"));
52+
private final RtpPacket frame1fragment2 =
53+
createRtpPacket(
54+
/* timestamp= */ 2599168056L,
55+
/* sequenceNumber= */ 40290,
56+
/* marker= */ true,
57+
/* payloadData= */ getBytesFromHexString("000B0C0D0E"));
58+
private final byte[] frame1Data = getBytesFromHexString("000102030405060708090A0B0C0D0E");
59+
private final RtpPacket frame2fragment1 =
60+
createRtpPacket(
61+
/* timestamp= */ 2599168344L,
62+
/* sequenceNumber= */ 40291,
63+
/* marker= */ false,
64+
/* payloadData= */ getBytesFromHexString("100D0C0B0A090807060504"));
65+
// Add optional headers
66+
private final RtpPacket frame2fragment2 =
67+
createRtpPacket(
68+
/* timestamp= */ 2599168344L,
69+
/* sequenceNumber= */ 40292,
70+
/* marker= */ true,
71+
/* payloadData= */ getBytesFromHexString("80D6AA95396103020100"));
72+
private final byte[] frame2Data = getBytesFromHexString("0D0C0B0A09080706050403020100");
73+
74+
private static final RtpPayloadFormat VP8_FORMAT =
75+
new RtpPayloadFormat(
76+
new Format.Builder()
77+
.setSampleMimeType(MimeTypes.VIDEO_VP8)
78+
.setSampleRate(500000)
79+
.build(),
80+
/* rtpPayloadType= */ 97,
81+
/* clockRate= */ 48_000,
82+
/* fmtpParameters= */ ImmutableMap.of());
83+
84+
@Rule
85+
public final MockitoRule mockito = MockitoJUnit.rule();
86+
87+
private ParsableByteArray packetData;
88+
89+
private RtpVp8Reader vp8Reader;
90+
private FakeTrackOutput trackOutput;
91+
@Mock
92+
private ExtractorOutput extractorOutput;
93+
94+
@Before
95+
public void setUp() {
96+
packetData = new ParsableByteArray();
97+
trackOutput = new FakeTrackOutput(/* deduplicateConsecutiveFormats= */ true);
98+
when(extractorOutput.track(anyInt(), anyInt())).thenReturn(trackOutput);
99+
vp8Reader = new RtpVp8Reader(VP8_FORMAT);
100+
vp8Reader.createTracks(extractorOutput, /* trackId= */ 0);
101+
}
102+
103+
@Test
104+
public void consume_validPackets() {
105+
vp8Reader.onReceivingFirstPacket(frame1fragment1.timestamp, frame1fragment1.sequenceNumber);
106+
consume(frame1fragment1);
107+
consume(frame1fragment2);
108+
consume(frame2fragment1);
109+
consume(frame2fragment2);
110+
111+
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
112+
assertThat(trackOutput.getSampleData(0)).isEqualTo(frame1Data);
113+
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
114+
assertThat(trackOutput.getSampleData(1)).isEqualTo(frame2Data);
115+
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(3200);
116+
}
117+
118+
@Test
119+
public void consume_fragmentedFrameMissingFirstFragment() {
120+
// First packet timing information is transmitted over RTSP, not RTP.
121+
vp8Reader.onReceivingFirstPacket(frame1fragment1.timestamp, frame1fragment1.sequenceNumber);
122+
consume(frame1fragment2);
123+
consume(frame2fragment1);
124+
consume(frame2fragment2);
125+
126+
assertThat(trackOutput.getSampleCount()).isEqualTo(1);
127+
assertThat(trackOutput.getSampleData(0)).isEqualTo(frame2Data);
128+
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(3200);
129+
}
130+
131+
@Test
132+
public void consume_fragmentedFrameMissingBoundaryFragment() {
133+
vp8Reader.onReceivingFirstPacket(frame1fragment1.timestamp, frame1fragment1.sequenceNumber);
134+
consume(frame1fragment1);
135+
consume(frame2fragment1);
136+
consume(frame2fragment2);
137+
138+
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
139+
assertThat(trackOutput.getSampleData(0))
140+
.isEqualTo(getBytesFromHexString("000102030405060708090A"));
141+
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
142+
assertThat(trackOutput.getSampleData(1)).isEqualTo(frame2Data);
143+
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(3200);
144+
}
145+
146+
@Test
147+
public void consume_outOfOrderFragmentedFrame() {
148+
vp8Reader.onReceivingFirstPacket(frame1fragment1.timestamp, frame1fragment1.sequenceNumber);
149+
consume(frame1fragment1);
150+
consume(frame2fragment1);
151+
consume(frame1fragment2);
152+
consume(frame2fragment2);
153+
154+
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
155+
assertThat(trackOutput.getSampleData(0))
156+
.isEqualTo(getBytesFromHexString("000102030405060708090A"));
157+
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
158+
assertThat(trackOutput.getSampleData(1)).isEqualTo(frame2Data);
159+
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(3200);
160+
}
161+
162+
private static RtpPacket createRtpPacket(
163+
long timestamp, int sequenceNumber, boolean marker, byte[] payloadData) {
164+
return new RtpPacket.Builder()
165+
.setTimestamp((int) timestamp)
166+
.setSequenceNumber(sequenceNumber)
167+
.setMarker(marker)
168+
.setPayloadData(payloadData)
169+
.build();
170+
}
171+
172+
private void consume(RtpPacket rtpPacket) {
173+
packetData.reset(rtpPacket.payloadData);
174+
vp8Reader.consume(
175+
packetData,
176+
rtpPacket.timestamp,
177+
rtpPacket.sequenceNumber,
178+
/* isFrameBoundary= */ rtpPacket.marker);
179+
}
180+
}

0 commit comments

Comments
 (0)