forked from zillevdr/vdr-plugin-softhddevice-drm
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathpes.cpp
More file actions
750 lines (659 loc) · 25.9 KB
/
Copy pathpes.cpp
File metadata and controls
750 lines (659 loc) · 25.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
// SPDX-License-Identifier: AGPL-3.0-or-later
/**
* @file pes.cpp
* PES Packet Parser
*
* @license{AGPL-3.0-or-later}
*/
#include <functional>
#include <map>
#include <stdexcept>
#include "pes.h"
#include "logger.h"
#include "misc.h"
#include <vdr/remux.h>
extern "C" {
#include <libavutil/avutil.h>
}
/**
* @addtogroup misc
* @{
*/
/**
* Codec Information Structure
*
* Contains lambdas for sync word detection and frame size calculation
*/
struct CodecInfo
{
int minSize;
std::function<bool(const uint8_t*)> MatchSyncWord;
std::function<int(const uint8_t*)> GetFrameSize;
};
/**
* Map of Audio Codec Information
*
* Key: AVCodecID
* Value: CodecInfo with sync word detection and frame size calculation lambdas
*/
static const std::map<AVCodecID, CodecInfo> AudioCodecMap = {
{AV_CODEC_ID_MP2, {
.minSize = 3,
.MatchSyncWord = [](const uint8_t* data) -> bool {
constexpr uint32_t MPEG_AUDIO_SYNC_WORD = 0xFF'E000;
constexpr uint32_t MPEG_AUDIO_VERSION_FORBIDDEN_VALUE = 0x00'0800;
constexpr uint32_t MPEG_AUDIO_LAYER_DESCRIPTION_FORBIDDEN_VALUE = 0x00'0000;
constexpr uint32_t MPEG_AUDIO_BITRATE_INDEX_FORBIDDEN_VALUE = 0x00'00F0;
uint32_t syncWord = ReadBytes(data, 3);
return (syncWord & 0b1111'1111'1110'0000'0000'0000) == MPEG_AUDIO_SYNC_WORD &&
(syncWord & 0b0000'0000'0001'1000'0000'0000) != MPEG_AUDIO_VERSION_FORBIDDEN_VALUE &&
(syncWord & 0b0000'0000'0000'0110'0000'0000) != MPEG_AUDIO_LAYER_DESCRIPTION_FORBIDDEN_VALUE &&
(syncWord & 0b0000'0000'0000'0000'1111'0000) != MPEG_AUDIO_BITRATE_INDEX_FORBIDDEN_VALUE;
},
.GetFrameSize = [](const uint8_t* data) -> int {
constexpr uint16_t BitRateTable[2][4][16] = {
// MPEG Version 1
{{},
{0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0},
{0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0},
{0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0}},
// MPEG Version 2 & 2.5
{{},
{0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0},
{0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0},
{0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0}
}
};
constexpr uint16_t SampleRateTable[4] = {44100, 48000, 32000, 0};
int mpeg2 = !(data[1] & 0x08) && (data[1] & 0x10);
int mpeg25 = !(data[1] & 0x08) && !(data[1] & 0x10);
int layer = 4 - ((data[1] >> 1) & 0x03);
int bitRateIndex = (data[2] >> 4) & 0x0F;
int sampleRateIndex = (data[2] >> 2) & 0x03;
int padding = (data[2] >> 1) & 0x01;
int sampleRate = SampleRateTable[sampleRateIndex];
if (!sampleRate)
throw std::invalid_argument("MPEG: invalid sample rate");
sampleRate >>= mpeg2;
sampleRate >>= mpeg25;
int bitRate = BitRateTable[mpeg2 | mpeg25][layer][bitRateIndex];
if (!bitRate)
throw std::invalid_argument("MPEG: invalid bit rate");
bitRate *= 1000;
int frameSize;
switch (layer) {
case 1:
frameSize = (12 * bitRate) / sampleRate;
frameSize = (frameSize + padding) * 4;
break;
case 2:
frameSize = (144 * bitRate) / sampleRate;
frameSize = frameSize + padding;
break;
case 3:
default:
frameSize = (((mpeg2 || mpeg25) ? 72 : 144) * bitRate) / sampleRate;
frameSize = frameSize + padding;
break;
}
return frameSize;
}
}},
{AV_CODEC_ID_AAC_LATM, {
.minSize = 3,
.MatchSyncWord = [](const uint8_t* data) -> bool {
constexpr uint32_t LOAS_SYNC_WORD_MASK = 0xFFE000;
constexpr uint32_t LOAS_SYNC_WORD = 0x2B7 << (24-11);
uint32_t syncWord = ReadBytes(data, 3);
return (syncWord & LOAS_SYNC_WORD_MASK) == LOAS_SYNC_WORD;
},
.GetFrameSize = [](const uint8_t* data) -> int {
return ((data[1] & 0x1F) << 8) + data[2] + 3;
}
}},
{AV_CODEC_ID_AC3, {
.minSize = 6,
.MatchSyncWord = [](const uint8_t* data) -> bool {
constexpr uint32_t AC3_SYNC_WORD_MASK = 0xFFFF00;
constexpr uint32_t AC3_SYNC_WORD = 0x0B77 << (24-16);
uint32_t syncWord = ReadBytes(data, 3);
return (syncWord & AC3_SYNC_WORD_MASK) == AC3_SYNC_WORD &&
data[5] <= (10 << 3);
},
.GetFrameSize = [](const uint8_t* data) -> int {
constexpr uint16_t Ac3FrameSizeTable[38][3] = {
{64, 69, 96}, {64, 70, 96}, {80, 87, 120}, {80, 88, 120},
{96, 104, 144}, {96, 105, 144}, {112, 121, 168}, {112, 122, 168},
{128, 139, 192}, {128, 140, 192}, {160, 174, 240}, {160, 175, 240},
{192, 208, 288}, {192, 209, 288}, {224, 243, 336}, {224, 244, 336},
{256, 278, 384}, {256, 279, 384}, {320, 348, 480}, {320, 349, 480},
{384, 417, 576}, {384, 418, 576}, {448, 487, 672}, {448, 488, 672},
{512, 557, 768}, {512, 558, 768}, {640, 696, 960}, {640, 697, 960},
{768, 835, 1152}, {768, 836, 1152}, {896, 975, 1344}, {896, 976, 1344},
{1024, 1114, 1536}, {1024, 1115, 1536}, {1152, 1253, 1728},
{1152, 1254, 1728}, {1280, 1393, 1920}, {1280, 1394, 1920},
};
int fscod = data[4] >> 6;
if (fscod == 0x03)
throw std::invalid_argument("AC3: invalid sample rate");
int frmsizcod = data[4] & 0x3F;
if (frmsizcod > 37)
throw std::invalid_argument("AC3: invalid frame size");
return Ac3FrameSizeTable[frmsizcod][fscod] * 2;
}
}},
{AV_CODEC_ID_EAC3, {
.minSize = 6,
.MatchSyncWord = [](const uint8_t* data) -> bool {
constexpr uint32_t AC3_SYNC_WORD = 0x0B77 << (24-16);
uint32_t syncWord = ReadBytes(data, 3);
return (syncWord & 0xFFFF00) == AC3_SYNC_WORD && data[5] > (10 << 3);
},
.GetFrameSize = [](const uint8_t* data) -> int {
if ((data[4] & 0xF0) == 0xF0)
throw std::invalid_argument("E-AC3: invalid fscod fscod2");
return (((data[2] & 0x07) << 8) + data[3] + 1) * 2;
}
}},
{AV_CODEC_ID_AAC, {
.minSize = 7,
.MatchSyncWord = [](const uint8_t* data) -> bool {
constexpr uint32_t ADTS_SYNC_WORD = 0xFFF000;
constexpr uint32_t ADTS_LAYER = 0x000000;
constexpr uint32_t ADTS_SAMPLING_FREQUENCY_FORBIDDEN_VALUE = 15 << 6;
uint32_t syncWord = ReadBytes(data, 3);
return (syncWord & 0b1111'1111'1111'0000'0000'0000) == ADTS_SYNC_WORD &&
(syncWord & 0b0000'0000'0000'0110'0000'0000) == ADTS_LAYER &&
(syncWord & 0b0000'0000'0000'0011'1100'0000) != ADTS_SAMPLING_FREQUENCY_FORBIDDEN_VALUE;
},
.GetFrameSize = [](const uint8_t* data) -> int {
return ((data[3] & 0x03) << 11) | ((data[4] & 0xFF) << 3) | ((data[5] & 0xE0) >> 5);
}
}},
{AV_CODEC_ID_DTS, {
.minSize = 8,
.MatchSyncWord = [](const uint8_t* data) -> bool {
constexpr uint32_t DTS_SYNC_WORD = 0x7FFE8001;
uint32_t syncWord = ReadBytes(data, 4);
return syncWord == DTS_SYNC_WORD;
},
.GetFrameSize = [](const uint8_t* data) -> int {
int frameSize = ((data[5] & 0x03) << 12) | ((data[6] & 0xFF) << 4) | ((data[7] & 0xF0) >> 4);
frameSize += 1;
if (frameSize < 80 || frameSize > 8191)
throw std::invalid_argument("DTS: invalid frame size");
return frameSize;
}
}}
};
/** @} */
/**
* Create a PES packet parser
*
* Initializes the parser with a pointer to PES packet data and its size.
* The actual validation is performed by calling Init() in derived classes.
*
* @param data Pointer to the raw PES packet data
* @param size Size of the PES packet in bytes
*/
cPes::cPes(const uint8_t *data, int size, bool isAudio)
: m_data(data),
m_size(size),
m_identifier(isAudio ? "audio" : "video")
{
}
/**
* Initialize and validate the PES packet
*
* Performs validation checks on the PES packet structure:
* - Validates the PES header (start code prefix)
* - Checks if the stream ID matches the expected type (audio/video)
* - Ensures the packet has sufficient size for the header
* - Verifies the payload offset is within bounds
*
* Sets m_valid to true if all checks pass. Called by derived class constructors.
*/
void cPes::Init(void)
{
if (IsHeaderValid() && IsStreamIdValid()) {
if (m_size <= 8 || PesPayloadOffset(m_data) > m_size) // header length field is at position 8 when the PES extension is present
LOGWARNING("pes: %s: %s packet too short: %d %02X", __FUNCTION__, m_identifier, m_size, GetStreamId());
else
m_valid = true;
} else if (PesLongEnough(m_size)) {
LOGDEBUG("pes: %s: invalid %s packet: %d %02X%02X%02X | %02X", __FUNCTION__, m_identifier, m_size, m_data[0], m_data[1], m_data[2], GetStreamId());
} else {
LOGDEBUG("pes: %s: %s packet too short: %d", __FUNCTION__, m_identifier, m_size);
}
}
/**
* Check if the PES packet is valid
*
* Validates that the PES packet is well-formed and matches the expected stream type by checking:
* - The PES header is valid
* - The stream ID matches the expected stream type (video or audio)
*
* The stream ID is masked with 0xF0 to check the stream type category (e.g., 0xE0 for video, 0xC0 for audio)
* while ignoring the low nibble which indicates the specific stream number.
*
* Video streams have stream IDs in the range 0xE0-0xEF according to
* H.222.0 03/2017 Table 2-22, audio streams have IDs in the range 0xC0-0xCF.
*
* @return true if the packet is valid and matches the expected stream type, false otherwise
*/
bool cPes::IsValid(void)
{
return m_valid;
}
/**
* Check if the PES header is valid
*
* Validates that the PES packet has a valid header by checking:
* - The packet is long enough to contain a header
* - The start code prefix (0x000001) is present
*
* @return true if the header is valid, false otherwise
*/
bool cPes::IsHeaderValid(void)
{
return PesLongEnough(m_size) && ReadBytes(m_data, 3) == PES_PACKET_START_CODE_PREFIX;
}
/**
* Check if the PES packet contains a Presentation Time Stamp (PTS)
*
* Examines the PES header flags to determine if a PTS is present in the packet.
* The PTS presence is indicated by specific bits in the PES header flags field.
*
* @return true if the PES packet contains a PTS, false otherwise
*/
bool cPes::HasPts(void)
{
return PesHasPts(m_data);
}
/**
* Get the Presentation Time Stamp (PTS) from the PES header
*
* Extracts the PTS value from the PES packet header if present.
* The PTS indicates when the decoded content should be presented.
*
* @return The PTS value in 90 kHz units, or AV_NOPTS_VALUE if no PTS is present
*/
int64_t cPes::GetPts(void)
{
if (!HasPts())
return AV_NOPTS_VALUE;
return PesGetPts(m_data);
}
/**
* Get a pointer to the PES payload data
*
* Returns a pointer to the start of the payload data, skipping the PES header.
* For H.264/HEVC streams with a leading zero byte, the leading zero is also skipped.
*
* @return Pointer to the payload data
*/
const uint8_t *cPes::GetPayload(void)
{
return &m_data[PesPayloadOffset(m_data)];
}
/**
* Get the size of the PES payload
*
* Calculates the size of the payload by subtracting the header size
* (and optional leading zero for H.264/HEVC) from the total packet size.
*
* @return Size of the payload in bytes
*/
int cPes::GetPayloadSize(void)
{
return m_size - PesPayloadOffset(m_data);
}
/**
* Get the total length of the PES packet
*
* Returns the complete size of the PES packet including both header and payload.
* The length is read from the PES packet header (bytes 4-5).
*
* For packets with a specified length field (common for audio):
* - Returns the actual PES packet length from the header
* - Calculated as 6 + length_field (per H.222.0 standard)
* - The length_field specifies bytes after the 6-byte header prefix
* (3 bytes start code + 1 byte stream ID + 2 bytes length field)
*
* For unbounded packets (length field = 0, common for video streams):
* - Returns the input buffer size (m_size)
*
* @return Total size in bytes: actual packet length if specified, otherwise input buffer size
*/
int cPes::GetPacketLength(void)
{
if (!PesHasLength(m_data))
return m_size; // Length field is 0, meaning unbounded/unspecified. Return raw data size.
return PesLength(m_data);
}
/********************************************************************************
* Reassembly buffer
*******************************************************************************/
/**
* Pop an AVPacket from the reassembly buffer
*
* Creates an AVPacket containing the specified amount of data from the buffer.
*
* @param size Number of bytes to pop from the buffer
*
* @return Allocated AVPacket with data, or nullptr if size is 0
*/
AVPacket *cReassemblyBuffer::PopAvPacket(int size)
{
if (size == 0)
return nullptr;
AVPacket *avpkt = av_packet_alloc();
if (!avpkt)
LOGFATAL("pes: %s: out of memory while allocating AVPacket", __FUNCTION__);
if (av_new_packet(avpkt, size)) // allocates size + AV_INPUT_BUFFER_PADDING_SIZE
LOGFATAL("pes: %s: out of memory while allocating AVPacket payload", __FUNCTION__);
memcpy(avpkt->data, m_buffer.Peek(), size);
memset(&avpkt->data[size], 0, AV_INPUT_BUFFER_PADDING_SIZE);
// Only audio:
// If a PES packet contains multiple frames, only the AVPacket with the first frame of that PES packet shall have a PTS value, when sending it to the decoder.
// The following AVPackets created from this PES packet shall have no PTS value.
// When retrieving PTS values from the same PES packet, they will be identical.
if (m_buffer.GetPts() != m_lastPoppedPts)
avpkt->pts = m_buffer.GetPts();
else
avpkt->pts = AV_NOPTS_VALUE;
m_lastPoppedPts = m_buffer.GetPts();
m_buffer.Erase(size);
return avpkt;
}
/********************************************************************************
* Video specific implementation
*******************************************************************************/
/**
* Parse video codec header to detect codec type
*
* Analyzes video frame start codes and stream type bytes to identify the codec.
* Supports MPEG2, H.264, and HEVC video codecs.
*
* @param fragment Pointer to video frame data
* @param size Size of the fragment in bytes
*
* @return true if a codec was detected, false otherwise
*/
bool cReassemblyBufferVideo::ParseCodecHeader(const uint8_t *fragment, int size)
{
if (size < VIDEO_FRAME_START_CODE_LEN)
return false;
const uint8_t *codecPayload = &fragment[VIDEO_FRAME_START_CODE_LEN];
uint32_t startCode = ReadBytes(fragment, VIDEO_FRAME_START_CODE_LEN);
// Looking for the MPEG2 start code and stream type in the PES payload
if (startCode == VIDEO_FRAME_START_CODE && codecPayload[0] == MPEG2_STREAM_TYPE)
m_codec = AV_CODEC_ID_MPEG2VIDEO;
else if (HasLeadingZero(fragment, size)) // Looking for a leading zero byte in front of the start code. Can be present in H.264/HEVC streams.
codecPayload++;
else if (startCode != VIDEO_FRAME_START_CODE)
return false; // No start code: PES packet carries fragmented payload, or unknown codec.
if (size > &codecPayload[7] - fragment) {
if ( codecPayload[0] == H264_STREAM_TYPE && (codecPayload[1] == 0x10 || codecPayload[1] == 0xF0 || codecPayload[7] == 0x64))
m_codec = AV_CODEC_ID_H264;
else if (codecPayload[0] == HEVC_STREAM_TYPE && (codecPayload[1] == 0x10 || codecPayload[1] == 0x50 || codecPayload[7] == 0x40))
m_codec = AV_CODEC_ID_HEVC;
}
return m_codec != AV_CODEC_ID_NONE;
}
/**
* Check if video data has a leading zero byte before the start code
*
* Some H.264/HEVC streams include a leading zero byte (0x00) before
* the standard start code (0x000001). This method detects that pattern.
*
* @param data Pointer to video data
* @param size Size of the data in bytes
*
* @return true if a leading zero is present, false otherwise
*/
bool cReassemblyBufferVideo::HasLeadingZero(const uint8_t *data, int size)
{
return size > VIDEO_FRAME_START_CODE_LEN + 1 && data[0] == 0 && ReadBytes(&data[1], VIDEO_FRAME_START_CODE_LEN) == VIDEO_FRAME_START_CODE;
}
/********************************************************************************
* Audio specific implementation
*******************************************************************************/
/**
* Pop an audio AVPacket from the reassembly buffer
*
* Truncates the buffer to the first valid audio frame, detects the codec,
* and pops a complete audio frame as an AVPacket.
*
* @return Allocated AVPacket with one audio frame, or nullptr if no valid frame found
*/
AVPacket *cReassemblyBufferAudio::PopAvPacket(void)
{
AVCodecID detectedCodec = TruncateBufferUntilFirstValidData();
if (detectedCodec == AV_CODEC_ID_NONE)
return nullptr; // No sync word found in the buffer. Wait for more data.
else if (m_codec != AV_CODEC_ID_NONE && detectedCodec != m_codec)
LOGERROR("pes: %s: audio codec changed unexpectedly from %s to %s", __FUNCTION__, avcodec_get_name(m_codec), avcodec_get_name(detectedCodec));
m_codec = detectedCodec;
try {
AVPacket *packet = cReassemblyBuffer::PopAvPacket(AudioCodecMap.at(m_codec).GetFrameSize(m_buffer.Peek()));
if (!packet)
return nullptr;
if (m_ptsInvalid) { // the PTS is invalid for this packet because the buffer was truncated before
packet->pts = AV_NOPTS_VALUE;
m_ptsInvalid = false;
}
return packet;
} catch (const std::invalid_argument &e) {
LOGWARNING("pes: %s: garbage in audio stream received: %s", __FUNCTION__, e.what());
// the garbage will be removed in the next call to TruncateBufferUntilFirstValidData()
}
return nullptr;
}
/**
* Truncate buffer until the first valid audio frame
*
* Searches for two consecutive audio frames with the same sync word to validate
* the frame header, then erases all data before the first valid frame.
* This removes garbage data and synchronizes to the audio stream.
*
* @return Detected codec ID, or AV_CODEC_ID_NONE if no valid frames found
*/
AVCodecID cReassemblyBufferAudio::TruncateBufferUntilFirstValidData(void)
{
int sizeBeforeTruncation = m_buffer.GetSize();
SyncWordInfo firstFrame = FindTwoConsecutiveFramesWithSameSyncWord();
m_buffer.Erase(firstFrame.pos);
if (m_buffer.GetSize() < sizeBeforeTruncation) {
LOGDEBUG("pes: %s: truncated %d of %d bytes while searching for sync word", __FUNCTION__, sizeBeforeTruncation - m_buffer.GetSize(), sizeBeforeTruncation);
m_ptsInvalid = true;
}
return firstFrame.codecId;
}
/**
* Find two consecutive audio frames with the same sync word
*
* Searches the buffer for a valid audio frame followed immediately by another
* frame of the same codec. This validates that the sync word and frame header
* are genuine and not false positives in random data.
*
* The function modifies the buffer internally by erasing false positives as it searches.
* When no sync word is found, it keeps the last MAX_HEADER_SIZE bytes.
*
* @return SyncWordInfo with codec ID and position, or AV_CODEC_ID_NONE if not found
*/
SyncWordInfo cReassemblyBufferAudio::FindTwoConsecutiveFramesWithSameSyncWord(void)
{
while (true) {
SyncWordInfo firstFrame = FindSyncWord(m_buffer.Peek(), m_buffer.GetSize());
if (firstFrame.codecId == AV_CODEC_ID_NONE) // No sync word found in the entire buffer. Keep only the last few bytes that could contain a partial sync word.
return SyncWordInfo{AV_CODEC_ID_NONE, std::max(0, (int)m_buffer.GetSize() - MAX_HEADER_SIZE)};
try {
// determine the length of the first found potential frame by reading the frame's header
int sizeOfFirstFrame = AudioCodecMap.at(firstFrame.codecId).GetFrameSize(&m_buffer.Peek()[firstFrame.pos]);
int secondSyncWord = firstFrame.pos + sizeOfFirstFrame;
// check if another sync word follows immediately after the first frame to validate the header of the first frame is a real header and no random data
if (secondSyncWord + MAX_HEADER_SIZE > (int)m_buffer.GetSize()) {
// Could not find the second sync word, because there might not be enough data in the buffer to contain a complete second sync word. Wait for more data.
// In case we have a false positive and the header's frame size field is invalid, we buffer the following amount of data in worst-case:
// - MP2: 6913 bytes (Layer 2/3: 384kbps @ 8kHz + padding)
// - AAC LATM: 8194 bytes (13-bit length field max: 0x1FFF + 3)
// - AC3: 2788 bytes (frmsizcod=37, fscod=1: 1394 * 2)
// - EAC3: 4096 bytes (11-bit field max: 2048 * 2)
// - AAC ADTS: 8191 bytes (13-bit length field max: 0x1FFF)
// - DTS: 8192 bytes (14-bit length field max: 0x1FFF + 1)
return SyncWordInfo{AV_CODEC_ID_NONE, firstFrame.pos};
} else if (DetectCodecFromSyncWord(&m_buffer.Peek()[secondSyncWord], m_buffer.GetSize() - secondSyncWord) == firstFrame.codecId)
// two consecutive frames with the same sync word found, and the first frame's header length field is valid
return SyncWordInfo{firstFrame.codecId, firstFrame.pos};
} catch (const std::invalid_argument &e) {
// Failed to read the frame size from the first frame's header. The found sync word is a false positive.
}
// If we found one sync word, but did not find a second one at the expected position, the first one was a false positive in the middle of random data.
// In this case, continue the search one position after the start of the first found sync word.
m_buffer.Erase(firstFrame.pos + 1);
}
}
/**
* Find the first audio sync word in data
*
* Scans the data byte-by-byte looking for any recognized audio sync word pattern.
* Checks all supported audio codecs (MP2, AAC LATM, AAC ADTS, AC3, E-AC3, DTS).
*
* @param data Pointer to audio data
* @param size Size of the data in bytes
*
* @return SyncWordInfo with detected codec and position, or AV_CODEC_ID_NONE if not found
*/
SyncWordInfo cReassemblyBufferAudio::FindSyncWord(const uint8_t *data, int size)
{
for (int i = 0; i < size; i++) {
AVCodecID detectedCodec = DetectCodecFromSyncWord(&data[i], size - i);
if (detectedCodec != AV_CODEC_ID_NONE)
return SyncWordInfo{detectedCodec, i};
}
return SyncWordInfo{AV_CODEC_ID_NONE, -1};
}
/**
* Detect audio codec from sync word pattern
*
* Checks if the data starts with a valid sync word for any supported audio codec.
* Uses the AudioCodecMap to test sync word patterns for MP2, AAC LATM, AAC ADTS, AC3, E-AC3 and DTS.
*
* @param syncWord Pointer to potential sync word data
* @param size Size of available data
*
* @return Detected AVCodecID, or AV_CODEC_ID_NONE if no match
*/
AVCodecID cReassemblyBufferAudio::DetectCodecFromSyncWord(const uint8_t *syncWord, int size)
{
for (const auto& [codecId, codecInfo] : AudioCodecMap) {
if (size >= codecInfo.minSize && codecInfo.MatchSyncWord(syncWord)) {
return codecId;
}
}
return AV_CODEC_ID_NONE;
}
/**
* Get the frame size for a given codec and frame header
*
* Calculates the frame size by parsing the codec-specific frame header.
* Only used for testing purposes to expose the AudioCodecMap frame size calculation.
*
* @param codec The audio codec ID
* @param data Pointer to the frame header data
*
* @return Frame size in bytes
* @throws std::out_of_range if codec is not in AudioCodecMap
* @throws std::invalid_argument if frame header is invalid
*/
int cReassemblyBufferAudio::GetFrameSizeForCodec(AVCodecID codec, const uint8_t *data)
{
return AudioCodecMap.at(codec).GetFrameSize(data);
}
/**
* Reset the reassembly buffer
*
* Clears all buffered data, PTS tracking, and resets codec detection state.
*/
void cReassemblyBuffer::Reset(void)
{
m_buffer.Reset();
m_codec = AV_CODEC_ID_NONE;
m_lastPoppedPts = AV_NOPTS_VALUE;
}
/********************************************************************************
* PTS tracking buffer
*******************************************************************************/
/**
* Push data into the PTS tracking buffer
*
* Appends data to the buffer and associates the PTS with the current buffer position
* if a valid PTS is provided.
*
* @param data Pointer to data to append
* @param size Size of data in bytes
* @param pts Presentation timestamp, or AV_NOPTS_VALUE if not available
*/
void cPtsTrackingBuffer::Push(const uint8_t *data, int size, int64_t pts)
{
if (pts != AV_NOPTS_VALUE) // PES packets not starting with a new frame (fragmented data) have no PTS
m_pts[m_data.size()] = pts;
m_data.insert(m_data.end(), data, data + size);
}
/**
* Erase data from the beginning of the buffer
*
* Removes the specified number of bytes from the front of the buffer and adjusts
* all PTS positions accordingly. The PTS value for the new position 0 is preserved
* by finding the last PTS value before the erase point.
*
* This ensures that when frames are popped from the buffer, they retain the PTS
* of the PES packet where the frame started, even if that PES packet has been
* partially consumed.
*
* @param amount Number of bytes to erase from the beginning
*/
void cPtsTrackingBuffer::Erase(size_t amount)
{
if (m_data.empty() || amount == 0)
return;
if (amount > m_data.size()) {
LOGERROR("pes: %s: %s: erase amount %zu exceeds buffer size %zu!", __FUNCTION__, m_identifier, amount, m_data.size());
amount = m_data.size();
}
// Only PES packets have PTS values, but not the (fragmented) frames inside.
// The reassembled frame's PTS value will become the PTS value of the PES packet where the frame starts.
// Therefore, always keep the PTS value for position 0 in the buffer, which is the PTS value of the PES packet where the frame starts.
// This is normally the largest PTS value to be removed, or, if future position 0 already has a PTS value, that value will be used.
int64_t smallestPts = AV_NOPTS_VALUE;
auto it = m_pts.upper_bound(amount);
if (it == m_pts.begin())
LOGFATAL("pes: %s: %s: no PTS value found for position 0 after erasing %zu bytes", __FUNCTION__, m_identifier, amount);
else {
--it; // Move to the last entry before 'amount'
smallestPts = it->second;
}
std::map<size_t, int64_t> adjusted_pts;
for (const auto& [pos, pts] : m_pts) {
if (pos >= amount) // erase all PTS entries for data that will be removed
adjusted_pts[pos - amount] = pts; // adjust remaining PTS entries to the new data indices
}
m_pts = std::move(adjusted_pts);
m_pts[0] = smallestPts;
m_data.erase(m_data.begin(), m_data.begin() + amount);
}
/**
* Get the PTS value for the current buffer position
*
* Returns the PTS associated with position 0 in the buffer, which represents
* the presentation timestamp for the data at the front of the buffer.
*
* @return PTS value, or AV_NOPTS_VALUE if no PTS is available
*/
int64_t cPtsTrackingBuffer::GetPts(void)
{
if (m_pts.empty())
return AV_NOPTS_VALUE;
return m_pts.begin()->second;
}