Skip to content

Commit 8943d0c

Browse files
authored
Merge pull request #8 from paulomorgado/MJpegTrack
Add MJpegTrack class for RTP MJPEG video handling
2 parents c3539cc + 7511ba5 commit 8943d0c

1 file changed

Lines changed: 245 additions & 0 deletions

File tree

src/SharpRTSPServer/MJpegTrack.cs

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
using System;
2+
using System.Buffers;
3+
using System.Buffers.Binary;
4+
using System.Collections.Generic;
5+
using System.Text;
6+
7+
namespace SharpRTSPServer
8+
{
9+
/// <see href="https://datatracker.ietf.org/doc/html/rfc2435" />
10+
public class MJpegTrack : TrackBase
11+
{
12+
/// <inheritdoc/>
13+
public override bool IsReady => true;
14+
15+
/// <inheritdoc/>
16+
public override int PayloadType { get; set; } = 26; // RTP_PT_JPEG - https://datatracker.ietf.org/doc/html/rfc2435 - Appendix C
17+
18+
/// <inheritdoc/>
19+
public override string Codec => "JPEG";
20+
21+
/// <inheritdoc/>
22+
public override int ID { get; set; }
23+
24+
public int Width { get; set; }
25+
26+
public int Height { get; set; }
27+
28+
/// <inheritdoc/>
29+
public override StringBuilder BuildSDP(StringBuilder sdp)
30+
{
31+
return sdp
32+
.Append("m=video 0 RTP/AVP ").Append(PayloadType).AppendLine()
33+
.Append("a=control:trackID=").Append(ID).AppendLine()
34+
;
35+
}
36+
37+
/// <inheritdoc/>
38+
public override (List<Memory<byte>>, List<IMemoryOwner<byte>>) CreateRtpPackets(List<byte[]> samples, uint rtpTimestamp)
39+
{
40+
if (samples.Count != 1)
41+
{
42+
throw new InvalidOperationException("Only 1 sample is supported.");
43+
}
44+
45+
var rtpPackets = new List<Memory<byte>>();
46+
var memoryOwners = new List<IMemoryOwner<byte>>();
47+
48+
for (int i = 0; i < samples.Count; i++)
49+
{
50+
// https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format
51+
const ushort SoiMarker = 0xFFD8; // SOI - Start of image header
52+
//const ushort App0Header = 0xFFE0; // Application Segment 0 header
53+
//const ushort App15Header = 0xFFEF; // Application Segment 15 header
54+
const ushort Sof0Marker = 0xFFC0; // SOF0 - Start of Frame marker
55+
const ushort DqtHeader = 0xFFDB; // Define Quantization Table header
56+
//const ushort SosMarker = 0xFFDA; // SOS - Start of Scan marker
57+
const ushort EoiMarker = 0xFFD9; // EOI - End of Image marker
58+
59+
var jpegImage = samples[i].AsSpan();
60+
61+
var header = BinaryPrimitives.ReadUInt16BigEndian(jpegImage);
62+
if (header != SoiMarker)
63+
{
64+
throw new InvalidOperationException($"JPEG image must start with SOI marker {SoiMarker.ToString("X4")} and {header.ToString("X4")}");
65+
}
66+
67+
header = BinaryPrimitives.ReadUInt16BigEndian(jpegImage.Slice(jpegImage.Length - 2));
68+
if (header != EoiMarker)
69+
{
70+
throw new InvalidOperationException($"JPEG image must start with SOI marker {EoiMarker.ToString("X4")} and {header.ToString("X4")}");
71+
}
72+
73+
//var reader = jpegImage[2..^2];
74+
var reader = jpegImage.Slice(2); // keep EOI
75+
76+
byte type = 1; // https://datatracker.ietf.org/doc/html/rfc2435#section-3.1.3
77+
byte q = 255; // https://datatracker.ietf.org/doc/html/rfc2435#section-3.1.4, https://datatracker.ietf.org/doc/html/rfc2435#section-4.2
78+
79+
int nbQuantizationTables = 0;
80+
var firstQuantizationtable = ReadOnlySpan<byte>.Empty;
81+
var secondQuantizationtable = ReadOnlySpan<byte>.Empty;
82+
83+
while (true)
84+
{
85+
header = BinaryPrimitives.ReadUInt16BigEndian(reader);
86+
87+
if (header == Sof0Marker)
88+
{
89+
break;
90+
}
91+
92+
reader = reader.Slice(2);
93+
94+
var size = BinaryPrimitives.ReadUInt16BigEndian(reader) - 2; reader = reader.Slice(2);
95+
96+
switch (header)
97+
{
98+
case DqtHeader:
99+
nbQuantizationTables++;
100+
if (nbQuantizationTables == 1)
101+
{
102+
firstQuantizationtable = reader.Slice(0, size);
103+
}
104+
else if (nbQuantizationTables == 2)
105+
{
106+
secondQuantizationtable = reader.Slice(0, size);
107+
}
108+
else
109+
{
110+
throw new InvalidOperationException("Error: More than 2 quantization tables in JPEG image");
111+
}
112+
break;
113+
default:
114+
break;
115+
}
116+
117+
reader = reader.Slice(size);
118+
}
119+
120+
// Build a list of 1 or more RTP packets
121+
// The last packet will have the M bit set to '1'
122+
123+
var endOfFrame = false;
124+
var firstFrame = true;
125+
126+
// -8 for UDP header, -20 for IP header, -16 normal RTP header len. ** LESS RTP EXTENSIONS !!!
127+
var packetMTU = 1400; // 65535;
128+
129+
var dataPointer = 0;
130+
131+
while (reader.Length > 0)
132+
{
133+
bool shouldSendQuantizationTables = firstFrame && q > 127;
134+
135+
firstFrame = false;
136+
137+
int payloadSize = Math.Min(packetMTU, reader.Length);
138+
139+
endOfFrame = payloadSize == reader.Length;
140+
141+
// 12 is header size. then jpeg header, then payload
142+
var destSize = 12 + 8 + payloadSize;
143+
var owner = MemoryPool<byte>.Shared.Rent(destSize);
144+
memoryOwners.Add(owner);
145+
var rtpPacket = owner.Memory.Slice(0, destSize);
146+
147+
// RTP Packet Header
148+
// 0 - Version, P, X, CC, M, PT and Sequence Number
149+
//32 - Timestamp. H264 uses a 90kHz clock
150+
//64 - SSRC
151+
//96 - CSRCs (optional)
152+
//nn - Extension ID and Length
153+
//nn - Extension header
154+
155+
var rtpPacketSpan = rtpPacket.Span;
156+
157+
rtpPacketSpan.Slice(3, 9).Clear();
158+
159+
RTPPacketUtil.WriteHeader(
160+
rtpPacket: rtpPacketSpan,
161+
rtpVersion: RTPPacketUtil.RTP_VERSION,
162+
rtpPadding: false,
163+
rtpExtension: false,
164+
rtpCsrcCount: 0,
165+
rtpMarker: endOfFrame,
166+
rtpPayloadType: PayloadType);
167+
168+
// sequence number and SSRC are set just before send
169+
RTPPacketUtil.WriteTS(rtpPacketSpan, rtpTimestamp);
170+
171+
rtpPacketSpan = rtpPacketSpan.Slice(12);
172+
173+
// For JPEG we need https://www.rfc-editor.org/rfc/rfc2435
174+
175+
BinaryPrimitives.WriteInt32BigEndian(rtpPacketSpan, dataPointer & 0x00FFFFFF);
176+
rtpPacketSpan = rtpPacketSpan.Slice(4);
177+
178+
// Write JPEG Header - https://datatracker.ietf.org/doc/html/rfc2435#section-3.1
179+
rtpPacketSpan[0] = type;
180+
rtpPacketSpan[1] = q;
181+
rtpPacketSpan[2] = (byte)(Width / 8);
182+
rtpPacketSpan[3] = (byte)(Height / 8);
183+
rtpPacketSpan = rtpPacketSpan.Slice(4);
184+
185+
// write quantization tables
186+
if (shouldSendQuantizationTables)
187+
{
188+
// Write Restart Marker header - https://datatracker.ietf.org/doc/html/rfc2435#section-3.1.7
189+
// Not present for type 1
190+
191+
// Write Quantization Table header https://datatracker.ietf.org/doc/html/rfc2435#section-3.1.8
192+
193+
if (nbQuantizationTables == 1)
194+
{
195+
// MBZ
196+
rtpPacketSpan[0] = (byte)(firstQuantizationtable[0] & 0xf);
197+
198+
// Precision
199+
rtpPacketSpan[1] = (byte)(firstQuantizationtable[0] >> 4);
200+
201+
// Length
202+
var qtSize = firstQuantizationtable.Length - 1;
203+
BinaryPrimitives.WriteInt16BigEndian(rtpPacketSpan.Slice(2), (short)(qtSize));
204+
205+
// Quantization Table Data
206+
firstQuantizationtable.Slice(1).CopyTo(rtpPacketSpan.Slice(4));
207+
qtSize += 4;
208+
rtpPacketSpan = rtpPacketSpan.Slice(qtSize);
209+
payloadSize -= qtSize;
210+
}
211+
else // nbQuantizationTables == 2
212+
{
213+
// MBZ
214+
rtpPacketSpan[0] = 0;
215+
216+
// Precision
217+
rtpPacketSpan[1] = (byte)(firstQuantizationtable[0] >> 4);
218+
219+
// Length
220+
var qtSize = firstQuantizationtable.Length + secondQuantizationtable.Length - 2;
221+
BinaryPrimitives.WriteInt16BigEndian(rtpPacketSpan.Slice(2), (short)(qtSize));
222+
223+
// Quantization Table Data
224+
firstQuantizationtable.Slice(1).CopyTo(rtpPacketSpan.Slice(4));
225+
secondQuantizationtable.Slice(1).CopyTo(rtpPacketSpan.Slice(3 + firstQuantizationtable.Length));
226+
qtSize += 4;
227+
rtpPacketSpan = rtpPacketSpan.Slice(qtSize);
228+
payloadSize -= qtSize;
229+
}
230+
}
231+
232+
// Write JPEG Payload
233+
234+
reader.Slice(0, rtpPacketSpan.Length).CopyTo(rtpPacketSpan);
235+
reader = reader.Slice(rtpPacketSpan.Length);
236+
dataPointer += rtpPacketSpan.Length;
237+
238+
rtpPackets.Add(rtpPacket);
239+
}
240+
}
241+
242+
return (rtpPackets, memoryOwners);
243+
}
244+
}
245+
}

0 commit comments

Comments
 (0)