Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ target_sources(av2_obu
src/core/obu_parser.cpp
src/core/base_obu.cpp
src/core/bitstream_reader.cpp
src/core/bitstream_writer.cpp
src/core/av2_sequence_header.cpp
src/core/av2_types.cpp
src/core/temporal_unit.cpp
Expand Down Expand Up @@ -91,6 +92,7 @@ target_sources(av2_obu
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/av2_obu/core/obu_parser.h>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/av2_obu/core/base_obu.h>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/av2_obu/core/bitstream_reader.h>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/av2_obu/core/bitstream_writer.h>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/av2_obu/core/av2_sequence_header.h>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/av2_obu/core/av2_types.h>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/av2_obu/core/temporal_unit.h>
Expand Down Expand Up @@ -160,10 +162,15 @@ set_target_properties(av2_obu PROPERTIES POSITION_INDEPENDENT_CODE ON)
add_subdirectory(apps/av2_obu_tool)
add_subdirectory(apps/av2_obu_switcher)

# av2_obu_packager - Optional (requires libisomedia)
option(BUILD_PACKAGER "Build av2_obu_packager with libisomedia support" OFF)
if(BUILD_PACKAGER)
message(STATUS "Building av2_obu_packager with libisomedia")
# Container tools (av2_mux + av2_demux) - Optional (require libisomedia).
# Legacy alias BUILD_PACKAGER is honoured to keep old build invocations working.
option(BUILD_CONTAINER_TOOLS "Build av2_mux / av2_demux with libisomedia support" OFF)
if(BUILD_PACKAGER AND NOT BUILD_CONTAINER_TOOLS)
message(STATUS "BUILD_PACKAGER is deprecated; treating as BUILD_CONTAINER_TOOLS=ON")
set(BUILD_CONTAINER_TOOLS ON)
endif()
if(BUILD_CONTAINER_TOOLS)
message(STATUS "Building container tools (av2_mux / av2_demux) with libisomedia")

# Fetch libisomedia from MPEG Group repo (pcm_sniffer branch)
FetchContent_Declare(
Expand All @@ -181,9 +188,10 @@ if(BUILD_PACKAGER)
# Make libisomedia available
FetchContent_MakeAvailable(libisomedia)

add_subdirectory(apps/av2_obu_packager)
add_subdirectory(apps/av2_mux)
add_subdirectory(apps/av2_demux)
else()
message(STATUS "Skipping av2_obu_packager (use -DBUILD_PACKAGER=ON to enable)")
message(STATUS "Skipping container tools (use -DBUILD_CONTAINER_TOOLS=ON to enable)")
endif()

add_subdirectory(apps/av2_obu_channel_sim)
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Tools developed within the AOMedia Storage and Transport Formats (STF) Working G

```bash
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Debug -DBUILD_PACKAGER=ON -DBUILD_EXAMPLES=ON
cmake .. -DCMAKE_BUILD_TYPE=Debug -DBUILD_CONTAINER_TOOLS=ON -DBUILD_EXAMPLES=ON
make -j
```

Expand All @@ -20,7 +20,8 @@ The project builds several applications that use the `libav2_obu.a`:

- [**av2_obu_tool**](./apps/av2_obu_tool/) - Parse, dump, and analyze AV2 bitstreams (JSON export, statistics)
- [**av2_obu_switcher**](./apps/av2_obu_switcher/) - Bitstream switching experiments
- [**av2_obu_packager**](./apps/av2_obu_packager/) - Package AV2 into MP4 containers (requires `-DBUILD_PACKAGER=ON`)
- [**av2_mux**](./apps/av2_mux/) - Mux AV2 elementary stream into MP4/ISOBMFF (requires `-DBUILD_CONTAINER_TOOLS=ON`)
- [**av2_demux**](./apps/av2_demux/) - Demux MP4/ISOBMFF back to an AV2 elementary stream (requires `-DBUILD_CONTAINER_TOOLS=ON`)
- [**av2_obu_channel_sim**](./apps/av2_obu_channel_sim/) - Simulate packet loss and network conditions

## Requirements
Expand Down
18 changes: 18 additions & 0 deletions apps/av2_demux/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
add_executable(av2_demux
main.cpp
av2_demuxer.cpp
)

target_link_libraries(av2_demux
PRIVATE
av2_obu
CLI11::CLI11
libisomediafile
)

target_compile_options(av2_demux PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Wpedantic>
$<$<CXX_COMPILER_ID:MSVC>:/W4>
)

install(TARGETS av2_demux RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
28 changes: 28 additions & 0 deletions apps/av2_demux/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# av2_demux

Demux an MP4/ISOBMFF file containing an AV2 video track back into an AV2 elementary bitstream (`.obu`, Annex B framed).

## Build

Same flag as `av2_mux`:

```bash
cmake -S . -B mybuild -DBUILD_CONTAINER_TOOLS=ON
cmake --build mybuild -j
```

## Usage

```bash
./av2_demux input.mp4 -o output.obu
./av2_demux input.mp4 -o output.obu -v # show every meaningful demuxing step
```

## What it does

1. Opens the `.mp4` and finds the first track whose sample entry is `'av02'`.
2. Pulls the `av2C` configuration box from that sample entry and extracts its `configOBUs[]` (everything after the 9-byte prefix).
3. Writes those configOBUs at the start of the output — they are already Annex B framed, so they go in verbatim.
4. For every sample in decode order, writes a Temporal Delimiter (`{0x01, 0x08}`) followed by the raw sample bytes.

The result is a parseable AV2 elementary bitstream that round-trips through `av2_obu_tool`.
247 changes: 247 additions & 0 deletions apps/av2_demux/av2_demuxer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
/*
* Copyright (c) 2025, Alliance for Open Media. All rights reserved
*
* This source code is subject to the terms of the BSD 3-Clause Clear License and the Alliance for
* Open Media Patent License 1.0. If the BSD 3-Clause Clear License was not distributed with this
* source code in the LICENSE file, you can obtain it at
* aomedia.org/license/software-license/bsd-3-c-c/. If the Alliance for Open Media Patent
* License 1.0 was not distributed with this source code in the PATENTS file, you can obtain it at
* aomedia.org/license/patent-license/.
*/

#include "av2_demuxer.h"

#include <cstring>
#include <fstream>
#include <vector>

#include <spdlog/spdlog.h>

#include <ISOMovies.h>
#include <MP4Movies.h>

namespace av2_obu {

namespace {

constexpr uint32_t kSampleEntryTypeAv02 = MP4_FOUR_CHAR_CODE('a', 'v', '0', '2');
constexpr uint32_t kAtomTypeAv2C = MP4_FOUR_CHAR_CODE('a', 'v', '2', 'C');

// av2C box payload layout (per av2-isobmff working draft): a 9-byte prefix followed by configOBUs[]
constexpr size_t kAv2CPrefixSize = 9;

// Annex B-framed Temporal Delimiter
constexpr uint8_t kTdBytes[2] = {0x01, 0x08};

// If the sample bytes start with an Annex-B-framed TD OBU, return its length in bytes (otherwise 0)
size_t leading_td_length(const uint8_t* p, size_t n) {
if (n < 2) return 0;
// Decode LEB128 OBU size (max 8 bytes per AV2 Annex B).
size_t i = 0;
uint64_t obu_size = 0;
uint32_t shift = 0;
while (i < n && i < 8) {
uint8_t b = p[i++];
obu_size |= static_cast<uint64_t>(b & 0x7F) << shift;
if (!(b & 0x80)) break;
shift += 7;
}
if (obu_size == 0 || i >= n || i + obu_size > n) return 0;
// p[i] is the OBU header byte: ext_flag(1) | obu_type(5) | tlayer(2).
uint32_t obu_type = (p[i] >> 2) & 0x1F;
if (obu_type != 2) return 0; // not a TEMPORAL_DELIMITER
return i + static_cast<size_t>(obu_size);
}

// Find the first track whose sample entry type is 'av02'.
uint32_t find_av02_track(MP4Movie movie, uint32_t* out_track_count) {
uint32_t count = 0;
if (MP4GetMovieTrackCount(movie, &count) != MP4NoErr) return 0;
if (out_track_count) *out_track_count = count;
for (uint32_t i = 1; i <= count; ++i) {
uint32_t entry_type = 0;
if (MP4GetMovieIndTrackSampleEntryType(movie, i, &entry_type) != MP4NoErr) continue;
if (entry_type == kSampleEntryTypeAv02) return i;
}
return 0;
}

// Read the av2C atom payload from the active sample entry of the media.
// Parse VisualSampleEntry bytes skip sample entry header (78 byets) then walk the children and find av2C
constexpr size_t kVisualSampleEntryFixedSize = 78;

uint32_t be32(const uint8_t* p) {
return (static_cast<uint32_t>(p[0]) << 24) | (static_cast<uint32_t>(p[1]) << 16) |
(static_cast<uint32_t>(p[2]) << 8) | static_cast<uint32_t>(p[3]);
}

bool read_av2c_payload(MP4Media media, std::vector<uint8_t>& out_bytes) {
MP4Handle entry_h = nullptr;
if (MP4NewHandle(0, &entry_h) != MP4NoErr || !entry_h) {
spdlog::error("MP4NewHandle(sampleEntry) failed");
return false;
}
uint32_t desc_index = 1;
if (MP4GetMediaSampleDescription(media, desc_index, entry_h, /*outIdx=*/nullptr) != MP4NoErr) {
spdlog::error("MP4GetMediaSampleDescription({}) failed", desc_index);
MP4DisposeHandle(entry_h);
return false;
}
uint32_t entry_size = 0;
MP4GetHandleSize(entry_h, &entry_size);
const uint8_t* p = reinterpret_cast<const uint8_t*>(*entry_h);

// Outer box header: u32 size, u32 type.
if (entry_size < 8 + kVisualSampleEntryFixedSize) {
spdlog::error("Sample entry too short ({} bytes) to contain a VisualSampleEntry", entry_size);
MP4DisposeHandle(entry_h);
return false;
}
size_t cursor = 8 + kVisualSampleEntryFixedSize;

// Walk child boxes: u32 size, u32 type, payload.
while (cursor + 8 <= entry_size) {
uint32_t child_size = be32(p + cursor);
uint32_t child_type = be32(p + cursor + 4);
if (child_size < 8 || cursor + child_size > entry_size) {
spdlog::error("Malformed child box at offset {} (size={})", cursor, child_size);
MP4DisposeHandle(entry_h);
return false;
}
if (child_type == kAtomTypeAv2C) {
out_bytes.assign(p + cursor + 8, p + cursor + child_size);
MP4DisposeHandle(entry_h);
return true;
}
cursor += child_size;
}

spdlog::error("av2C box not found among VisualSampleEntry children");
MP4DisposeHandle(entry_h);
return false;
}

} // namespace

bool Av2Demuxer::demux(const std::string& input_mp4, const std::string& output_obu) {
spdlog::debug("Demuxing {} -> {}", input_mp4, output_obu);

MP4Movie movie = nullptr;
if (MP4OpenMovieFile(&movie, input_mp4.c_str(), MP4OpenMovieNormal) != MP4NoErr || !movie) {
spdlog::error("Failed to open {}", input_mp4);
return false;
}

uint32_t track_count = 0;
uint32_t track_idx = find_av02_track(movie, &track_count);
if (track_idx == 0) {
spdlog::error("No 'av02' video track found in {} ({} track(s) total)", input_mp4, track_count);
MP4DisposeMovie(movie);
return false;
}
spdlog::debug("Selected track {} of {} (sample entry type 'av02')", track_idx, track_count);

MP4Track track = nullptr;
if (MP4GetMovieIndTrack(movie, track_idx, &track) != MP4NoErr || !track) {
spdlog::error("MP4GetMovieIndTrack({}) failed", track_idx);
MP4DisposeMovie(movie);
return false;
}
MP4Media media = nullptr;
if (MP4GetTrackMedia(track, &media) != MP4NoErr || !media) {
spdlog::error("MP4GetTrackMedia failed");
MP4DisposeMovie(movie);
return false;
}

std::vector<uint8_t> av2c_bytes;
if (!read_av2c_payload(media, av2c_bytes)) {
MP4DisposeMovie(movie);
return false;
}
if (av2c_bytes.size() < kAv2CPrefixSize) {
spdlog::error("av2C payload too short ({} bytes, need >= {})", av2c_bytes.size(),
kAv2CPrefixSize);
MP4DisposeMovie(movie);
return false;
}
const uint8_t* config_obus = av2c_bytes.data() + kAv2CPrefixSize;
size_t config_obus_size = av2c_bytes.size() - kAv2CPrefixSize;
spdlog::debug("av2C: {} payload bytes ({} prefix + {} configOBUs)", av2c_bytes.size(),
kAv2CPrefixSize, config_obus_size);

uint32_t sample_count = 0;
if (MP4GetMediaSampleCount(media, &sample_count) != MP4NoErr) {
spdlog::error("MP4GetMediaSampleCount failed");
MP4DisposeMovie(movie);
return false;
}
spdlog::debug("Track has {} samples", sample_count);

std::ofstream out(output_obu, std::ios::binary);
if (!out) {
spdlog::error("Failed to open output {} for writing", output_obu);
MP4DisposeMovie(movie);
return false;
}

// For every sample: emit a TD first, then (only for sample 1) the configOBUs, then the sample bytes.
MP4Handle sample_h = nullptr;
if (MP4NewHandle(0, &sample_h) != MP4NoErr || !sample_h) {
spdlog::error("MP4NewHandle(sample) failed");
MP4DisposeMovie(movie);
return false;
}

uint64_t total_sample_bytes = 0;
uint32_t samples_with_td = 0;
for (uint32_t i = 1; i <= sample_count; ++i) {
u32 size = 0;
u64 dts = 0, dur = 0;
s32 cts_off = 0;
u32 flags = 0, desc_idx = 0;
if (MP4GetIndMediaSample(media, i, sample_h, &size, &dts, &cts_off, &dur, &flags,
&desc_idx) != MP4NoErr) {
spdlog::error("MP4GetIndMediaSample({}) failed", i);
MP4DisposeHandle(sample_h);
MP4DisposeMovie(movie);
return false;
}
const uint8_t* sample_data = reinterpret_cast<const uint8_t*>(*sample_h);
size_t td_prefix = leading_td_length(sample_data, size);
if (td_prefix > 0) {
// Sample already carries its own TD. Emit the TD that came from the bitstream.
// For sample 1 splice configOBUs in between the TD and the frame data.
out.write(reinterpret_cast<const char*>(sample_data),
static_cast<std::streamsize>(td_prefix));
if (i == 1) {
out.write(reinterpret_cast<const char*>(config_obus),
static_cast<std::streamsize>(config_obus_size));
}
out.write(reinterpret_cast<const char*>(sample_data + td_prefix),
static_cast<std::streamsize>(size - td_prefix));
++samples_with_td;
} else {
// Sample has no TD (the default --drop-td muxing). Synthesize one.
out.write(reinterpret_cast<const char*>(kTdBytes), sizeof(kTdBytes));
if (i == 1) {
out.write(reinterpret_cast<const char*>(config_obus),
static_cast<std::streamsize>(config_obus_size));
}
out.write(reinterpret_cast<const char*>(sample_data),
static_cast<std::streamsize>(size));
}
total_sample_bytes += size;
}

MP4DisposeHandle(sample_h);
MP4DisposeMovie(movie);
out.close();

spdlog::info("Wrote {} ({} samples [{} carried a TD, {} got a synthesized TD], {} configOBU bytes)",
output_obu, sample_count, samples_with_td, sample_count - samples_with_td,
config_obus_size);
return true;
}

} // namespace av2_obu
26 changes: 26 additions & 0 deletions apps/av2_demux/av2_demuxer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2025, Alliance for Open Media. All rights reserved
*
* This source code is subject to the terms of the BSD 3-Clause Clear License and the Alliance for
* Open Media Patent License 1.0. If the BSD 3-Clause Clear License was not distributed with this
* source code in the LICENSE file, you can obtain it at
* aomedia.org/license/software-license/bsd-3-c-c/. If the Alliance for Open Media Patent
* License 1.0 was not distributed with this source code in the PATENTS file, you can obtain it at
* aomedia.org/license/patent-license/.
*/

#pragma once

#include <cstdint>
#include <string>

namespace av2_obu {

// Av2Demuxer reads an MP4/ISOBMFF file containing an AV2 video track and reconstructs the AV2 elementary bitstream:
// TDs are stripped on the way in by av2_mux, so we re-prepend TD at every sample boundary
class Av2Demuxer {
public:
bool demux(const std::string& input_mp4, const std::string& output_obu);
};

} // namespace av2_obu
Loading
Loading