Skip to content

Commit e54d2f5

Browse files
committed
Merge pull request #119 from ittiam-systems:rtp_h263_test_and_fix
PiperOrigin-RevId: 463146426
2 parents 8ce3d4d + ef57a06 commit e54d2f5

File tree

3 files changed

+274
-17
lines changed

3 files changed

+274
-17
lines changed

RELEASENOTES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
small icon ([#104](https://github.com/androidx/media/issues/104)).
3131
* Ensure commands sent before `MediaController.release()` are not dropped
3232
([#99](https://github.com/androidx/media/issues/99)).
33+
* RTSP:
34+
* Add H263 fragmented packet handling
35+
([#119](https://github.com/androidx/media/pull/119)).
3336

3437
### 1.0.0-beta02 (2022-07-21)
3538

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

Lines changed: 49 additions & 17 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;
@@ -61,6 +63,12 @@
6163
private boolean isKeyFrame;
6264
private boolean isOutputFormatSet;
6365
private long startTimeOffsetUs;
66+
private long fragmentedSampleTimeUs;
67+
/**
68+
* Whether the first packet of a H263 frame is received, it mark the start of a H263 partition. A
69+
* H263 frame can be split into multiple RTP packets.
70+
*/
71+
private boolean gotFirstPacketOfH263Frame;
6472

6573
/** Creates an instance. */
6674
public RtpH263Reader(RtpPayloadFormat payloadFormat) {
@@ -76,7 +84,10 @@ public void createTracks(ExtractorOutput extractorOutput, int trackId) {
7684
}
7785

7886
@Override
79-
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+
}
8091

8192
@Override
8293
public void consume(
@@ -103,6 +114,12 @@ public void consume(
103114
}
104115

105116
if (pBitIsSet) {
117+
if (gotFirstPacketOfH263Frame && fragmentedSampleSizeBytes > 0) {
118+
// Received new H263 fragment, output data of previous fragment to decoder.
119+
outputSampleMetadataForFragmentedPackets();
120+
}
121+
gotFirstPacketOfH263Frame = true;
122+
106123
int payloadStartCode = data.peekUnsignedByte() & 0xFC;
107124
// Packets that begin with a Picture Start Code(100000). Refer RFC4629 Section 6.1.
108125
if (payloadStartCode < PICTURE_START_CODE) {
@@ -113,10 +130,10 @@ public void consume(
113130
data.getData()[currentPosition] = 0;
114131
data.getData()[currentPosition + 1] = 0;
115132
data.setPosition(currentPosition);
116-
} else {
133+
} else if (gotFirstPacketOfH263Frame) {
117134
// Check that this packet is in the sequence of the previous packet.
118135
int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber);
119-
if (sequenceNumber != expectedSequenceNumber) {
136+
if (sequenceNumber < expectedSequenceNumber) {
120137
Log.w(
121138
TAG,
122139
Util.formatInvariant(
@@ -125,6 +142,12 @@ public void consume(
125142
expectedSequenceNumber, sequenceNumber));
126143
return;
127144
}
145+
} else {
146+
Log.w(
147+
TAG,
148+
"First payload octet of the H263 packet is not the beginning of a new H263 partition,"
149+
+ " Dropping current packet.");
150+
return;
128151
}
129152

130153
if (fragmentedSampleSizeBytes == 0) {
@@ -141,20 +164,10 @@ public void consume(
141164
// Write the video sample.
142165
trackOutput.sampleData(data, fragmentSize);
143166
fragmentedSampleSizeBytes += fragmentSize;
167+
fragmentedSampleTimeUs = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp);
144168

145169
if (rtpMarker) {
146-
if (firstReceivedTimestamp == C.TIME_UNSET) {
147-
firstReceivedTimestamp = timestamp;
148-
}
149-
long timeUs = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp);
150-
trackOutput.sampleMetadata(
151-
timeUs,
152-
isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0,
153-
fragmentedSampleSizeBytes,
154-
/* offset= */ 0,
155-
/* cryptoData= */ null);
156-
fragmentedSampleSizeBytes = 0;
157-
isKeyFrame = false;
170+
outputSampleMetadataForFragmentedPackets();
158171
}
159172
previousSequenceNumber = sequenceNumber;
160173
}
@@ -167,8 +180,8 @@ public void seek(long nextRtpTimestamp, long timeUs) {
167180
}
168181

169182
/**
170-
* Parses and set VOP Coding type and resolution. The {@link ParsableByteArray#position} is
171-
* preserved.
183+
* Parses and set VOP Coding type and resolution. The {@linkplain ParsableByteArray#getPosition()
184+
* position} is preserved.
172185
*/
173186
private void parseVopHeader(ParsableByteArray data, boolean gotResolution) {
174187
// Picture Segment Packets (RFC4629 Section 6.1).
@@ -211,6 +224,25 @@ private void parseVopHeader(ParsableByteArray data, boolean gotResolution) {
211224
isKeyFrame = false;
212225
}
213226

227+
/**
228+
* Outputs sample metadata of the received fragmented packets.
229+
*
230+
* <p>Call this method only after receiving an end of a H263 partition.
231+
*/
232+
private void outputSampleMetadataForFragmentedPackets() {
233+
checkNotNull(trackOutput)
234+
.sampleMetadata(
235+
fragmentedSampleTimeUs,
236+
isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0,
237+
fragmentedSampleSizeBytes,
238+
/* offset= */ 0,
239+
/* cryptoData= */ null);
240+
fragmentedSampleSizeBytes = 0;
241+
fragmentedSampleTimeUs = C.TIME_UNSET;
242+
isKeyFrame = false;
243+
gotFirstPacketOfH263Frame = false;
244+
}
245+
214246
private static long toSampleUs(
215247
long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp) {
216248
return startTimeOffsetUs
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
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 RtpH263Reader}. */
39+
@RunWith(AndroidJUnit4.class)
40+
public final class RtpH263ReaderTest {
41+
42+
private static final long MEDIA_CLOCK_FREQUENCY = 90_000;
43+
44+
private static final byte[] FRAME_1_FRAGMENT_1_DATA =
45+
getBytesFromHexString("80020c0419b7b7d9591f03023e0c37b");
46+
private static final long PARTITION_1_RTP_TIMESTAMP = 2599168056L;
47+
private static final RtpPacket PACKET_FRAME_1_FRAGMENT_1 =
48+
new RtpPacket.Builder()
49+
.setTimestamp(PARTITION_1_RTP_TIMESTAMP)
50+
.setSequenceNumber(40289)
51+
.setMarker(false)
52+
.setPayloadData(
53+
Bytes.concat(
54+
/*payload header */ getBytesFromHexString("0400"), FRAME_1_FRAGMENT_1_DATA))
55+
.build();
56+
private static final byte[] FRAME_1_FRAGMENT_2_DATA =
57+
getBytesFromHexString("03140e0e77d5e83021a0c37");
58+
private static final RtpPacket PACKET_FRAME_1_FRAGMENT_2 =
59+
new RtpPacket.Builder()
60+
.setTimestamp(PARTITION_1_RTP_TIMESTAMP)
61+
.setSequenceNumber(40290)
62+
.setMarker(true)
63+
.setPayloadData(
64+
Bytes.concat(
65+
/*payload header */ getBytesFromHexString("0000"), FRAME_1_FRAGMENT_2_DATA))
66+
.build();
67+
// Needs to add 0000 to byte stream, refer to RFC4629 Section 6.1.1.
68+
private static final byte[] FRAME_1_DATA =
69+
Bytes.concat(getBytesFromHexString("0000"), FRAME_1_FRAGMENT_1_DATA, FRAME_1_FRAGMENT_2_DATA);
70+
71+
private static final byte[] FRAME_2_FRAGMENT_1_DATA =
72+
getBytesFromHexString("800a0e023ffffffffffffffffff");
73+
private static final long PARTITION_2_RTP_TIMESTAMP = 2599168344L;
74+
private static final RtpPacket PACKET_FRAME_2_FRAGMENT_1 =
75+
new RtpPacket.Builder()
76+
.setTimestamp(PARTITION_2_RTP_TIMESTAMP)
77+
.setSequenceNumber(40291)
78+
.setMarker(false)
79+
.setPayloadData(
80+
Bytes.concat(
81+
/*payload header */ getBytesFromHexString("0400"), FRAME_2_FRAGMENT_1_DATA))
82+
.build();
83+
private static final byte[] FRAME_2_FRAGMENT_2_DATA =
84+
getBytesFromHexString("830df80c501839dfccdbdbecac");
85+
private static final RtpPacket PACKET_FRAME_2_FRAGMENT_2 =
86+
new RtpPacket.Builder()
87+
.setTimestamp(PARTITION_2_RTP_TIMESTAMP)
88+
.setSequenceNumber(40292)
89+
.setMarker(true)
90+
.setPayloadData(
91+
Bytes.concat(
92+
/*payload header */ getBytesFromHexString("0000"), FRAME_2_FRAGMENT_2_DATA))
93+
.build();
94+
private static final byte[] FRAME_2_DATA =
95+
Bytes.concat(getBytesFromHexString("0000"), FRAME_2_FRAGMENT_1_DATA, FRAME_2_FRAGMENT_2_DATA);
96+
97+
private static final long PARTITION_2_PRESENTATION_TIMESTAMP_US =
98+
Util.scaleLargeTimestamp(
99+
(PARTITION_2_RTP_TIMESTAMP - PARTITION_1_RTP_TIMESTAMP),
100+
/* multiplier= */ C.MICROS_PER_SECOND,
101+
/* divisor= */ MEDIA_CLOCK_FREQUENCY);
102+
103+
private static final RtpPayloadFormat H263_FORMAT =
104+
new RtpPayloadFormat(
105+
new Format.Builder()
106+
.setSampleMimeType(MimeTypes.VIDEO_H263)
107+
.setWidth(352)
108+
.setHeight(288)
109+
.build(),
110+
/* rtpPayloadType= */ 96,
111+
/* clockRate= */ (int) MEDIA_CLOCK_FREQUENCY,
112+
/* fmtpParameters= */ ImmutableMap.of());
113+
114+
private FakeExtractorOutput extractorOutput;
115+
116+
@Before
117+
public void setUp() {
118+
extractorOutput = new FakeExtractorOutput();
119+
}
120+
121+
@Test
122+
public void consume_validPackets() {
123+
RtpH263Reader h263Reader = new RtpH263Reader(H263_FORMAT);
124+
h263Reader.createTracks(extractorOutput, /* trackId= */ 0);
125+
h263Reader.onReceivingFirstPacket(
126+
PACKET_FRAME_1_FRAGMENT_1.timestamp, PACKET_FRAME_1_FRAGMENT_1.sequenceNumber);
127+
consume(h263Reader, PACKET_FRAME_1_FRAGMENT_1);
128+
consume(h263Reader, PACKET_FRAME_1_FRAGMENT_2);
129+
consume(h263Reader, PACKET_FRAME_2_FRAGMENT_1);
130+
consume(h263Reader, PACKET_FRAME_2_FRAGMENT_2);
131+
132+
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
133+
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
134+
assertThat(trackOutput.getSampleData(0)).isEqualTo(FRAME_1_DATA);
135+
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
136+
assertThat(trackOutput.getSampleData(1)).isEqualTo(FRAME_2_DATA);
137+
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
138+
}
139+
140+
@Test
141+
public void consume_fragmentedFrameMissingFirstFragment() {
142+
RtpH263Reader h263Reader = new RtpH263Reader(H263_FORMAT);
143+
h263Reader.createTracks(extractorOutput, /* trackId= */ 0);
144+
h263Reader.onReceivingFirstPacket(
145+
PACKET_FRAME_1_FRAGMENT_1.timestamp, PACKET_FRAME_1_FRAGMENT_1.sequenceNumber);
146+
consume(h263Reader, PACKET_FRAME_1_FRAGMENT_2);
147+
consume(h263Reader, PACKET_FRAME_2_FRAGMENT_1);
148+
consume(h263Reader, PACKET_FRAME_2_FRAGMENT_2);
149+
150+
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
151+
assertThat(trackOutput.getSampleCount()).isEqualTo(1);
152+
assertThat(trackOutput.getSampleData(0)).isEqualTo(FRAME_2_DATA);
153+
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
154+
}
155+
156+
@Test
157+
public void consume_fragmentedFrameMissingBoundaryFragment() {
158+
RtpH263Reader h263Reader = new RtpH263Reader(H263_FORMAT);
159+
h263Reader.createTracks(extractorOutput, /* trackId= */ 0);
160+
h263Reader.onReceivingFirstPacket(
161+
PACKET_FRAME_1_FRAGMENT_1.timestamp, PACKET_FRAME_1_FRAGMENT_1.sequenceNumber);
162+
consume(h263Reader, PACKET_FRAME_1_FRAGMENT_1);
163+
consume(h263Reader, PACKET_FRAME_2_FRAGMENT_1);
164+
consume(h263Reader, PACKET_FRAME_2_FRAGMENT_2);
165+
166+
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
167+
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
168+
assertThat(trackOutput.getSampleData(0))
169+
.isEqualTo(Bytes.concat(getBytesFromHexString("0000"), FRAME_1_FRAGMENT_1_DATA));
170+
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
171+
assertThat(trackOutput.getSampleData(1)).isEqualTo(FRAME_2_DATA);
172+
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
173+
}
174+
175+
@Test
176+
public void consume_outOfOrderPackets() {
177+
RtpH263Reader h263Reader = new RtpH263Reader(H263_FORMAT);
178+
h263Reader.createTracks(extractorOutput, /* trackId= */ 0);
179+
h263Reader.onReceivingFirstPacket(
180+
PACKET_FRAME_1_FRAGMENT_1.timestamp, PACKET_FRAME_1_FRAGMENT_1.sequenceNumber);
181+
consume(h263Reader, PACKET_FRAME_1_FRAGMENT_1);
182+
consume(h263Reader, PACKET_FRAME_2_FRAGMENT_1);
183+
consume(h263Reader, PACKET_FRAME_1_FRAGMENT_2);
184+
consume(h263Reader, PACKET_FRAME_2_FRAGMENT_2);
185+
186+
FakeTrackOutput trackOutput = extractorOutput.trackOutputs.get(0);
187+
assertThat(trackOutput.getSampleCount()).isEqualTo(2);
188+
assertThat(trackOutput.getSampleData(0))
189+
.isEqualTo(Bytes.concat(getBytesFromHexString("0000"), FRAME_1_FRAGMENT_1_DATA));
190+
assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0);
191+
assertThat(trackOutput.getSampleData(1)).isEqualTo(FRAME_2_DATA);
192+
assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(PARTITION_2_PRESENTATION_TIMESTAMP_US);
193+
}
194+
195+
private static void consume(RtpH263Reader h263Reader, RtpPacket rtpPacket) {
196+
rtpPacket = copyPacket(rtpPacket);
197+
h263Reader.consume(
198+
new ParsableByteArray(rtpPacket.payloadData),
199+
rtpPacket.timestamp,
200+
rtpPacket.sequenceNumber,
201+
rtpPacket.marker);
202+
}
203+
204+
private static RtpPacket copyPacket(RtpPacket packet) {
205+
RtpPacket.Builder builder =
206+
new RtpPacket.Builder()
207+
.setPadding(packet.padding)
208+
.setMarker(packet.marker)
209+
.setPayloadType(packet.payloadType)
210+
.setSequenceNumber(packet.sequenceNumber)
211+
.setTimestamp(packet.timestamp)
212+
.setSsrc(packet.ssrc);
213+
214+
if (packet.csrc.length > 0) {
215+
builder.setCsrc(Arrays.copyOf(packet.csrc, packet.csrc.length));
216+
}
217+
if (packet.payloadData.length > 0) {
218+
builder.setPayloadData(Arrays.copyOf(packet.payloadData, packet.payloadData.length));
219+
}
220+
return builder.build();
221+
}
222+
}

0 commit comments

Comments
 (0)