A lightweight, platform-agnostic Ogg container demuxer for embedded systems and general use.
- Zero-copy optimization: Returns pointers directly to input buffer when possible
- Streaming demux: Feed data in chunks, demuxer handles internal buffering
- Dynamic buffer growth: Starts small (1KB), grows as needed (configurable max)
- Custom allocators: Bring your own malloc/free/realloc or use defaults
- Platform-agnostic: No dependencies on ESP-IDF, libogg, or any platform-specific code
- RFC 3533 compliant: Single logical bitstream Ogg files with optional CRC validation
- Clean C++ API: Simple, well-documented interface
microOggDemuxer extracts packets from Ogg container streams. It handles:
- Page header parsing
- Segment table navigation
- Packet boundary detection
- Multi-page packet reassembly
- CRC validation (optional)
- Stream sequence validation
The demuxer is designed for memory-constrained embedded systems but works equally well on host platforms.
mkdir build && cd build
cmake ..
cmake --build .This library is designed to be included as a submodule in codec wrapper projects:
git submodule add https://github.com/esphome-libs/micro-ogg-demuxer.gitThen in your CMakeLists.txt:
add_subdirectory(micro-ogg-demuxer)
target_link_libraries(your_target PRIVATE micro_ogg_demuxer)#include <micro_ogg/ogg_demuxer.h>
micro_ogg::OggDemuxer demuxer;
while (have_data) {
micro_ogg::OggDemuxState state = demuxer.getNextPacket(input_ptr, input_len);
if (state.result == micro_ogg::OGG_OK) {
// Process packet
processPacket(state.packet.data, state.packet.length);
}
input_ptr += state.bytes_consumed;
input_len -= state.bytes_consumed;
}micro_ogg::OggDemuxerConfig config;
config.min_buffer_size = 512; // Start with 512 bytes
config.max_buffer_size = 32768; // Grow up to 32KB
config.enable_crc = true; // Enable CRC validation (see caveats below)
micro_ogg::OggDemuxer demuxer(config);micro_ogg::OggDemuxerConfig config;
config.alloc = [](size_t size) -> void* {
return my_malloc(size);
};
config.realloc = [](void* ptr, size_t size) -> void* {
return my_realloc(ptr, size);
};
config.free = [](void* ptr) {
my_free(ptr);
};
micro_ogg::OggDemuxer demuxer(config);Configuration structure for customizing demuxer behavior:
| Field | Type | Default | Description |
|---|---|---|---|
min_buffer_size |
size_t |
1024 | Initial buffer size in bytes |
max_buffer_size |
size_t |
8192 | Maximum buffer size (conservative default) |
enable_crc |
bool |
false | Enable CRC32 validation (see CRC Validation) |
alloc |
function pointer | nullptr | Custom allocator (nullptr = use malloc) |
realloc |
function pointer | nullptr | Custom reallocator (nullptr = use realloc) |
free |
function pointer | nullptr | Custom deallocator (nullptr = use free) |
Result codes returned by demuxer methods:
| Code | Value | Description |
|---|---|---|
OGG_OK |
0 | Success - packet available |
OGG_NEED_MORE_DATA |
1 | Need more input data |
OGG_PACKET_SKIPPED |
2 | Packet too large, was skipped |
OGG_INVALID_CAPTURE |
-1 | Invalid "OggS" magic bytes |
OGG_INVALID_VERSION |
-2 | Unsupported Ogg version |
OGG_CRC_FAILED |
-3 | CRC checksum mismatch |
OGG_STREAM_SEQUENCE_ERROR |
-4 | Page sequence error |
OGG_STREAM_BOS_ERROR |
-5 | BOS flag violation |
OGG_STREAM_EOS_ERROR |
-6 | EOS flag violation |
OGG_STREAM_SERIAL_MISMATCH |
-7 | New stream (call reset() to continue) |
OGG_ALLOCATION_FAILED |
-8 | Memory allocation failed |
Packet data structure:
struct OggPacket {
const uint8_t* data; // Packet payload (zero-copy when possible)
size_t length; // Packet length in bytes
int64_t granule_position; // Codec-specific position
bool is_bos; // Beginning of stream flag
bool is_eos; // End of stream flag
bool is_last_on_page; // Last packet on current page
};Important: data pointer is only valid until the next demuxer call. Copy the data if you need to keep it.
OggDemuxState getNextPacket(const uint8_t* input, size_t input_len);Demux input and return next complete packet. Returns struct containing:
result: Demux result codebytes_consumed: How many input bytes were consumedpacket: Packet data (only valid if result == OGG_OK)
void reset();Reset demuxer state. Does not deallocate buffers.
The demuxer allocates buffers on first call to getNextPacket():
| Buffer | Size | Description |
|---|---|---|
page_header_staging |
282 bytes | Header accumulation and segment table |
internal_buffer |
min -> max | Packet assembly, grows as needed |
Typical memory usage: 1-4 KB per demuxer instance for most audio streams.
Zero-copy optimization: Packets are returned as zero-copy pointers when they fit entirely within the input buffer. Internal buffering only occurs when packets span page or input buffer boundaries.
CRC validation is disabled by default due to a fundamental limitation of the zero-copy architecture. Ogg pages contain a CRC32 checksum that covers the entire page (header + body). However, the demuxer returns packets as soon as they are found in the input buffer to enable zero-copy optimization. Since CRC validation requires the complete page, validation can only occur after the entire page body has been read.
This means:
- Packets 1 through N-1 on a page are returned with
OGG_OKbefore CRC validation - CRC validation occurs when the final packet (N) on the page is ready
- If CRC fails, the final packet returns
OGG_CRC_FAILEDinstead ofOGG_OK - The caller may have already processed the earlier (potentially corrupted) packets
For most use cases, leave CRC disabled (the default). Consider enabling CRC only if:
- You need to detect data corruption and can handle discarding previously returned packets
- You're implementing a validation tool rather than real-time playback
- Your application can buffer and defer processing until page boundaries
If you enable CRC and want strict validation before processing packets, you must manually buffer packet data and track page boundaries:
- Copy packet data - The
packet.datapointer is only valid until the nextgetNextPacket()call, so you must copy the data to your own buffer - Track page boundaries - Use
packet.is_last_on_pageto know when a page ends - Check for CRC errors - CRC validation occurs when the last packet on a page is processed. If validation fails,
OGG_CRC_FAILEDis returned instead ofOGG_OKfor that final packet - Discard on failure - If CRC fails, discard all buffered packets from that page (the earlier packets that were returned successfully)
std::vector<std::vector<uint8_t>> page_packets;
while (have_data) {
auto state = demuxer.getNextPacket(input, len);
if (state.result == OGG_OK) {
// Copy packet data (pointer only valid until next call)
page_packets.emplace_back(
state.packet.data,
state.packet.data + state.packet.length
);
if (state.packet.is_last_on_page) {
// Page complete and CRC valid - process all packets from this page
for (auto& pkt : page_packets) {
processPacket(pkt.data(), pkt.size());
}
page_packets.clear();
}
} else if (state.result == OGG_CRC_FAILED) {
// CRC failed on last packet - discard ALL packets from this page
page_packets.clear();
// Demuxer automatically moves to next page
}
input += state.bytes_consumed;
len -= state.bytes_consumed;
}Each OggDemuxer instance must be used from a single thread only. The demuxer maintains internal state that is not thread-safe.
For concurrent demuxing of multiple streams, create a separate OggDemuxer instance per thread:
// Thread 1
micro_ogg::OggDemuxer demuxer1;
// ... use demuxer1
// Thread 2
micro_ogg::OggDemuxer demuxer2;
// ... use demuxer2microOggDemuxer is designed with these principles:
- Platform-agnostic: Works on embedded systems, desktop, and anywhere C++11 runs
- Zero dependencies: No libogg, no platform-specific code
- Memory efficient: Lazy allocation, dynamic growth, zero-copy when possible
- Flexible: Custom allocators, configurable buffer sizes, optional CRC
- RFC 3533 based: Single logical bitstream Ogg page demuxing. For multiplexed files (grouped streams), callers must filter pages by serial number externally.
Buffer growth strategy:
- Starts at
min_buffer_size(default 1KB) - Doubles in size when more space needed
- Caps at
max_buffer_size(default 8KB) - Packets exceeding max size are skipped (not an error)
For typical audio streams:
- Initial allocation: 1 KB
- Typical usage: 1-2 KB (covers 99% of packets)
- Maximum allocation: 8 KB (configurable)
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Contributions welcome! Please ensure:
- No platform-specific code
- No external dependencies
- Maintain zero-copy design
- Follow existing code style
- Add tests for new features