arduino-audio-tools
Loading...
Searching...
No Matches
MP4Parser.h
Go to the documentation of this file.
1#pragma once
2
3#include <cstdint>
4#include <cstring>
5#include <functional>
6#include <string>
7
9
10namespace audio_tools {
11
45class MP4Parser {
46 public:
50 struct Box {
51 friend class MP4Parser;
52 friend class MP4ParserExt;
54 size_t id = 0;
55 size_t seq = 0;
56 char type[5];
57 const uint8_t* data =
58 nullptr;
59 size_t data_size = 0;
60 size_t size =
61 0;
62 int level = 0;
63 uint64_t file_offset = 0;
64 int available = 0;
65 bool is_complete = false;
66 bool is_incremental = false;
67 bool is_container = false;
68 };
69
70 using BoxCallback = std::function<void(Box&, void* ref)>;
71
76 char type[5];
79 true;
80 };
81
86 void setReference(void* ref) { this->ref = ref; }
87
92 void setCallback(BoxCallback cb) { callback = cb; }
93
101 void setCallback(const char* type, BoxCallback cb, bool callGeneric = true) {
102 CallbackEntry entry;
103 strncpy(entry.type, type, 4);
104 entry.type[4] = '\0'; // Ensure null-termination
105 entry.cb = cb;
106 entry.callGeneric = callGeneric;
107 callbacks.push_back(entry);
108 };
109
115 bool resize(size_t size) {
116 buffer.resize(size);
117 return buffer.size() == size;
118 }
119
129 bool begin(uint64_t startFileOffset = 0) {
130 buffer.clear();
131 if (buffer.size() == 0) buffer.resize(2 * 1024);
132 parseOffset = 0;
133 fileOffset = startFileOffset;
135 box.is_complete = true; // Start with no open box
136 box.data = nullptr;
137 box.size = 0;
138 box.level = 0;
139 box.file_offset = startFileOffset;
140 box.id = 0;
141 box.is_incremental = false;
142 box.is_complete = true;
143 // reset incremental-box state too, in case begin() is called again
144 // mid-parse (see startFileOffset above)
145 box_in_progress = false;
148 box_seq = 0;
150 moov_found = false;
151 return true;
152 }
153
160 size_t write(const uint8_t* data, size_t len) {
161 if (is_error) return len; // If an error occurred, skip writing
162 // The accumulation buffer (default 2KB, see begin()) is usually much
163 // smaller than a single caller-side write (e.g. a 16-64KB StreamCopy
164 // chunk): a single writeArray() silently truncates at buffer.size(),
165 // dropping the remainder with no error, which desyncs the parser from
166 // the real byte stream (misread box headers, huge bogus sizes, etc.).
167 // Feed it in buffer-sized slices, parsing (and thereby freeing space)
168 // between each one, until every byte has been accepted.
169 size_t total_written = 0;
170 while (total_written < len) {
171 size_t avail = buffer.availableForWrite();
172 if (avail == 0) {
173 parse();
174 avail = buffer.availableForWrite();
175 if (avail == 0) break; // parser made no progress: avoid a hang
176 }
177 size_t chunk = std::min(avail, len - total_written);
178 size_t written = buffer.writeArray(data + total_written, chunk);
179 if (written == 0) break; // safety net, should not happen
180 total_written += written;
181 parse();
182 }
183 return total_written;
184 }
185
192 size_t write(const char* data, size_t len) {
193 return write(reinterpret_cast<const uint8_t*>(data), len);
194 }
195
201
210
216 void addContainer(const char* name, int start = 0) {
217 ContainerInfo info;
218 info.name = name;
219 info.start = start; // offset of child boxes
220 }
221
228 int parseString(const uint8_t* str, int len, int fileOffset = 0,
229 int level = 0) {
230 char type[5];
231 int idx = 0;
232 Box box;
233 while (true) {
234 if (!isValidType((const char*)str + idx + 4)) {
235 return idx;
236 }
237 size_t box_size = readU32(str + idx) - 8;
238 box.data = str + 8 + idx;
239 box.size = box_size;
240 box.level = level;
242 box.file_offset = fileOffset + idx;
243 box.is_complete = true;
244 box.is_incremental = false;
245 strncpy(box.type, (char*)(str + idx + 4), 4);
246 box.type[4] = '\0';
247 idx += box.size;
249 if (idx >= len) break; // No more data to parse
250 }
251 return idx;
252 }
253
255 bool findBox(const char* name, const uint8_t* data, size_t len, Box& result) {
256 for (int j = 0; j < len - 4; j++) {
257 if (!isValidType((const char*)data + j + 4)) {
258 continue; // Skip invalid types
259 }
260 size_t box_size = readU32(data + j) - 8;
261 if (box_size < 8) continue; // Invalid box size
262 Box box;
263 box.data = data + j + 8;
264 box.size = box_size;
266 strncpy(box.type, (char*)(data + j + 4), 4);
267 box.type[4] = '\0';
268 if (StrView(box.type) == name) {
269 result = box;
270 return true; // Found the box
271 }
272 }
273 return false;
274 }
275
281 static void defaultCallback(const Box& box, void* ref) {
282 char space[box.level * 2 + 1];
283 char str_buffer[200];
284 memset(space, ' ', box.level * 2);
285 space[box.level * 2] = '\0'; // Null-terminate the string
286 snprintf(str_buffer, sizeof(str_buffer),
287 "%s- #%u %u) %s, Offset: %u, Size: %u, Data Size: %u, Available: %u", space,
288 (unsigned)box.id, (unsigned) box.seq, box.type, (unsigned)box.file_offset,
289 (unsigned)box.size, (unsigned) box.data_size, (unsigned) box.available);
290#ifdef ARDUINO
291 Serial.println(str_buffer);
292#else
293 printf("%s\n", str_buffer);
294#endif
295 }
296
297 protected:
302 size_t parseOffset = 0;
303 uint64_t fileOffset = 0;
304 void* ref = this;
306 bool is_error = false;
307 bool moov_found = false;
309
314 const char* name = nullptr;
315 int start = 0;
316 };
318protected:
320 false;
323 char box_type[5] = {0};
324 int box_level = 0;
325 int box_seq = 0;
327
331 void parse() {
332 while (true) {
333 size_t bufferSize = buffer.available();
334 if (!box_in_progress) {
335 if (!tryStartNewBox(bufferSize)) break;
336 } else {
337 if (!continueIncrementalBox()) break;
338 }
339 popLevels();
340 }
342 }
343
349 bool tryStartNewBox(size_t bufferSize) {
350 if (parseOffset + 8 > bufferSize) return false;
351 char type[5];
352 box_seq = 0;
353
354 // get basic box information
356 const uint8_t* p = buffer.data() + parseOffset;
357 uint32_t size32 = readU32(p);
358 strncpy(type, (char*)(p + 4), 4);
359 type[4] = '\0';
360
361 uint64_t boxSize = size32;
362 size_t headerSize = 8;
363 // A 32-bit size of 0 or 1 are reserved ISO/IEC 14496-12 special cases
364 // that a plain 32-bit compare against headerSize can't handle.
365 bool unbounded = false;
366 if (size32 == 1) {
367 // 64-bit "largesize" follows the type field
368 if (parseOffset + 16 > bufferSize) return false; // wait for largesize
369 boxSize = readU64(p + 8);
370 headerSize = 16;
371 } else if (size32 == 0) {
372 // box extends to the end of the file/stream (e.g. a streamed mdat
373 // written by an encoder that never seeks back to patch in the size)
374 unbounded = true;
375 }
376
377 if (!unbounded && boxSize < headerSize) return false;
378
379 int level = static_cast<int>(levelStack.size());
380
381 // mdat holds the audio payload and must be preceded by moov, otherwise
382 // sample/format info needed to decode it is missing.
383 if (level == 0 && strcmp(type, "mdat") == 0 && !moov_found) {
385 LOGE(
386 "mdat box found before moov box: moov must precede mdat (e.g. "
387 "use 'ffmpeg -movflags +faststart') or this file will not play "
388 "correctly");
389 } else {
390 LOGW(
391 "mdat box found before moov box: moov should precede mdat (e.g. "
392 "use 'ffmpeg -movflags +faststart') - continuing anyway since "
393 "the requirement was opted out of");
394 }
395 }
396
397 bool is_container = isContainerBox(type);
398
399 if (is_container) {
400 if (unbounded) {
401 LOGE("Unsupported: container box '%s' with size 0 (extends to EOF)",
402 type);
403 return false;
404 }
405 handleContainerBox(type, boxSize, headerSize, level);
406 return true;
407 }
408
409 size_t payload_size =
410 unbounded ? SIZE_MAX : static_cast<size_t>(boxSize - headerSize);
411 if (!unbounded && parseOffset + boxSize <= bufferSize) {
412 // start with full buffer!
413 handleCompleteBox(type, p, headerSize, payload_size, level);
414 parseOffset += boxSize;
415 } else {
416 startIncrementalBox(type, p, headerSize, payload_size, level, bufferSize);
417 return false; // Wait for more data
418 }
419 return true;
420 }
421
428 void handleContainerBox(const char* type, uint64_t boxSize,
429 size_t headerSize, int level) {
430 strcpy(box.type, type);
431 ++box.id;
432 box.data = nullptr;
433 box.size = static_cast<size_t>(boxSize - headerSize);
434 box.data_size = 0;
435 box.available = 0;
436 box.level = level;
438 box.is_incremental = false;
439 box.is_complete = true;
440 box.is_container = true;
441 box.seq = 0;
442
443 if (strcmp(type, "moov") == 0) moov_found = true;
444
446
447 uint64_t absBoxOffset = fileOffset + parseOffset;
448 levelStack.push_back(absBoxOffset + boxSize);
449 parseOffset += headerSize;
450 }
451
460 void handleCompleteBox(const char* type, const uint8_t* p, size_t headerSize,
461 size_t payload_size, int level) {
462 strcpy(box.type, type);
463 ++box.id;
464 box.data = p + headerSize;
465 box.size = payload_size;
466 box.data_size = payload_size;
467 box.level = level;
469 box.is_complete = true;
470 box.is_container = false;
471 box.available = payload_size;
472 box.is_incremental = false;
473 box.seq = 0;
474
476 }
477
487 void startIncrementalBox(const char* type, const uint8_t* p,
488 size_t headerSize, size_t payload_size, int level,
489 size_t bufferSize) {
490 box_in_progress = true;
492 box_bytes_expected = payload_size;
493 strncpy(box_type, type, 5);
494 box_level = level;
495 box_seq = 0;
496
497 size_t available_payload = bufferSize - parseOffset - headerSize;
499 if (available_payload > 0) {
500 box_bytes_received += available_payload;
501 strcpy(box.type, box_type);
502 ++box.id;
503 box.data = p + headerSize;
506 box.available = available_payload;
509 box.seq = 0;
510 box.is_incremental = true;
511 box.is_complete = false;
512 box.is_container = false;
514 }
515 // fileOffset += (bufferSize - buffer.available());
516 if (payload_size == SIZE_MAX) {
517 // unbounded box (extends to EOF): its end is unknown, so only
518 // advance past the header that was just consumed
519 fileOffset += (parseOffset + headerSize);
520 } else {
521 fileOffset += (parseOffset + payload_size + headerSize);
522 }
523 incremental_offset += available_payload;
524 buffer.clear();
525 parseOffset = 0;
526 }
527
534 size_t to_read = std::min((size_t)box_bytes_expected - box_bytes_received,
535 (size_t)buffer.available());
536 if (to_read == 0) return true;
537 strcpy(box.type, box_type);
538 ++box.id;
539 box.data = buffer.data();
542 box.available = to_read;
546 box.is_container = false;
547 box.is_incremental = true;
548 box.seq = ++box_seq;
550 box_bytes_received += to_read;
551 // fileOffset += to_read;
552 buffer.clearArray(to_read);
553 incremental_offset += to_read;
554
556 box_in_progress = false;
557 }
558 return false;
559 }
560
565 if (parseOffset > 0) {
568 parseOffset = 0;
569 }
570 }
571
576 uint64_t currentFileOffset() { return fileOffset + parseOffset; }
577
583 static uint32_t readU32(const uint8_t* p) {
584 return (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3];
585 }
586
592 static uint64_t readU64(const uint8_t* p) {
593 return ((uint64_t)readU32(p) << 32) | readU32(p + 4);
594 }
595
596
600 void popLevels() {
601 // Pop levels if we've passed their bounds (absolute file offset)
602 while (!levelStack.empty() &&
605 }
606 }
607
615 bool is_called = false;
616 bool call_generic = true;
617 for (const auto& entry : callbacks) {
618 if (strncmp(entry.type, box.type, 4) == 0) {
619 entry.cb(box, ref);
620 is_called = true;
621 if (!entry.callGeneric) call_generic = false;
622 }
623 }
625 if ((!is_called || call_generic) && callback) callback(box, ref);
626 }
627
633 bool isContainerBox(const char* type) {
634 // fill with default values if nothing has been defined
635 if (containers.empty()) {
636 // pure containers
637 static const char* containers_str[] = {
638 "moov", "trak", "mdia", "minf", "stbl", "edts", "dinf", "udta",
639 "ilst", "moof", "traf", "mfra", "tref", "iprp", "sinf", "schi"};
640 for (const char* c : containers_str) {
641 ContainerInfo info;
642 info.name = c;
643 info.start = 0;
644 containers.push_back(info);
645 }
646 // container with data
647 ContainerInfo info;
648 info.name = "meta";
649 info.start = 4; // 4 bytes: version (1 byte) + flags (3 bytes)
650 containers.push_back(info);
651 }
652 // find the container by name
653 for (auto& cont : containers) {
654 if (StrView(type) == cont.name) return true;
655 }
656 return false;
657 }
658
664 int getContainerDataLength(const char* type) {
665 for (auto& cont : containers) {
666 if (StrView(type) == cont.name) return cont.start;
667 }
668 return 0;
669 }
670
677 bool isValidType(const char* type, int offset = 0) const {
678 // Check if the type is a valid 4-character string
679 return (type != nullptr && isalnum(type[offset]) &&
680 isalnum(type[offset + 1]) && isalnum(type[offset + 2]) &&
681 isalnum(type[offset + 3]));
682 }
683
689 size_t current = parseOffset;
690 const char* type = (char*)(buffer.data() + parseOffset + 4);
691 for (int j = 0; j < buffer.available() - parseOffset - 4; j += 4) {
692 if (isValidType(type, j)) {
693 if (j != 0) {
694 // report the data under the last valid box
695 box.size = 0;
696 box.data_size = j;
697 box.level = static_cast<int>(levelStack.size()) + 1;
700 }
701
702 return j + parseOffset;
703 }
704 }
705 return parseOffset;
706 }
707};
708
709} // namespace audio_tools
static HardwareSerial Serial
Definition Arduino.h:179
#define LOGW(...)
Definition AudioLoggerIDF.h:29
#define LOGE(...)
Definition AudioLoggerIDF.h:30
void clear()
same as reset
Definition Buffers.h:96
MP4Parser is a class that parses MP4 container files and extracts boxes (atoms). It provides a callba...
Definition MP4Parser.h:45
void handleCompleteBox(const char *type, const uint8_t *p, size_t headerSize, size_t payload_size, int level)
Handles a complete (non-incremental) box.
Definition MP4Parser.h:460
void setCallback(const char *type, BoxCallback cb, bool callGeneric=true)
Defines a specific callback for a box type.
Definition MP4Parser.h:101
void * ref
Reference pointer for callbacks.
Definition MP4Parser.h:304
Vector< size_t > levelStack
Stack for container box levels.
Definition MP4Parser.h:301
size_t box_bytes_expected
Total expected bytes for the current box.
Definition MP4Parser.h:322
static void defaultCallback(const Box &box, void *ref)
Default callback that prints box information to Serial.
Definition MP4Parser.h:281
size_t checkParseOffset()
Checks and adjusts the parse offset for valid box types.
Definition MP4Parser.h:688
void setRequireMoovBeforeMdat(bool flag)
Defines whether an mdat box found before any moov box is treated as an error (LOGE,...
Definition MP4Parser.h:209
int getContainerDataLength(const char *type)
Gets the start offset for a subcontainer.
Definition MP4Parser.h:664
int box_level
Current box level (nesting)
Definition MP4Parser.h:324
bool continueIncrementalBox()
Continue filling an incremental box. Returns false if not enough data.
Definition MP4Parser.h:533
Vector< CallbackEntry > callbacks
List of type-specific callbacks.
Definition MP4Parser.h:299
void startIncrementalBox(const char *type, const uint8_t *p, size_t headerSize, size_t payload_size, int level, size_t bufferSize)
Starts parsing a box incrementally.
Definition MP4Parser.h:487
Vector< ContainerInfo > containers
List of container box info.
Definition MP4Parser.h:317
static uint32_t readU32(const uint8_t *p)
Reads a 32-bit big-endian unsigned integer from a buffer.
Definition MP4Parser.h:583
void setCallback(BoxCallback cb)
Defines the generic callback for all boxes.
Definition MP4Parser.h:92
int availableForWrite()
Returns the available space for writing.
Definition MP4Parser.h:200
char box_type[5]
Current box type.
Definition MP4Parser.h:323
int parseString(const uint8_t *str, int len, int fileOffset=0, int level=0)
Trigger separate parsing (and callbacks) on the indicated string.
Definition MP4Parser.h:228
void finalizeParse()
Finalizes parsing, updating file offset and clearing buffer.
Definition MP4Parser.h:564
bool begin(uint64_t startFileOffset=0)
Initializes the parser.
Definition MP4Parser.h:129
void setReference(void *ref)
Defines an optional reference. By default it is the parser itself.
Definition MP4Parser.h:86
size_t incremental_offset
Definition MP4Parser.h:326
bool isValidType(const char *type, int offset=0) const
Checks if a type string is a valid 4-character box type.
Definition MP4Parser.h:677
static uint64_t readU64(const uint8_t *p)
Reads a 64-bit big-endian unsigned integer from a buffer.
Definition MP4Parser.h:592
uint64_t currentFileOffset()
Returns the current file offset (absolute position in file).
Definition MP4Parser.h:576
void addContainer(const char *name, int start=0)
Adds a box name that will be interpreted as a container.
Definition MP4Parser.h:216
bool moov_found
True once a top-level moov box has been seen.
Definition MP4Parser.h:307
bool tryStartNewBox(size_t bufferSize)
Try to start parsing a new box. Returns false if not enough data.
Definition MP4Parser.h:349
SingleBuffer< uint8_t > buffer
Buffer for incoming data.
Definition MP4Parser.h:300
bool require_moov_before_mdat
false: warn instead of error
Definition MP4Parser.h:308
Box box
Current box being processed.
Definition MP4Parser.h:305
bool box_in_progress
True if currently parsing a box incrementally.
Definition MP4Parser.h:319
void processCallback(Box &box)
Processes the callback for a box. Calls the type-specific callback if present, and the generic callba...
Definition MP4Parser.h:614
size_t parseOffset
Current parse offset in buffer.
Definition MP4Parser.h:302
int box_seq
Definition MP4Parser.h:325
bool is_error
True if an error occurred.
Definition MP4Parser.h:306
void handleContainerBox(const char *type, uint64_t boxSize, size_t headerSize, int level)
Handles a container box (box with children).
Definition MP4Parser.h:428
bool findBox(const char *name, const uint8_t *data, size_t len, Box &result)
find box in box
Definition MP4Parser.h:255
void parse()
Main parsing loop. Handles incremental and complete boxes.
Definition MP4Parser.h:331
BoxCallback callback
Generic callback for all boxes.
Definition MP4Parser.h:298
bool resize(size_t size)
Defines a specific buffer size.
Definition MP4Parser.h:115
size_t box_bytes_received
Bytes received so far for the current box.
Definition MP4Parser.h:321
bool isContainerBox(const char *type)
Checks if a box type is a container box.
Definition MP4Parser.h:633
size_t write(const char *data, size_t len)
Provide the data to the parser (in chunks if needed).
Definition MP4Parser.h:192
uint64_t fileOffset
Current file offset.
Definition MP4Parser.h:303
std::function< void(Box &, void *ref)> BoxCallback
Definition MP4Parser.h:70
void popLevels()
Pops levels from the stack if we've passed their bounds.
Definition MP4Parser.h:600
size_t write(const uint8_t *data, size_t len)
Provide the data to the parser (in chunks if needed).
Definition MP4Parser.h:160
A simple Buffer implementation which just uses a (dynamically sized) array.
Definition Buffers.h:194
size_t size() override
Definition Buffers.h:325
int available() override
provides the number of entries that are available to read
Definition Buffers.h:255
int availableForWrite() override
provides the number of entries that are available to write
Definition Buffers.h:260
int writeArray(const T data[], int len) override
Fills the buffer data.
Definition Buffers.h:223
T * data()
Provides address of actual data.
Definition Buffers.h:306
bool resize(size_t size)
Resizes the buffer if supported: returns false if not supported.
Definition Buffers.h:327
int clearArray(int len) override
consumes len bytes and moves current data to the beginning
Definition Buffers.h:274
A simple wrapper to provide string functions on existing allocated char*. If the underlying char* is ...
Definition StrView.h:29
Vector implementation which provides the most important methods as defined by std::vector....
Definition Vector.h:21
void pop_back()
Definition Vector.h:214
bool empty()
Definition Vector.h:180
T & back()
Definition Vector.h:289
void push_back(T &&value)
Definition Vector.h:182
void clear()
Definition Vector.h:176
int size()
Definition Vector.h:178
Generic Implementation of sound input and output for desktop environments using portaudio.
Definition LMSEchoCancellationStream.h:6
Represents an individual box in the MP4 file.
Definition MP4Parser.h:50
size_t id
Unique box ID.
Definition MP4Parser.h:54
bool is_container
True if the box is a container.
Definition MP4Parser.h:67
const uint8_t * data
Pointer to box payload (not including header)
Definition MP4Parser.h:57
int available
Number of bytes available as data.
Definition MP4Parser.h:64
uint64_t file_offset
File offset where box starts.
Definition MP4Parser.h:63
size_t size
Size of payload including subboxes (not including header)
Definition MP4Parser.h:60
bool is_complete
True if the box data is complete.
Definition MP4Parser.h:65
bool is_incremental
True if the box is being parsed incrementally.
Definition MP4Parser.h:66
size_t seq
Sequence number for the box per id.
Definition MP4Parser.h:55
char type[5]
4-character box type (null-terminated)
Definition MP4Parser.h:56
friend class MP4ParserExt
Definition MP4Parser.h:52
int level
Nesting depth.
Definition MP4Parser.h:62
size_t data_size
Size of payload (not including header)
Definition MP4Parser.h:59
Structure for type-specific callbacks.
Definition MP4Parser.h:75
BoxCallback cb
Callback function.
Definition MP4Parser.h:77
char type[5]
4-character box type
Definition MP4Parser.h:76
bool callGeneric
If true, also call the generic callback after this one.
Definition MP4Parser.h:78
Structure for container box information.
Definition MP4Parser.h:313
int start
Offset of child boxes.
Definition MP4Parser.h:315
const char * name
Name of the container box.
Definition MP4Parser.h:314