From 8c3a72dd00b47ca5138c77b78dff6ab7b1025016 Mon Sep 17 00:00:00 2001 From: Dimitri Podborski Date: Thu, 18 Jun 2026 15:59:00 -0700 Subject: [PATCH 01/10] frame_header_info: fix bridge/SEF parser bugs --- include/av2_obu/core/frame_header_info.h | 23 ++++ src/core/frame_header_info.cpp | 149 +++++++++++++++++++---- 2 files changed, 146 insertions(+), 26 deletions(-) diff --git a/include/av2_obu/core/frame_header_info.h b/include/av2_obu/core/frame_header_info.h index 7662f00..6f11902 100644 --- a/include/av2_obu/core/frame_header_info.h +++ b/include/av2_obu/core/frame_header_info.h @@ -48,15 +48,29 @@ struct FrameHeaderInfo { int32_t LongTermId = -1; uint32_t long_term_id_plus_1 = 0; + // Bridge frame + bool IsBridge = false; + uint32_t bridge_frame_ref_idx = 0; + uint32_t bridge_frame_overwrite_flag = 0; + uint32_t frame_size_override_flag = 0; // Display order + // OrderHintLsbs from the bitstream (per AV2 syntax `f(OrderHintBits) order_hint`). + // This is NOT the lifted full DispOrderHint produced by get_disp_order_hint(); + // callers that need the lifted value compute it from this field plus their own + // wraparound state (the lightweight parser does not track reference state). uint32_t order_hint = 0; // Output flags uint32_t immediate_output_frame = 0; uint32_t implicit_output_frame = 0; + // True iff this frame produces an output picture in the temporal unit. + // Derived from immediate_output_frame || implicit_output_frame || ShowExistingFrame. + // Used by the packager to identify the output frame within a TU for CTS / ctts derivation. + bool is_output_frame = false; + // Primary reference uint32_t primary_ref_frame = PRIMARY_REF_NONE; uint32_t signal_primary_ref_frame = 0; @@ -82,6 +96,15 @@ struct FrameHeaderInfo { uint32_t bru_ref = 0; uint32_t bru_inactive = 0; + // Inter ref-frame MV signalling + uint32_t use_ref_frame_mvs = 0; + uint32_t tmvp_sample_step_minus_1 = 0; + + // Film grain (per-frame; references SH film_grain_params_present) + uint32_t apply_grain = 0; + uint32_t fgm_id = 0; + uint32_t grain_seed = 0; + // TIP uint32_t TipFrameMode = TIP_FRAME_DISABLED; diff --git a/src/core/frame_header_info.cpp b/src/core/frame_header_info.cpp index a94f8c2..36ebd78 100644 --- a/src/core/frame_header_info.cpp +++ b/src/core/frame_header_info.cpp @@ -17,11 +17,31 @@ namespace av2_obu { +// film_grain_config() per AV2 spec §film_grain_config_syntax (05_Syntax_structures.bs:4159). +// Reads at most 1 + 19 bits depending on conditions; updates fhi.{apply_grain, fgm_id, grain_seed}. +static void parse_film_grain_config(BitstreamReader& br, const AV2SequenceHeader& sh, + FrameHeaderInfo& fhi) { + if (!sh.film_grain_params_present || + (!fhi.immediate_output_frame && !fhi.implicit_output_frame)) { + fhi.apply_grain = 0; + } else if (sh.single_picture_header_flag) { + fhi.apply_grain = 1; + } else { + fhi.apply_grain = br.read_bit(); + } + if (fhi.apply_grain) { + fhi.fgm_id = br.read_bits(3); + // load_grain_model() is decoder state — not parsed. + fhi.grain_seed = br.read_bits(16); + } +} + bool FrameHeaderInfo::parse_lightweight(BitstreamReader& br, OBUType obu_type, const AV2SequenceHeader& sh) { - // Derive frame classification from OBU type (spec helper functions) - (void)is_key_frame_obu(obu_type); // TODO: use keyFrame when deep parsing is complete - bool IsBridge = (obu_type == OBUType::BRIDGE_FRAME); + // Frame classification flags. The spec helper `keyFrame` (CLK || OLK) is only used + // for state we don't track in lightweight (LCR activation, RefValid reset, OlkEncountered), + // so we don't bind it. IsBridge is needed locally and exposed in the struct. + IsBridge = (obu_type == OBUType::BRIDGE_FRAME); // --- cur_mfh_id --- if (IsBridge) { @@ -35,12 +55,10 @@ bool FrameHeaderInfo::parse_lightweight(BitstreamReader& br, OBUType obu_type, seq_header_id_in_frame_header = br.read_uvlc(); } - // --- Bridge frame ref idx (skip for now) --- + // --- Bridge frame ref idx (spec line 60-63) --- if (IsBridge) { uint32_t n = CeilLog2(sh.inter_config.NumRefFrames); - if (n > 0) { - br.read_bits(n); // bridge_frame_ref_idx - } + bridge_frame_ref_idx = (n > 0) ? br.read_bits(n) : 0; } uint32_t allFrames = (1 << sh.inter_config.NumRefFrames) - 1; @@ -69,10 +87,16 @@ bool FrameHeaderInfo::parse_lightweight(BitstreamReader& br, OBUType obu_type, // For SEF: refresh_frame_flags = 0, FrameType derived from ref refresh_frame_flags = 0; immediate_output_frame = 1; + // Spec §frame_header_info: film_grain_config() is called here, before the early + // return. Must consume its bits to keep the bitstream pointer correct for any + // subsequent (deep) parsing of this OBU. + parse_film_grain_config(br, sh, *this); TipFrameMode = TIP_FRAME_DISABLED; // Frame dimensions come from the referenced frame (not signaled here) FrameWidth = static_cast(sh.max_frame_width_minus_1) + 1; FrameHeight = static_cast(sh.max_frame_height_minus_1) + 1; + // SEF always produces an output picture (the previously decoded frame is shown). + is_output_frame = true; parsed = true; return true; } @@ -165,6 +189,11 @@ bool FrameHeaderInfo::parse_lightweight(BitstreamReader& br, OBUType obu_type, } } + // --- bridge_frame_overwrite_flag (spec line 207, BEFORE refresh_frame_flags) --- + if (IsBridge) { + bridge_frame_overwrite_flag = br.read_bit(); + } + // --- refresh_frame_flags --- if (FrameType == KEY_FRAME) { if (obu_type == OBUType::CLK && sh.max_mlayer_id == 0) { @@ -176,10 +205,9 @@ bool FrameHeaderInfo::parse_lightweight(BitstreamReader& br, OBUType obu_type, } else { refresh_frame_flags = br.read_bits(sh.inter_config.NumRefFrames); } - } else if (IsBridge) { - // Bridge: refresh_frame_flags depends on bridge_frame_overwrite_flag - // For lightweight, skip — not critical for basic packaging - refresh_frame_flags = 0; + } else if (IsBridge && !bridge_frame_overwrite_flag) { + // Spec line 237-238: bridge with !overwrite refreshes only the slot it replaces. + refresh_frame_flags = 1u << bridge_frame_ref_idx; } else if (obu_type == OBUType::RAS_FRAME && sh.max_mlayer_id == 0) { // RAS with single layer: refresh non-long-term refs // For lightweight, approximate as 0 (full logic needs ref state) @@ -188,6 +216,7 @@ bool FrameHeaderInfo::parse_lightweight(BitstreamReader& br, OBUType obu_type, refresh_frame_flags = br.read_bits(sh.inter_config.NumRefFrames); } else if (sh.inter_config.enable_short_refresh_frame_flags && FrameType != SWITCH_FRAME && FrameType != KEY_FRAME) { + // Bridge with overwrite==1 falls through here when SH enables short flags. has_refresh_frame_flags = br.read_bit(); if (has_refresh_frame_flags) { uint32_t n = CeilLog2(sh.inter_config.NumRefFrames); @@ -243,16 +272,20 @@ bool FrameHeaderInfo::parse_lightweight(BitstreamReader& br, OBUType obu_type, ref_frame_idx.resize(NumTotalRefs); for (uint32_t i = 0; i < NumTotalRefs; i++) { if (IsBridge) { - ref_frame_idx[i] = 0; // bridge_frame_ref_idx (already parsed above) + // Spec line 297-298: ref_frame_idx[i] = bridge_frame_ref_idx (already parsed above). + ref_frame_idx[i] = bridge_frame_ref_idx; } else if (explicitRefFrameMap) { uint32_t n = CeilLog2(sh.inter_config.NumRefFrames); ref_frame_idx[i] = (n > 0) ? br.read_bits(n) : 0; } } - // --- frame_size / frame_size_with_refs --- + // --- frame_size / frame_size_with_refs / frame_size_with_bridge --- if (IsBridge) { - // frame_size_with_bridge: read bridge dims + // frame_size_with_bridge: read bridge_frame_{width,height}_minus_1. + // Spec also clamps via Min(RefFrame{Width,Height}[bridge_frame_ref_idx], ...); + // we lack ref state in lightweight mode, so we report the bitstream value + 1 + // (an upper bound on the actual FrameWidth/FrameHeight). uint32_t w_bits = sh.frame_width_bits_minus_1 + 1; uint32_t h_bits = sh.frame_height_bits_minus_1 + 1; FrameWidth = br.read_bits(w_bits) + 1; @@ -305,16 +338,43 @@ bool FrameHeaderInfo::parse_lightweight(BitstreamReader& br, OBUType obu_type, } } + // --- use_ref_frame_mvs (spec line 331-336) --- + if (FrameType == SWITCH_FRAME || !sh.inter_config.enable_ref_frame_mvs || IsBridge || + bru_inactive) { + use_ref_frame_mvs = 0; + } else { + use_ref_frame_mvs = br.read_bit(); + } + + // --- tmvp_sample_step_minus_1 (spec line 337-342) --- + // Per-frame SbSize: 256x256 promotes to BLOCK_256X256 only for non-intra frames. + BlockSize SbSize; + if (sh.partition_config.use_256x256_superblock) { + SbSize = FrameIsIntra ? BLOCK_128X128 : BLOCK_256X256; + } else if (sh.partition_config.use_128x128_superblock) { + SbSize = BLOCK_128X128; + } else { + SbSize = BLOCK_64X64; + } + if (use_ref_frame_mvs && NumTotalRefs > 1 && SbSize != BLOCK_64X64) { + tmvp_sample_step_minus_1 = br.read_bit(); + } + // LIGHTWEIGHT STOP for inter frames - // (skip: use_ref_frame_mvs, TIP details, screen_content_params, - // intrabc_params, MV precision, interpolation filter, motion modes, etc.) + // (skip: enable_tip block, screen_content_params, intrabc_params, MV precision, + // interpolation filter, motion modes, etc.) + // TipFrameMode here is left at the default (TIP_FRAME_DISABLED). The spec assigns + // TipFrameMode either to TIP_FRAME_AS_OUTPUT (when EnableTipOutput && is_tip_frame()) + // or from `f(1) tip_frame_mode` — both are inside the enable_tip block we skip. TipFrameMode = TIP_FRAME_DISABLED; - // Note: TipFrameMode would need deeper parsing of enable_tip conditions - // and use_ref_frame_mvs. For lightweight packaging, we know TIP OBUs - // set TIP_FRAME_AS_OUTPUT from the OBU type (handled by TIP OBU class). } parsed = true; + // is_output_frame: derived from the per-frame output flags. SEF returns earlier with + // is_output_frame = true. For other paths, single_picture_header_flag forces + // immediate_output_frame = 1, otherwise immediate_output_frame and implicit_output_frame + // are read from the bitstream above. + is_output_frame = (immediate_output_frame != 0) || (implicit_output_frame != 0); return true; } @@ -341,11 +401,13 @@ json FrameHeaderInfo::to_json() const { j["seq_header_id_in_frame_header"] = seq_header_id_in_frame_header; } - // Frame type + // Frame type: emit only when reliably known. For SEF the spec sets FrameType from + // RefFrameType[frame_to_show_map_idx] (decoder reference state we don't track), so + // skip emission to avoid reporting the constructor default. const char* frame_type_names[] = {"KEY_FRAME", "INTER_FRAME", "INTRA_ONLY_FRAME", "SWITCH_FRAME"}; - j["FrameType"] = (FrameType <= 3) ? frame_type_names[FrameType] : "UNKNOWN"; - j["FrameIsIntra"] = FrameIsIntra; - j["ShowExistingFrame"] = ShowExistingFrame; + if (!ShowExistingFrame) { + j["FrameType"] = (FrameType <= 3) ? frame_type_names[FrameType] : "UNKNOWN"; + } if (ShowExistingFrame) { j["frame_to_show_map_idx"] = frame_to_show_map_idx; @@ -353,6 +415,11 @@ json FrameHeaderInfo::to_json() const { if (derive_sef_order_hint == 0) { j["sef_order_hint"] = sef_order_hint; } + if (apply_grain) { + j["apply_grain"] = apply_grain; + j["fgm_id"] = fgm_id; + j["grain_seed"] = grain_seed; + } } if (!ShowExistingFrame) { @@ -362,13 +429,17 @@ json FrameHeaderInfo::to_json() const { if (FrameType == KEY_FRAME && LongTermId >= 0) { j["long_term_id_plus_1"] = long_term_id_plus_1; - j["LongTermId"] = LongTermId; } j["immediate_output_frame"] = immediate_output_frame; j["implicit_output_frame"] = implicit_output_frame; j["frame_size_override_flag"] = frame_size_override_flag; - j["order_hint"] = order_hint; + // For bridge frames, order_hint comes from RefOrderHintLsbs[bridge_frame_ref_idx] + // (decoder ref state) — not from the bitstream — so we skip emission rather than + // report the placeholder 0. + if (!IsBridge) { + j["order_hint"] = order_hint; + } j["primary_ref_frame"] = primary_ref_frame; j["refresh_frame_flags"] = refresh_frame_flags; j["FrameWidth"] = FrameWidth; @@ -384,10 +455,36 @@ json FrameHeaderInfo::to_json() const { j["bru_ref"] = bru_ref; j["bru_inactive"] = bru_inactive; } + j["use_ref_frame_mvs"] = use_ref_frame_mvs; + if (use_ref_frame_mvs) { + j["tmvp_sample_step_minus_1"] = tmvp_sample_step_minus_1; + } + } + + if (IsBridge) { + j["bridge_frame_ref_idx"] = bridge_frame_ref_idx; + j["bridge_frame_overwrite_flag"] = bridge_frame_overwrite_flag; } - j["TipFrameMode"] = TipFrameMode; + if (apply_grain) { + j["apply_grain"] = apply_grain; + j["fgm_id"] = fgm_id; + j["grain_seed"] = grain_seed; + } + } + + // Computed values not directly serialized in the bitstream (no f(N) descriptor). + // Placed under a "derived" sub-object to keep them visually distinct from + // bitstream-read fields above. + json derived = json::object(); + derived["FrameIsIntra"] = FrameIsIntra; + derived["ShowExistingFrame"] = ShowExistingFrame; + derived["is_output_frame"] = is_output_frame; + derived["TipFrameMode"] = TipFrameMode; + if (FrameType == KEY_FRAME && LongTermId >= 0) { + derived["LongTermId"] = LongTermId; } + j["derived"] = derived; return j; } From 80832a70ea616e68e5deffa5f937de8b241de18d Mon Sep 17 00:00:00 2001 From: Dimitri Podborski Date: Thu, 18 Jun 2026 16:08:01 -0700 Subject: [PATCH 02/10] simplify timing info fetching from CI OBU --- include/av2_obu/core/obu_parser.h | 9 +++++++++ .../av2_obu/obus/content_interpretation_obu.h | 1 + src/core/obu_parser.cpp | 16 ++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/include/av2_obu/core/obu_parser.h b/include/av2_obu/core/obu_parser.h index 0c2badb..4c1585e 100644 --- a/include/av2_obu/core/obu_parser.h +++ b/include/av2_obu/core/obu_parser.h @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -113,6 +114,14 @@ class OBUParser { bool has_layer_config = false; bool has_operating_point_set = false; } config; + + // Content Interpretation OBU info (first CI OBU encountered in the stream). + // For multistream, future work may need per-xlayer breakdown. + struct ContentInterpretationInfo { + bool present = false; + bool has_timing_info = false; + TimingInfo timing_info; // valid iff has_timing_info + } content_interpretation; }; Statistics get_statistics() const; diff --git a/include/av2_obu/obus/content_interpretation_obu.h b/include/av2_obu/obus/content_interpretation_obu.h index 643b705..ea309ed 100644 --- a/include/av2_obu/obus/content_interpretation_obu.h +++ b/include/av2_obu/obus/content_interpretation_obu.h @@ -37,6 +37,7 @@ class ContentInterpretationOBU : public BaseOBU { bool has_chroma_sample_position() const { return chroma_sample_position_present_flag_ != 0; } bool has_aspect_ratio_info() const { return aspect_ratio_info_present_flag_ != 0; } bool has_timing_info() const { return timing_info_present_flag_ != 0; } + const TimingInfo& get_timing_info() const { return timing_info_; } protected: bool parse_payload(std::ifstream& ifs) override; diff --git a/src/core/obu_parser.cpp b/src/core/obu_parser.cpp index 09cee13..acab2f5 100644 --- a/src/core/obu_parser.cpp +++ b/src/core/obu_parser.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -231,6 +232,21 @@ OBUParser::Statistics OBUParser::get_statistics() const { stats.config.has_operating_point_set = true; break; + case OBUType::CONTENT_INTERPRETATION: { + // Capture the first CI OBU encountered. Multistream may have multiple CI + // OBUs (one per xlayer); per-xlayer surfacing is future work. + if (!stats.content_interpretation.present) { + stats.content_interpretation.present = true; + if (auto* ci = dynamic_cast(obu.get())) { + if (ci->has_timing_info()) { + stats.content_interpretation.has_timing_info = true; + stats.content_interpretation.timing_info = ci->get_timing_info(); + } + } + } + break; + } + default: break; } From bbafa9727c85a92d0faf9f1405bd9c7020d276c2 Mon Sep 17 00:00:00 2001 From: Dimitri Podborski Date: Thu, 18 Jun 2026 17:49:51 -0700 Subject: [PATCH 03/10] bitstream-faithful TUs, spec-conformant sync sample, parser fixes --- apps/av2_obu_packager/main.cpp | 8 +- .../temporal_unit_builder.cpp | 108 ------------------ apps/av2_obu_packager/temporal_unit_builder.h | 57 --------- include/av2_obu/core/obu_parser.h | 1 - include/av2_obu/core/temporal_unit.h | 13 ++- src/core/obu_parser.cpp | 15 +-- src/core/temporal_unit.cpp | 57 +++++++-- 7 files changed, 68 insertions(+), 191 deletions(-) delete mode 100644 apps/av2_obu_packager/temporal_unit_builder.cpp delete mode 100644 apps/av2_obu_packager/temporal_unit_builder.h diff --git a/apps/av2_obu_packager/main.cpp b/apps/av2_obu_packager/main.cpp index a6146d1..6d0b267 100644 --- a/apps/av2_obu_packager/main.cpp +++ b/apps/av2_obu_packager/main.cpp @@ -111,15 +111,15 @@ int main(int argc, char** argv) { return 1; } - size_t keyframe_count = 0; + size_t sync_sample_count = 0; for (const auto& tu : temporal_units) { - if (tu.is_keyframe()) { - keyframe_count++; + if (tu.is_sync_sample()) { + sync_sample_count++; } } spdlog::info(" {} temporal units (MP4 samples)", temporal_units.size()); - spdlog::info(" {} keyframe samples", keyframe_count); + spdlog::info(" {} sync samples", sync_sample_count); spdlog::info(""); // ======================================================================== diff --git a/apps/av2_obu_packager/temporal_unit_builder.cpp b/apps/av2_obu_packager/temporal_unit_builder.cpp deleted file mode 100644 index ebb1fbf..0000000 --- a/apps/av2_obu_packager/temporal_unit_builder.cpp +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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 "temporal_unit_builder.h" - -#include - -namespace av2_obu { - -bool TemporalUnit::is_keyframe() const { - for (const auto* obu : obus_) { - auto type = obu->type(); - if (type == OBUType::CLK || type == OBUType::OLK) { - return true; - } - } - return false; -} - -size_t TemporalUnit::get_total_size() const { - size_t total = 0; - for (const auto* obu : obus_) { - // Annex B format: size_field + header + payload - total += - obu->position().size_field_len + obu->position().header_len + obu->position().payload_size; - } - return total; -} - -std::vector TemporalUnitBuilder::build( - const std::vector>& obus, const PackagingStrategy& strategy) { - spdlog::debug("Building temporal units from {} OBUs", obus.size()); - return build_td_based(obus, strategy.drop_temporal_delimiters); -} - -std::vector TemporalUnitBuilder::build_td_based( - const std::vector>& obus, bool drop_tds) { - spdlog::debug("Building TUs using temporal delimiter boundaries"); - - std::vector temporal_units; - TemporalUnit current_tu; - - for (const auto& obu : obus) { - // Skip config OBUs (they go in sample entry, not samples) - if (obu->type() == OBUType::SEQUENCE_HEADER || - obu->type() == OBUType::LAYER_CONFIGURATION_RECORD || - obu->type() == OBUType::OPERATING_POINT_SET) { - continue; - } - - // Temporal delimiter marks start of new TU - if (obu->type() == OBUType::TEMPORAL_DELIMITER) { - // Finalize previous TU if not empty - if (!current_tu.empty()) { - spdlog::debug(" TU #{}: {} OBUs, {} bytes, keyframe: {}", temporal_units.size(), - current_tu.obu_count(), current_tu.get_total_size(), - current_tu.is_keyframe()); - temporal_units.push_back(std::move(current_tu)); - current_tu = TemporalUnit(); - } - - // Add TD to new TU (unless dropping) - if (!drop_tds) { - current_tu.add_obu(obu.get()); - } - } else { - // Add OBU to current TU - current_tu.add_obu(obu.get()); - } - } - - // Finalize last TU - if (!current_tu.empty()) { - spdlog::debug(" TU #{}: {} OBUs, {} bytes, keyframe: {}", temporal_units.size(), - current_tu.obu_count(), current_tu.get_total_size(), current_tu.is_keyframe()); - temporal_units.push_back(std::move(current_tu)); - } - - spdlog::info("Built {} temporal units (TD-based)", temporal_units.size()); - return temporal_units; -} - -bool TemporalUnitBuilder::should_include_in_sample(const BaseOBU* obu, - const PackagingStrategy& strategy) const { - // Never include config OBUs in samples - if (obu->type() == OBUType::SEQUENCE_HEADER || - obu->type() == OBUType::LAYER_CONFIGURATION_RECORD || - obu->type() == OBUType::OPERATING_POINT_SET) { - return false; - } - - // Drop TDs if requested - if (obu->type() == OBUType::TEMPORAL_DELIMITER && strategy.drop_temporal_delimiters) { - return false; - } - - return true; -} - -} // namespace av2_obu diff --git a/apps/av2_obu_packager/temporal_unit_builder.h b/apps/av2_obu_packager/temporal_unit_builder.h deleted file mode 100644 index d88e48d..0000000 --- a/apps/av2_obu_packager/temporal_unit_builder.h +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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 -#include - -#include "packaging_strategy.h" -#include - -namespace av2_obu { - -// Represents one temporal unit (one MP4 sample) -class TemporalUnit { -public: - void add_obu(const BaseOBU* obu) { obus_.push_back(obu); } - - const std::vector& obus() const { return obus_; } - size_t obu_count() const { return obus_.size(); } - bool empty() const { return obus_.empty(); } - - // Check if this TU contains a keyframe - bool is_keyframe() const; - - // Get total size of all OBUs in Annex B format - size_t get_total_size() const; - -private: - std::vector obus_; // Non-owning pointers -}; - -// Groups parsed OBUs into temporal units -class TemporalUnitBuilder { -public: - // Build temporal units from parsed OBUs - std::vector build(const std::vector>& obus, - const PackagingStrategy& strategy); - -private: - // Build using temporal delimiter boundaries - std::vector build_td_based(const std::vector>& obus, - bool drop_tds); - - // Helper: check if OBU should be included in samples - bool should_include_in_sample(const BaseOBU* obu, const PackagingStrategy& strategy) const; -}; - -} // namespace av2_obu diff --git a/include/av2_obu/core/obu_parser.h b/include/av2_obu/core/obu_parser.h index 4c1585e..b250cf5 100644 --- a/include/av2_obu/core/obu_parser.h +++ b/include/av2_obu/core/obu_parser.h @@ -134,7 +134,6 @@ class OBUParser { uint32_t read_annex_b_size(std::ifstream& ifs, uint32_t& value); void build_temporal_units(); - bool is_config_obu(const BaseOBU* obu) const; std::string current_file_; std::vector> obus_; diff --git a/include/av2_obu/core/temporal_unit.h b/include/av2_obu/core/temporal_unit.h index 987e0bd..7bf1af2 100644 --- a/include/av2_obu/core/temporal_unit.h +++ b/include/av2_obu/core/temporal_unit.h @@ -26,12 +26,22 @@ class TemporalUnit { size_t obu_count() const { return obus_.size(); } bool empty() const { return obus_.empty(); } - bool is_keyframe() const; + // True if this TU is a sync sample per av2-isobmff spec + bool is_sync_sample() const; + uint32_t display_order() const { return display_order_; } + // All OBUs in this TU as parsed from the bitstream (no filtering) const std::vector& obus() const { return obus_; } const BaseOBU* obu(size_t index) const { return obus_[index]; } + // Packager-oriented filtered views (computed on demand). + // Can be used to drive sample-entry transitions on SH change. + std::vector hls_obus() const; + + // OBUs that go into the ISOBMFF sample bytes. + std::vector sample_obus(bool keep_td = false) const; + auto begin() const { return obus_.begin(); } auto end() const { return obus_.end(); } @@ -45,7 +55,6 @@ class TemporalUnit { size_t index_ = 0; std::vector obus_; uint32_t display_order_ = 0; - bool is_keyframe_ = false; }; } // namespace av2_obu diff --git a/src/core/obu_parser.cpp b/src/core/obu_parser.cpp index acab2f5..6c4c3ab 100644 --- a/src/core/obu_parser.cpp +++ b/src/core/obu_parser.cpp @@ -310,14 +310,12 @@ void OBUParser::build_temporal_units() { TemporalUnit current_tu; current_tu.index_ = 0; + // Every parsed OBU lands in the TU it belongs to. + // Tools that want a ground-truth view consume tu.obus(). + // The packager applies its own filters via tu.sample_obus() / tu.hls_obus(). for (const auto& obu : obus_) { - if (is_config_obu(obu.get())) { - continue; - } - if (obu->type() == OBUType::TEMPORAL_DELIMITER) { if (!current_tu.empty()) { - current_tu.is_keyframe_ = current_tu.is_keyframe(); temporal_units_.push_back(std::move(current_tu)); current_tu = TemporalUnit(); current_tu.index_ = temporal_units_.size(); @@ -332,17 +330,10 @@ void OBUParser::build_temporal_units() { } if (!current_tu.empty()) { - current_tu.is_keyframe_ = current_tu.is_keyframe(); temporal_units_.push_back(std::move(current_tu)); } spdlog::debug("Built {} temporal units", temporal_units_.size()); } -bool OBUParser::is_config_obu(const BaseOBU* obu) const { - auto type = obu->type(); - return type == OBUType::SEQUENCE_HEADER || type == OBUType::LAYER_CONFIGURATION_RECORD || - type == OBUType::OPERATING_POINT_SET; -} - } // namespace av2_obu diff --git a/src/core/temporal_unit.cpp b/src/core/temporal_unit.cpp index 21e43f7..d1f4984 100644 --- a/src/core/temporal_unit.cpp +++ b/src/core/temporal_unit.cpp @@ -12,16 +12,59 @@ #include #include +#include + namespace av2_obu { -bool TemporalUnit::is_keyframe() const { +namespace { + +// Coded frame OBU types per AV2 spec (anything carrying frame data for an extended layer) +bool is_coded_frame_obu(OBUType t) { + return is_tile_group(t) || is_tip_frame(t) || is_sef(t) || t == OBUType::BRIDGE_FRAME; +} + +bool is_hls_obu(OBUType t) { + return t == OBUType::SEQUENCE_HEADER || t == OBUType::LAYER_CONFIGURATION_RECORD || + t == OBUType::OPERATING_POINT_SET; +} + +} // namespace + +bool TemporalUnit::is_sync_sample() const { + // Per av2-isobmff this TU is a sync sample if every coded extended layer unit present is a CLK + // We rely on AV2 TU ordering: within an xlayer's CLU, frames come in + // ascending mlayer order, so the FIRST coded frame OBU we encounter per + // xlayer in bitstream order IS the first frame at that xlayer's lowest present mlayer + std::set seen_xlayers; + bool any_frame = false; + for (const auto* obu : obus_) { + if (!is_coded_frame_obu(obu->type())) continue; + uint8_t xid = obu->header().get_xlayer_id(); + if (!seen_xlayers.insert(xid).second) continue; // already saw this xlayer's first frame + if (obu->type() != OBUType::CLK) return false; + any_frame = true; + } + return any_frame; +} + +std::vector TemporalUnit::hls_obus() const { + std::vector out; + for (const auto* obu : obus_) { + if (is_hls_obu(obu->type())) out.push_back(obu); + } + return out; +} + +std::vector TemporalUnit::sample_obus(bool keep_td) const { + std::vector out; for (const auto* obu : obus_) { - auto type = obu->type(); - if (type == OBUType::CLK || type == OBUType::OLK) { - return true; - } + auto t = obu->type(); + if (is_hls_obu(t)) continue; // belongs in configOBUs + if (t == OBUType::PADDING) continue; // forbidden in samples per av2-isobmff + if (t == OBUType::TEMPORAL_DELIMITER && !keep_td) continue; // sample boundary == TU boundary + out.push_back(obu); } - return false; + return out; } size_t TemporalUnit::total_size() const { @@ -37,7 +80,7 @@ nlohmann::json TemporalUnit::to_json() const { nlohmann::json j; j["index"] = index_; j["obu_count"] = obu_count(); - j["is_keyframe"] = is_keyframe(); + j["is_sync_sample"] = is_sync_sample(); j["display_order"] = display_order_; j["total_size"] = total_size(); From 260132b8a7a40b2afaf53b66f5b7bca2dfb56a1d Mon Sep 17 00:00:00 2001 From: Dimitri Podborski Date: Fri, 19 Jun 2026 10:59:53 -0700 Subject: [PATCH 04/10] setu av2C class --- CMakeLists.txt | 2 + apps/av2_obu_packager/CMakeLists.txt | 1 + apps/av2_obu_packager/av2_codec_config.cpp | 107 +++++++++++++++++++++ apps/av2_obu_packager/av2_codec_config.h | 80 +++++++++++++++ include/av2_obu/core/bitstream_writer.h | 46 +++++++++ src/core/bitstream_writer.cpp | 38 ++++++++ 6 files changed, 274 insertions(+) create mode 100644 apps/av2_obu_packager/av2_codec_config.cpp create mode 100644 apps/av2_obu_packager/av2_codec_config.h create mode 100644 include/av2_obu/core/bitstream_writer.h create mode 100644 src/core/bitstream_writer.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 35c4a1f..cdbfaa4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 @@ -91,6 +92,7 @@ target_sources(av2_obu $ $ $ + $ $ $ $ diff --git a/apps/av2_obu_packager/CMakeLists.txt b/apps/av2_obu_packager/CMakeLists.txt index d9091e0..dff7aed 100644 --- a/apps/av2_obu_packager/CMakeLists.txt +++ b/apps/av2_obu_packager/CMakeLists.txt @@ -1,6 +1,7 @@ add_executable(av2_obu_packager main.cpp packaging_strategy.cpp + av2_codec_config.cpp ) target_link_libraries(av2_obu_packager diff --git a/apps/av2_obu_packager/av2_codec_config.cpp b/apps/av2_obu_packager/av2_codec_config.cpp new file mode 100644 index 0000000..8e856c1 --- /dev/null +++ b/apps/av2_obu_packager/av2_codec_config.cpp @@ -0,0 +1,107 @@ +/* + * 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_codec_config.h" + +#include + +#include + +namespace av2_obu { + +AV2CodecConfigurationBox& AV2CodecConfigurationBox::set_from_sequence_header( + const AV2SequenceHeader& sh) { + configurationVersion = 1; + seq_profile_idc = sh.seq_profile_idc; + seq_level_idx = sh.seq_level_idx; + seq_tier = sh.seq_tier; + chroma_format_idc = static_cast(sh.chroma_format_idc); + bit_depth_idc = static_cast(sh.bit_depth_idc); + monotonic_output_order_flag = sh.monotonic_output_order_flag; + still_picture = sh.still_picture; + film_grain_params_present = sh.film_grain_params_present; + seq_initial_display_delay_present_flag = sh.seq_initial_display_delay_present_flag; + seq_initial_display_delay_minus_1 = sh.seq_initial_display_delay_minus_1; + max_frame_width_minus_1 = static_cast(sh.max_frame_width_minus_1); + max_frame_height_minus_1 = static_cast(sh.max_frame_height_minus_1); + return *this; +} + +bool AV2CodecConfigurationBox::append_config_obu(const BaseOBU& obu) { + if (!input_file_) { + spdlog::error("append_config_obu: no input file bound"); + return false; + } + + const auto& pos = obu.position(); + size_t total = pos.size_field_len + pos.header_len + pos.payload_size; + + auto saved_pos = input_file_->tellg(); + + input_file_->clear(); + input_file_->seekg(pos.start_pos); + if (!*input_file_) { + spdlog::error("Failed to seek to OBU at offset {}", static_cast(pos.start_pos)); + input_file_->clear(); + input_file_->seekg(saved_pos); + return false; + } + + ConfigEntry entry; + entry.obu = &obu; + entry.bytes.resize(total); + if (!input_file_->read(reinterpret_cast(entry.bytes.data()), + static_cast(total))) { + spdlog::error("Failed to read {} bytes for OBU at offset {}", total, + static_cast(pos.start_pos)); + input_file_->clear(); + input_file_->seekg(saved_pos); + return false; + } + + config_obus.push_back(std::move(entry)); + input_file_->clear(); + input_file_->seekg(saved_pos); + return true; +} + +std::vector AV2CodecConfigurationBox::serialize() const { + BitstreamWriter w; + w.write_bits(configurationVersion, 8); + w.write_bits(seq_profile_idc, 5); + w.write_bits(seq_level_idx, 5); + w.write_bits(seq_tier, 1); + w.write_bits(chroma_format_idc, 3); + w.write_bits(bit_depth_idc, 3); + w.write_bits(monotonic_output_order_flag, 1); + w.write_bits(still_picture, 1); + w.write_bits(film_grain_params_present, 1); + w.write_bits(seq_initial_display_delay_present_flag, 1); + w.write_bits(seq_initial_display_delay_minus_1, 4); + w.write_bits(0, 7); // reserved1 + w.write_bits(max_frame_width_minus_1, 16); + w.write_bits(max_frame_height_minus_1, 16); + + std::vector out = w.take(); + if (out.size() != 9) { + spdlog::error("av2C prefix size = {} bytes (expected 9)", out.size()); + return {}; + } + + for (const auto& entry : config_obus) { + out.insert(out.end(), entry.bytes.begin(), entry.bytes.end()); + } + spdlog::debug("Serialized av2C: {} bytes (9 prefix + {} configOBUs)", out.size(), + config_obus.size()); + return out; +} + +} // namespace av2_obu diff --git a/apps/av2_obu_packager/av2_codec_config.h b/apps/av2_obu_packager/av2_codec_config.h new file mode 100644 index 0000000..226e4be --- /dev/null +++ b/apps/av2_obu_packager/av2_codec_config.h @@ -0,0 +1,80 @@ +/* + * 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 +#include +#include + +#include +#include + +namespace av2_obu { + +// AV2CodecConfigurationBox ('av2C') is the codec config box for AV2 in ISOBMFF. +struct AV2CodecConfigurationBox { + uint32_t configurationVersion = 1; + + uint32_t seq_profile_idc = 0; // serialized as 5 bits + uint32_t seq_level_idx = 0; // 5 bits + uint32_t seq_tier = 0; // 1 bit + + uint32_t chroma_format_idc = 0; // 3 bits + uint32_t bit_depth_idc = 0; // 3 bits + + uint32_t monotonic_output_order_flag = 0; // 1 bit + uint32_t still_picture = 0; // 1 bit + uint32_t film_grain_params_present = 0; // 1 bit + uint32_t seq_initial_display_delay_present_flag = 0; // 1 bit + uint32_t seq_initial_display_delay_minus_1 = 0; // 4 bits + // reserved1 (7 bits) is implicit + + uint32_t max_frame_width_minus_1 = 0; // 16 bits + uint32_t max_frame_height_minus_1 = 0; // 16 bits + + // One entry per OBU embedded in configOBUs[]. Holds the OBU pointer for + // inspection (type, position, JSON) and the pre-read Annex B-framed bytes. + struct ConfigEntry { + const BaseOBU* obu = nullptr; + std::vector bytes; // leb128 size + header + payload + }; + + // Ordered list. The first entry must be the active Sequence Header OBU + // per av2-isobmff. Non-owning OBU pointers — caller keeps OBUParser alive. + std::vector config_obus; + + // Default constructor for tests; append_config_obu() requires bind_input_file() + // to have been called first (or use the file-binding constructor below). + AV2CodecConfigurationBox() = default; + + // Bind to an input bitstream. The ifstream must outlive this object. + explicit AV2CodecConfigurationBox(std::ifstream& input_file) : input_file_(&input_file) {} + + void bind_input_file(std::ifstream& input_file) { input_file_ = &input_file; } + + // Populate scalar fields from a parsed sequence header. The SH OBU and any + // additional configOBUs entries are appended separately via append_config_obu. + AV2CodecConfigurationBox& set_from_sequence_header(const AV2SequenceHeader& sh); + + // Read obu's Annex B-framed bytes from the bound input file (preserving + // stream position) and append a ConfigEntry. Returns false on I/O error or + // if no input file has been bound. + bool append_config_obu(const BaseOBU& obu); + + // Bit-pack scalars + concatenate config_obus[].bytes. Pure const operation. + std::vector serialize() const; + +private: + std::ifstream* input_file_ = nullptr; +}; + +} // namespace av2_obu diff --git a/include/av2_obu/core/bitstream_writer.h b/include/av2_obu/core/bitstream_writer.h new file mode 100644 index 0000000..b0582f0 --- /dev/null +++ b/include/av2_obu/core/bitstream_writer.h @@ -0,0 +1,46 @@ +/* + * 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 +#include + +namespace av2_obu { + +// MSB-first bit writer matching the ISOBMFF / AV2 f(N) descriptor convention. +// Writes are appended to an internal byte buffer. +class BitstreamWriter { +public: + // f(n) — write the low n bits of value (0 < n <= 32). + void write_bits(uint32_t value, uint32_t n); + + // f(1) + void write_bit(uint32_t value) { write_bits(value, 1); } + + // Pad with zero bits to the next byte boundary. No-op if already aligned. + void byte_align(); + + // Number of bits / bytes written so far. + size_t bit_count() const { return bit_pos_; } + size_t byte_count() const { return buf_.size(); } + bool byte_aligned() const { return (bit_pos_ & 7) == 0; } + + // Borrow / take the underlying buffer. + const std::vector& bytes() const { return buf_; } + std::vector take() { return std::move(buf_); } + +private: + std::vector buf_; + size_t bit_pos_ = 0; +}; + +} // namespace av2_obu diff --git a/src/core/bitstream_writer.cpp b/src/core/bitstream_writer.cpp new file mode 100644 index 0000000..5f8e13a --- /dev/null +++ b/src/core/bitstream_writer.cpp @@ -0,0 +1,38 @@ +/* + * 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 + +#include + +namespace av2_obu { + +void BitstreamWriter::write_bits(uint32_t value, uint32_t n) { + while (n > 0) { + if ((bit_pos_ & 7) == 0) buf_.push_back(0); + uint32_t bit_in_byte = static_cast(bit_pos_ & 7); + uint32_t bits_left_in_byte = 8 - bit_in_byte; + uint32_t bits_to_write = std::min(n, bits_left_in_byte); + uint32_t shift = n - bits_to_write; + uint8_t bits = static_cast((value >> shift) & ((1u << bits_to_write) - 1)); + buf_.back() |= static_cast(bits << (bits_left_in_byte - bits_to_write)); + bit_pos_ += bits_to_write; + n -= bits_to_write; + } +} + +void BitstreamWriter::byte_align() { + if ((bit_pos_ & 7) != 0) { + write_bits(0, 8 - static_cast(bit_pos_ & 7)); + } +} + +} // namespace av2_obu From 332b354618e55b96e944b8640cf28c114c3b6c0c Mon Sep 17 00:00:00 2001 From: Dimitri Podborski Date: Sat, 20 Jun 2026 11:20:26 -0700 Subject: [PATCH 05/10] setup packaging and the strategy --- apps/av2_obu_packager/CMakeLists.txt | 2 + apps/av2_obu_packager/av2_packager.cpp | 142 ++++++++++++++ apps/av2_obu_packager/av2_packager.h | 54 ++++++ apps/av2_obu_packager/main.cpp | 133 +++---------- apps/av2_obu_packager/mp4_writer.cpp | 187 +++++++++++++++++++ apps/av2_obu_packager/mp4_writer.h | 66 +++++++ apps/av2_obu_packager/packaging_strategy.cpp | 53 +++--- apps/av2_obu_packager/packaging_strategy.h | 86 ++++++++- 8 files changed, 588 insertions(+), 135 deletions(-) create mode 100644 apps/av2_obu_packager/av2_packager.cpp create mode 100644 apps/av2_obu_packager/av2_packager.h create mode 100644 apps/av2_obu_packager/mp4_writer.cpp create mode 100644 apps/av2_obu_packager/mp4_writer.h diff --git a/apps/av2_obu_packager/CMakeLists.txt b/apps/av2_obu_packager/CMakeLists.txt index dff7aed..38d6eac 100644 --- a/apps/av2_obu_packager/CMakeLists.txt +++ b/apps/av2_obu_packager/CMakeLists.txt @@ -2,6 +2,8 @@ add_executable(av2_obu_packager main.cpp packaging_strategy.cpp av2_codec_config.cpp + mp4_writer.cpp + av2_packager.cpp ) target_link_libraries(av2_obu_packager diff --git a/apps/av2_obu_packager/av2_packager.cpp b/apps/av2_obu_packager/av2_packager.cpp new file mode 100644 index 0000000..9b2afad --- /dev/null +++ b/apps/av2_obu_packager/av2_packager.cpp @@ -0,0 +1,142 @@ +/* + * 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_packager.h" + +#include + +#include + +#include "av2_codec_config.h" +#include +#include +#include +#include + +namespace av2_obu { + +Av2Packager::Av2Packager(const std::string& input_path, const PackagingStrategy& strategy) + : input_path_(input_path), strategy_(strategy) {} + +bool Av2Packager::package(const OBUParser& parser, const std::string& output_path) { + input_ifs_.open(input_path_, std::ios::binary); + if (!input_ifs_) { + spdlog::error("Failed to open {} for byte extraction", input_path_); + return false; + } + + if (!setup_video_track(parser)) return false; + + // Resolve TU range: [start, start + num_samples) clamped to available TUs. + const auto& tus = parser.temporal_units(); + if (strategy_.start_tu >= tus.size()) { + spdlog::error("--start-tu {} out of range (have {} TUs)", strategy_.start_tu, tus.size()); + return false; + } + if (strategy_.start_tu > 0 && !tus[strategy_.start_tu].is_sync_sample()) { + spdlog::warn("--start-tu {} is not a sync sample; output may not be cleanly decodable", + strategy_.start_tu); + } + const uint32_t end_tu = + strategy_.num_samples == 0 + ? static_cast(tus.size()) + : std::min(strategy_.start_tu + strategy_.num_samples, + static_cast(tus.size())); + spdlog::info("Writing TUs [{}, {}) of {} total", strategy_.start_tu, end_tu, tus.size()); + + // 5.1: write only the first TU in the selected range. 5.2 will loop. + if (!write_tu(tus[strategy_.start_tu])) return false; + + return writer_.finalize(output_path); +} + +bool Av2Packager::setup_video_track(const OBUParser& parser) { + const SequenceHeaderOBU* first_sh = find_first_sequence_header(parser); + if (!first_sh) { + spdlog::error("No Sequence Header OBU found"); + return false; + } + const AV2SequenceHeader& sh = first_sh->sequence_header(); + + AV2CodecConfigurationBox av2c(input_ifs_); + av2c.set_from_sequence_header(sh); + if (!av2c.append_config_obu(*first_sh)) { + spdlog::error("Failed to append SH to av2C configOBUs"); + return false; + } + + uint32_t w_full = static_cast(sh.max_frame_width_minus_1) + 1; + uint32_t h_full = static_cast(sh.max_frame_height_minus_1) + 1; + if (w_full > 0xFFFF || h_full > 0xFFFF) { + spdlog::warn("Frame dimensions {}x{} exceed 16-bit av2C field; truncating", w_full, h_full); + } + uint16_t width = static_cast(w_full & 0xFFFF); + uint16_t height = static_cast(h_full & 0xFFFF); + + return writer_.add_video_track(strategy_.timescale, width, height, av2c); +} + +bool Av2Packager::write_tu(const TemporalUnit& tu) { + std::vector bytes; + if (!assemble_sample_bytes(tu, bytes)) return false; + return writer_.add_sample(bytes, strategy_.default_sample_duration, tu.is_sync_sample()); +} + +bool Av2Packager::assemble_sample_bytes(const TemporalUnit& tu, std::vector& out) { + out.clear(); + const bool keep_td = !strategy_.drop_temporal_delimiters; + for (const auto* obu : tu.sample_obus(keep_td)) { + const auto& pos = obu->position(); + size_t total = pos.size_field_len + pos.header_len + pos.payload_size; + size_t prev = out.size(); + out.resize(prev + total); + + input_ifs_.clear(); + input_ifs_.seekg(pos.start_pos); + if (!input_ifs_.read(reinterpret_cast(out.data() + prev), + static_cast(total))) { + spdlog::error("Failed to read {} bytes for OBU at offset {}", total, + static_cast(pos.start_pos)); + return false; + } + } + return true; +} + +const SequenceHeaderOBU* Av2Packager::find_first_sequence_header(const OBUParser& parser) { + for (const auto& obu : parser.obus()) { + if (obu->type() == OBUType::SEQUENCE_HEADER) { + if (auto* sh = dynamic_cast(obu.get())) return sh; + } + } + return nullptr; +} + +void log_stream_summary(const OBUParser& parser) { + const auto stats = parser.get_statistics(); + spdlog::debug("Stream summary:"); + spdlog::debug(" OBUs: {} ({} bytes)", stats.total_obus, stats.total_bytes); + spdlog::debug(" Sequence headers: {}{}", stats.sequence_headers.count, + stats.sequence_headers.has_changes ? " (with changes)" : " (identical)"); + spdlog::debug(" TDs: {}", stats.temporal.td_count); + spdlog::debug(" Frames: {} ({} key)", stats.frames.total_frames, stats.frames.keyframe_count); + spdlog::debug(" Single layer: {}", stats.layers.is_single_layer ? "yes" : "no"); + spdlog::debug(" CI timing_info: {}", + stats.content_interpretation.has_timing_info ? "present" : "absent"); + + size_t sync_count = 0; + for (const auto& tu : parser.temporal_units()) { + if (tu.is_sync_sample()) sync_count++; + } + spdlog::debug(" TUs: {} ({} sync samples)", parser.temporal_units().size(), sync_count); +} + +} // namespace av2_obu diff --git a/apps/av2_obu_packager/av2_packager.h b/apps/av2_obu_packager/av2_packager.h new file mode 100644 index 0000000..eb4d97f --- /dev/null +++ b/apps/av2_obu_packager/av2_packager.h @@ -0,0 +1,54 @@ +/* + * 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 +#include +#include + +#include "mp4_writer.h" +#include "packaging_strategy.h" + +namespace av2_obu { + +class OBUParser; +class TemporalUnit; +class SequenceHeaderOBU; +class BaseOBU; + +// Av2Packager turns a parsed AV2 elementary bitstream into an ISOBMFF .mp4 +// using the bound PackagingStrategy. Owns the input ifstream and the Mp4Writer. +class Av2Packager { +public: + Av2Packager(const std::string& input_path, const PackagingStrategy& strategy); + + // Drive the whole flow: install sample entry from the first SH, write each + // TU as a sample, finalize. + bool package(const OBUParser& parser, const std::string& output_path); + +private: + bool setup_video_track(const OBUParser& parser); + bool write_tu(const TemporalUnit& tu); + bool assemble_sample_bytes(const TemporalUnit& tu, std::vector& out); + + static const SequenceHeaderOBU* find_first_sequence_header(const OBUParser& parser); + + std::string input_path_; + PackagingStrategy strategy_; + std::ifstream input_ifs_; + Mp4Writer writer_; +}; + +// Verbose-only log of stream characteristics. Does not influence packaging. +void log_stream_summary(const class OBUParser& parser); + +} // namespace av2_obu diff --git a/apps/av2_obu_packager/main.cpp b/apps/av2_obu_packager/main.cpp index 6d0b267..4beb7b7 100644 --- a/apps/av2_obu_packager/main.cpp +++ b/apps/av2_obu_packager/main.cpp @@ -13,20 +13,14 @@ #include #include +#include "av2_packager.h" #include "packaging_strategy.h" #include #include -// libisomedia headers -#include -#include - -#include -#include - using namespace av2_obu; -void setup_logging(bool verbose) { +static void setup_logging(bool verbose) { auto console = spdlog::stdout_color_mt("console"); spdlog::set_default_logger(console); spdlog::set_level(verbose ? spdlog::level::debug : spdlog::level::info); @@ -35,130 +29,59 @@ void setup_logging(bool verbose) { } int main(int argc, char** argv) { - CLI::App app{"AV2 OBU Packager - Package AV2 bitstreams into MP4 containers"}; + CLI::App app{"AV2 OBU Packager — Package AV2 bitstreams into MP4 containers"}; app.set_version_flag("--version", av2_obu::build_version()); - // Global options - bool verbose = false; - app.add_flag("-v,--verbose", verbose, "Enable verbose/debug logging"); - - // Input/output std::string input; std::string output; + UserOptions opts; + bool verbose = false; app.add_option("input", input, "Input AV2 bitstream (.bin, .obu, .av2)")->required(); app.add_option("-o,--output", output, "Output MP4 file")->required(); - - // Packaging options - double frame_rate = 30.0; - bool drop_tds = true; - - app.add_option("--fps", frame_rate, "Frame rate (default: 30.0)"); - app.add_flag("--keep-td,!--drop-td", drop_tds, + app.add_option("--fps", opts.frame_rate, "Frame rate (default: 30.0)"); + app.add_option("--samples-per-chunk", opts.samples_per_chunk, + "Samples per chunk in stsc (default: 30)"); + app.add_option("--start-tu", opts.start_tu, + "Start packaging at this TU index (0-based; should be a sync sample)"); + app.add_option("--num-samples", opts.num_samples, + "Maximum samples to write (0 = all from start)"); + app.add_flag("--keep-td,!--drop-td", opts.drop_temporal_delimiters, "Keep temporal delimiters in samples (default: drop)"); + app.add_flag("-v,--verbose", verbose, "Enable verbose/debug logging"); CLI11_PARSE(app, argc, argv); - setup_logging(verbose); spdlog::info("=== AV2 OBU Packager ==="); - spdlog::info("Input: {}", input); + spdlog::info("Input: {}", input); spdlog::info("Output: {}", output); - spdlog::info(""); - - // ======================================================================== - // PHASE 1: Parse bitstream and build temporal units - // ======================================================================== - spdlog::info("Phase 1: Parsing bitstream..."); + // --- Parse the bitstream --- OBUParser parser; - parser.set_include_temporal_delimiters(!drop_tds); - + parser.set_include_temporal_delimiters(!opts.drop_temporal_delimiters); if (!parser.parse_file(input)) { spdlog::error("Failed to parse bitstream"); return 1; } - spdlog::info(" Parsed {} OBUs", parser.obu_count()); - spdlog::info(" Built {} temporal units", parser.temporal_unit_count()); - spdlog::info(""); - - // ======================================================================== - // PHASE 2: Analyze bitstream characteristics - // ======================================================================== - spdlog::info("Phase 2: Analyzing bitstream..."); - auto stats = parser.get_statistics(); - - spdlog::info(" Total OBUs: {}", stats.total_obus); - spdlog::info(" Total bytes: {}", stats.total_bytes); - spdlog::info(" Sequence headers: {}{}", stats.sequence_headers.count, - stats.sequence_headers.has_changes ? " (with changes)" : "(identical)"); - spdlog::info(" Temporal delimiter count: {}", stats.temporal.td_count); - spdlog::info(" Frames: {} ({} keyframes)", stats.frames.total_frames, - stats.frames.keyframe_count); - spdlog::info(" Single layer: {}", stats.layers.is_single_layer ? "yes" : "no"); - spdlog::info(""); - - // ======================================================================== - // PHASE 3: Access temporal units from parser - // ======================================================================== - spdlog::info("Phase 3: Processing temporal units..."); - - const auto& temporal_units = parser.temporal_units(); - - if (temporal_units.empty()) { - spdlog::error("No temporal units created!"); - return 1; - } - - size_t sync_sample_count = 0; - for (const auto& tu : temporal_units) { - if (tu.is_sync_sample()) { - sync_sample_count++; - } - } - - spdlog::info(" {} temporal units (MP4 samples)", temporal_units.size()); - spdlog::info(" {} sync samples", sync_sample_count); - spdlog::info(""); - - // ======================================================================== - // PHASE 4: Write MP4 (TODO: Implement fully) - // ======================================================================== - spdlog::info("Phase 4: Writing MP4..."); - spdlog::warn("MP4 writing not yet fully implemented!"); - spdlog::warn("TODO:"); - spdlog::warn(" - Create sample entry with av2C box"); - spdlog::warn(" - Write temporal units as samples"); - spdlog::warn(" - Handle sync samples (keyframes)"); - spdlog::warn(" - Set correct timing"); + // --- Derive packaging strategy from stats + user options --- + const PackagingStrategy strategy = determine_strategy(parser.get_statistics(), opts); + log_stream_summary(parser); // verbose-only - // For now, just create an empty MP4 to test libisomedia - MP4Movie moov; - MP4Err err = MP4NewMovie(&moov, 1, 0xff, 0xff, 0xff, 0xff, 0xff); - if (err != MP4NoErr) { - spdlog::error("Failed to create MP4 movie, error code: {}", err); + // --- Pre-flight check --- + if (parser.temporal_units().empty()) { + spdlog::error("No temporal units; nothing to package"); return 1; } - // Set brands - ISOSetMovieBrand(moov, ISOISOBrand, 1); - ISOSetMovieCompatibleBrand(moov, ISOISOBrand); - ISOSetMovieCompatibleBrand(moov, MP4_FOUR_CHAR_CODE('a', 'v', '0', '2')); - - spdlog::info("Writing placeholder MP4 to: {}", output); - err = MP4WriteMovieToFile(moov, output.c_str()); - if (err != MP4NoErr) { - spdlog::error("Failed to write MP4 file, error code: {}", err); - MP4DisposeMovie(moov); + // --- Package --- + Av2Packager packager(input, strategy); + if (!packager.package(parser, output)) { + spdlog::error("Packaging failed"); return 1; } - MP4DisposeMovie(moov); - - spdlog::info(""); - spdlog::info("Success! (Note: MP4 writing is incomplete - placeholder file created)"); - spdlog::info("Next steps: Implement full MP4 track and sample writing"); - + spdlog::info("Done: {}", output); return 0; } diff --git a/apps/av2_obu_packager/mp4_writer.cpp b/apps/av2_obu_packager/mp4_writer.cpp new file mode 100644 index 0000000..1b7f81d --- /dev/null +++ b/apps/av2_obu_packager/mp4_writer.cpp @@ -0,0 +1,187 @@ +/* + * 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 "mp4_writer.h" + +#include + +#include + +namespace av2_obu { + +namespace { + +constexpr uint32_t kBrandAv02 = MP4_FOUR_CHAR_CODE('a', 'v', '0', '2'); +constexpr uint32_t kBrandIso6 = MP4_FOUR_CHAR_CODE('i', 's', 'o', '6'); +constexpr uint32_t kAtomTypeAv2C = MP4_FOUR_CHAR_CODE('a', 'v', '2', 'C'); +constexpr uint32_t kSampleEntryTypeAv02 = MP4_FOUR_CHAR_CODE('a', 'v', '0', '2'); + +// Allocate an MP4Handle and copy bytes into it. +MP4Handle make_handle(const void* data, size_t size) { + MP4Handle h = nullptr; + if (MP4NewHandle(static_cast(size), &h) != MP4NoErr || !h) return nullptr; + if (size > 0) std::memcpy(*h, data, size); + return h; +} + +} // namespace + +Mp4Writer::Mp4Writer() { + // Profile/level fields are MPEG-4 systems-era and irrelevant for AV2 video; + // 0xff means "no profile" per libisomedia conventions. + MP4Err err = MP4NewMovie(&movie_, /*initialODID=*/1, + /*OD*/ 0xff, /*scene*/ 0xff, + /*audio*/ 0xff, /*visual*/ 0xff, + /*graphics*/ 0xff); + if (err != MP4NoErr) { + spdlog::error("MP4NewMovie failed (err={})", err); + movie_ = nullptr; + return; + } + + ISOSetMovieBrand(movie_, kBrandAv02, /*minor=*/0); + ISOSetMovieCompatibleBrand(movie_, kBrandAv02); + ISOSetMovieCompatibleBrand(movie_, kBrandIso6); +} + +Mp4Writer::~Mp4Writer() { + if (sample_entry_) MP4DisposeHandle(sample_entry_); + if (movie_) MP4DisposeMovie(movie_); +} + +bool Mp4Writer::add_video_track(uint32_t timescale, uint16_t width, uint16_t height, + const AV2CodecConfigurationBox& av2c) { + if (!movie_) return false; + + MP4Err err = MP4NewMovieTrack(movie_, MP4NewTrackIsVisual, &track_); + if (err != MP4NoErr || !track_) { + spdlog::error("MP4NewMovieTrack failed (err={})", err); + return false; + } + + err = MP4NewTrackMedia(track_, &media_, MP4VisualHandlerType, timescale, /*dataRef=*/nullptr); + if (err != MP4NoErr || !media_) { + spdlog::error("MP4NewTrackMedia failed (err={})", err); + return false; + } + + // Build the av2C extension atom from the box bytes. + std::vector av2c_bytes = av2c.serialize(); + if (av2c_bytes.empty()) { + spdlog::error("av2C serialization produced empty payload"); + return false; + } + + MP4Handle av2c_payload = make_handle(av2c_bytes.data(), av2c_bytes.size()); + if (!av2c_payload) { + spdlog::error("MP4NewHandle(av2C payload) failed"); + return false; + } + + MP4GenericAtom av2c_atom = nullptr; + err = MP4NewForeignAtom(&av2c_atom, kAtomTypeAv2C, av2c_payload); + // Ownership of the payload bytes transfers to the atom on success. + // (libisomedia takes a copy; we still dispose ours to be safe — but only + // if the call succeeded with a different owner, otherwise we leak.) + if (err != MP4NoErr || !av2c_atom) { + spdlog::error("MP4NewForeignAtom('av2C') failed (err={})", err); + MP4DisposeHandle(av2c_payload); + return false; + } + + // Create the sample entry and attach the av2C atom. + err = MP4NewHandle(0, &sample_entry_); + if (err != MP4NoErr || !sample_entry_) { + spdlog::error("MP4NewHandle(sample_entry) failed (err={})", err); + return false; + } + + err = ISONewGeneralSampleDescription(track_, sample_entry_, /*dataRefIdx=*/1, + kSampleEntryTypeAv02, av2c_atom); + if (err != MP4NoErr) { + spdlog::error("ISONewGeneralSampleDescription failed (err={})", err); + return false; + } + + err = ISOSetSampleDescriptionDimensions(sample_entry_, width, height); + if (err != MP4NoErr) { + spdlog::error("ISOSetSampleDescriptionDimensions failed (err={})", err); + return false; + } + + return true; +} + +bool Mp4Writer::add_sample(const std::vector& bytes, uint32_t duration, bool is_sync) { + if (!media_ || !sample_entry_) return false; + pending_sizes_.push_back(static_cast(bytes.size())); + pending_data_.insert(pending_data_.end(), bytes.begin(), bytes.end()); + pending_durations_.push_back(duration); + if (is_sync) { + // 1-based index relative to this chunk. + pending_sync_indices_.push_back(static_cast(pending_sizes_.size())); + } + return true; +} + +bool Mp4Writer::flush_chunk() { + if (pending_sizes_.empty()) return true; + if (!media_) return false; + + uint32_t n = static_cast(pending_sizes_.size()); + + MP4Handle data = make_handle(pending_data_.data(), pending_data_.size()); + MP4Handle sizes = make_handle(pending_sizes_.data(), n * sizeof(uint32_t)); + MP4Handle durations = make_handle(pending_durations_.data(), n * sizeof(uint32_t)); + MP4Handle sync = pending_sync_indices_.empty() + ? nullptr + : make_handle(pending_sync_indices_.data(), + pending_sync_indices_.size() * sizeof(uint32_t)); + + // First chunk passes sample_entry_; later chunks pass NULL to re-use it. + MP4Handle entry_for_call = first_chunk_ ? sample_entry_ : nullptr; + + MP4Err err = MP4AddMediaSamples(media_, data, n, durations, sizes, entry_for_call, + /*decodingOffsetsH=*/nullptr, sync); + + MP4DisposeHandle(data); + MP4DisposeHandle(sizes); + MP4DisposeHandle(durations); + if (sync) MP4DisposeHandle(sync); + + if (err != MP4NoErr) { + spdlog::error("MP4AddMediaSamples failed (err={}, samples={})", err, n); + return false; + } + + first_chunk_ = false; + pending_data_.clear(); + pending_sizes_.clear(); + pending_durations_.clear(); + pending_sync_indices_.clear(); + spdlog::debug("Flushed chunk: {} samples", n); + return true; +} + +bool Mp4Writer::finalize(const std::string& output_path) { + if (!movie_) return false; + if (!flush_chunk()) return false; + + MP4Err err = MP4WriteMovieToFile(movie_, output_path.c_str()); + if (err != MP4NoErr) { + spdlog::error("MP4WriteMovieToFile failed (err={})", err); + return false; + } + spdlog::info("Wrote {}", output_path); + return true; +} + +} // namespace av2_obu diff --git a/apps/av2_obu_packager/mp4_writer.h b/apps/av2_obu_packager/mp4_writer.h new file mode 100644 index 0000000..9870681 --- /dev/null +++ b/apps/av2_obu_packager/mp4_writer.h @@ -0,0 +1,66 @@ +/* + * 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 +#include +#include + +// libisomedia +#include +#include + +#include "av2_codec_config.h" + +namespace av2_obu { + +// Mp4Writer wraps libisomedia to produce an AV2-in-ISOBMFF .mp4. Owns the +class Mp4Writer { +public: + Mp4Writer(); + ~Mp4Writer(); + + Mp4Writer(const Mp4Writer&) = delete; + Mp4Writer& operator=(const Mp4Writer&) = delete; + + // Create one video track and install its sample entry from av2c. + bool add_video_track(uint32_t timescale, uint16_t width, uint16_t height, + const AV2CodecConfigurationBox& av2c); + + // Buffer a sample for the next chunk flush. duration is in track timescale units + // Bytes are copied into an internal buffer; caller's vector can be freed after the call returns. + bool add_sample(const std::vector& bytes, uint32_t duration, bool is_sync); + + // Flush all currently buffered samples to libisomedia (one MP4AddMediaSamples call = one chunk in the stsc box). + // No-op if buffer is empty. Auto-triggers by samples_per_chunk. + bool flush_chunk(); + + // Flush any pending samples and write the .mp4 to disk. + bool finalize(const std::string& output_path); + +private: + MP4Movie movie_ = nullptr; + MP4Track track_ = nullptr; + MP4Media media_ = nullptr; + MP4Handle sample_entry_ = nullptr; + + // First chunk passes sample_entry_ to MP4AddMediaSamples; subsequent chunks pass NULL to re-use it (per MP4Movies.h) + bool first_chunk_ = true; + + // Pending-chunk buffer. + std::vector pending_data_; // concatenated sample bytes + std::vector pending_sizes_; // per-sample byte sizes + std::vector pending_durations_; + std::vector pending_sync_indices_; // 1-based indices within this chunk +}; + +} // namespace av2_obu diff --git a/apps/av2_obu_packager/packaging_strategy.cpp b/apps/av2_obu_packager/packaging_strategy.cpp index 4d57cba..f937d46 100644 --- a/apps/av2_obu_packager/packaging_strategy.cpp +++ b/apps/av2_obu_packager/packaging_strategy.cpp @@ -17,28 +17,37 @@ namespace av2_obu { PackagingStrategy determine_strategy(const OBUParser::Statistics& stats, const UserOptions& user_opts) { - PackagingStrategy strategy; - - // Timing parameters - strategy.frame_rate = user_opts.frame_rate; - strategy.timescale = static_cast(user_opts.frame_rate * 1000); - strategy.drop_temporal_delimiters = user_opts.drop_temporal_delimiters; - - // Determine sample entry strategy - if (stats.sequence_headers.has_changes) { - spdlog::info("Multiple different sequence headers detected"); - spdlog::info("Strategy: Will use multiple sample entries"); - strategy.sample_entry_mode = PackagingStrategy::SampleEntryMode::kMultiple; - } else { - spdlog::info("Strategy: Using single sample entry"); - strategy.sample_entry_mode = PackagingStrategy::SampleEntryMode::kSingle; - } - - // Log additional info - spdlog::info("Timing: {} fps (timescale: {})", strategy.frame_rate, strategy.timescale); - spdlog::info("Drop TDs from samples: {}", strategy.drop_temporal_delimiters ? "yes" : "no"); - - return strategy; + PackagingStrategy s; + + // Pass-through user opts + s.drop_temporal_delimiters = user_opts.drop_temporal_delimiters; + s.samples_per_chunk = user_opts.samples_per_chunk; + s.start_tu = user_opts.start_tu; + s.num_samples = user_opts.num_samples; + + // Timing — CLI takes precedence today. CI timing_info() will land in Step 8. + s.frame_rate = user_opts.frame_rate; + s.timescale = static_cast(user_opts.frame_rate * 1000); + s.default_sample_duration = 1000; + s.timing_source = PackagingStrategy::TimingSource::kCli; + + // Sample entry mode: any SH change → multiple entries. + s.sample_entry_mode = stats.sequence_headers.has_changes + ? PackagingStrategy::SampleEntryMode::kMultiple + : PackagingStrategy::SampleEntryMode::kSingle; + + // any_non_monotonic and ctts wiring land in Step 5.3 — needs SH access + // beyond what Statistics currently exposes. + + spdlog::debug("Strategy: sample_entry_mode={}", + s.sample_entry_mode == PackagingStrategy::SampleEntryMode::kMultiple ? "kMultiple" + : "kSingle"); + spdlog::debug("Strategy: timescale={}, duration={}, source=CLI", s.timescale, + s.default_sample_duration); + spdlog::debug("Strategy: drop_TDs={}, samples_per_chunk={}", + s.drop_temporal_delimiters ? "yes" : "no", s.samples_per_chunk); + + return s; } } // namespace av2_obu diff --git a/apps/av2_obu_packager/packaging_strategy.h b/apps/av2_obu_packager/packaging_strategy.h index ab295b1..6b89522 100644 --- a/apps/av2_obu_packager/packaging_strategy.h +++ b/apps/av2_obu_packager/packaging_strategy.h @@ -11,32 +11,102 @@ #pragma once +#include +#include + #include namespace av2_obu { -// Packaging strategy determined from bitstream analysis and user options +// FOURCC packed big-endian (matches ISOBMFF on-wire byte order: a is MSB). +inline constexpr uint32_t fourcc(char a, char b, char c, char d) { + return (static_cast(static_cast(a)) << 24) | + (static_cast(static_cast(b)) << 16) | + (static_cast(static_cast(c)) << 8) | + static_cast(static_cast(d)); +} + +// Packaging strategy: derived from bitstream analysis and user options. struct PackagingStrategy { enum class SampleEntryMode { - kSingle, // One sample entry for whole file - kMultiple // New entry when sequence header changes + kSingle, // one sample entry for the whole file + kMultiple // new entry on each new SH (Step 6) }; - SampleEntryMode sample_entry_mode = SampleEntryMode::kSingle; + enum class TimingSource { + kCli, // --fps from CLI (highest precedence) + kCiTimingInfo, // CI OBU timing_info() (Step 8) + kDefault // 30 fps fallback + }; + + // === Pass-through / user-driven === bool drop_temporal_delimiters = true; - // Timing + // === Sample entry (Step 6) === + SampleEntryMode sample_entry_mode = SampleEntryMode::kSingle; + + // === Timing === double frame_rate = 30.0; - uint32_t timescale = 30000; // frame_rate * 1000 + uint32_t timescale = 30000; + uint32_t default_sample_duration = 1000; + TimingSource timing_source = TimingSource::kDefault; + + // === ctts (Step 5.3) === + // Set true if any SH in the stream has monotonic_output_order_flag == 0. + // Triggers MP4UseSignedCompositionTimeOffsets in Mp4Writer. + bool any_non_monotonic = false; + + // === Chunking (Step 5.4) === + // Auto-flush trigger inside Mp4Writer::add_sample. Default ≈ fps. + uint32_t samples_per_chunk = 30; + + // === TU range selection (testing / trimming) === + uint32_t start_tu = 0; // 0-based TU index to start from + uint32_t num_samples = 0; // 0 = all (from start_tu to end) + + // === File-type / brands (Step 9) === + uint32_t major_brand = fourcc('a', 'v', '0', '2'); + std::vector compatible_brands = { + fourcc('a', 'v', '0', '2'), + fourcc('i', 's', 'o', '6'), + }; + + // === Fragmentation hooks (deferred — v2 / future) === + // Spec §CMAF will pin down what to put here. For now, defaults disable + // fragmentation entirely; Mp4Writer ignores these fields. + struct Fragmentation { + bool enabled = false; + uint32_t fragment_duration_ms = 0; // target fragment duration; 0 = unset + bool emit_sidx = false; // produce SegmentIndexBox for DASH + uint32_t subsegs_per_sidx = 0; // sidx layout + bool daisy_chain_sidx = false; // chained vs root sidx + } fragmentation; + + // === Encryption / CENC hooks (deferred — v2 / future) === + // Spec §CommonEncryption will pin down what to put here. For now, defaults + // disable encryption entirely; Mp4Writer ignores these fields. + struct Encryption { + bool enabled = false; + uint32_t scheme = 0; // 'cenc' / 'cbc1' / 'cens' / 'cbcs' / 'sve1' + std::vector kid; // 16-byte Key ID + uint32_t pattern_crypt_block_count = 0; // cbcs / cens pattern + uint32_t pattern_skip_block_count = 0; + // Key delivery, per-sample IVs, subsample mapping, PSSH boxes — out of + // strategy scope; would be supplied by an EncryptionStrategy / KeySource + // when CENC support lands. + } encryption; }; -// User options from CLI +// User options from CLI (raw input, no analysis applied). struct UserOptions { double frame_rate = 30.0; bool drop_temporal_delimiters = true; + uint32_t samples_per_chunk = 30; + uint32_t start_tu = 0; + uint32_t num_samples = 0; }; -// Determines packaging strategy from bitstream statistics and user options +// Apply user options + analysis to produce a strategy. PackagingStrategy determine_strategy(const OBUParser::Statistics& stats, const UserOptions& user_opts); From 22e5d323abc06f0eafe32e9f54ebab55a507bd9c Mon Sep 17 00:00:00 2001 From: Dimitri Podborski Date: Mon, 22 Jun 2026 15:00:48 -0700 Subject: [PATCH 06/10] simple demuxer --- CMakeLists.txt | 18 +- README.md | 5 +- apps/av2_demux/CMakeLists.txt | 18 ++ apps/av2_demux/README.md | 28 ++ apps/av2_demux/av2_demuxer.cpp | 247 ++++++++++++++++++ apps/av2_demux/av2_demuxer.h | 26 ++ apps/av2_demux/main.cpp | 44 ++++ apps/av2_obu_packager/CMakeLists.txt | 22 -- apps/av2_obu_packager/README.md | 25 -- apps/av2_obu_packager/av2_codec_config.cpp | 107 -------- apps/av2_obu_packager/av2_codec_config.h | 80 ------ apps/av2_obu_packager/av2_packager.cpp | 142 ---------- apps/av2_obu_packager/av2_packager.h | 54 ---- apps/av2_obu_packager/main.cpp | 87 ------ apps/av2_obu_packager/mp4_writer.cpp | 187 ------------- apps/av2_obu_packager/mp4_writer.h | 66 ----- apps/av2_obu_packager/packaging_strategy.cpp | 53 ---- apps/av2_obu_packager/packaging_strategy.h | 113 -------- .../av2_obu/obus/content_interpretation_obu.h | 5 + src/obus/sequence_header_obu.cpp | 11 +- 20 files changed, 392 insertions(+), 946 deletions(-) create mode 100644 apps/av2_demux/CMakeLists.txt create mode 100644 apps/av2_demux/README.md create mode 100644 apps/av2_demux/av2_demuxer.cpp create mode 100644 apps/av2_demux/av2_demuxer.h create mode 100644 apps/av2_demux/main.cpp delete mode 100644 apps/av2_obu_packager/CMakeLists.txt delete mode 100644 apps/av2_obu_packager/README.md delete mode 100644 apps/av2_obu_packager/av2_codec_config.cpp delete mode 100644 apps/av2_obu_packager/av2_codec_config.h delete mode 100644 apps/av2_obu_packager/av2_packager.cpp delete mode 100644 apps/av2_obu_packager/av2_packager.h delete mode 100644 apps/av2_obu_packager/main.cpp delete mode 100644 apps/av2_obu_packager/mp4_writer.cpp delete mode 100644 apps/av2_obu_packager/mp4_writer.h delete mode 100644 apps/av2_obu_packager/packaging_strategy.cpp delete mode 100644 apps/av2_obu_packager/packaging_strategy.h diff --git a/CMakeLists.txt b/CMakeLists.txt index cdbfaa4..7f07e07 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -162,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( @@ -183,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) diff --git a/README.md b/README.md index 3858e74..c8d104a 100644 --- a/README.md +++ b/README.md @@ -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 ``` @@ -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 diff --git a/apps/av2_demux/CMakeLists.txt b/apps/av2_demux/CMakeLists.txt new file mode 100644 index 0000000..c579890 --- /dev/null +++ b/apps/av2_demux/CMakeLists.txt @@ -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 + $<$:-Wall -Wextra -Wpedantic> + $<$:/W4> +) + +install(TARGETS av2_demux RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/apps/av2_demux/README.md b/apps/av2_demux/README.md new file mode 100644 index 0000000..f9fc70b --- /dev/null +++ b/apps/av2_demux/README.md @@ -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`. diff --git a/apps/av2_demux/av2_demuxer.cpp b/apps/av2_demux/av2_demuxer.cpp new file mode 100644 index 0000000..db38c1b --- /dev/null +++ b/apps/av2_demux/av2_demuxer.cpp @@ -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 +#include +#include + +#include + +#include +#include + +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(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(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(p[0]) << 24) | (static_cast(p[1]) << 16) | + (static_cast(p[2]) << 8) | static_cast(p[3]); +} + +bool read_av2c_payload(MP4Media media, std::vector& 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(*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 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(*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(sample_data), + static_cast(td_prefix)); + if (i == 1) { + out.write(reinterpret_cast(config_obus), + static_cast(config_obus_size)); + } + out.write(reinterpret_cast(sample_data + td_prefix), + static_cast(size - td_prefix)); + ++samples_with_td; + } else { + // Sample has no TD (the default --drop-td muxing). Synthesize one. + out.write(reinterpret_cast(kTdBytes), sizeof(kTdBytes)); + if (i == 1) { + out.write(reinterpret_cast(config_obus), + static_cast(config_obus_size)); + } + out.write(reinterpret_cast(sample_data), + static_cast(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 diff --git a/apps/av2_demux/av2_demuxer.h b/apps/av2_demux/av2_demuxer.h new file mode 100644 index 0000000..ff5199c --- /dev/null +++ b/apps/av2_demux/av2_demuxer.h @@ -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 +#include + +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 diff --git a/apps/av2_demux/main.cpp b/apps/av2_demux/main.cpp new file mode 100644 index 0000000..375b3c6 --- /dev/null +++ b/apps/av2_demux/main.cpp @@ -0,0 +1,44 @@ +/* + * 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 +#include +#include + +#include "av2_demuxer.h" +#include + +int main(int argc, char** argv) { + CLI::App app{"av2_demux — demux MP4/ISOBMFF back into an AV2 elementary bitstream"}; + app.set_version_flag("--version", av2_obu::build_version()); + + std::string input; + std::string output; + bool verbose = false; + + app.add_option("input", input, "Input MP4 file (must contain an 'av02' video track)")->required(); + app.add_option("-o,--output", output, "Output AV2 elementary bitstream (.obu)")->required(); + app.add_flag("-v,--verbose", verbose, "Enable verbose/debug logging"); + + CLI11_PARSE(app, argc, argv); + + auto console = spdlog::stdout_color_mt("console"); + spdlog::set_default_logger(console); + spdlog::set_level(verbose ? spdlog::level::debug : spdlog::level::info); + spdlog::set_pattern("[%H:%M:%S.%e] [%^%l%$] %v"); + + av2_obu::Av2Demuxer demuxer; + if (!demuxer.demux(input, output)) { + spdlog::error("Demuxing failed"); + return 1; + } + return 0; +} diff --git a/apps/av2_obu_packager/CMakeLists.txt b/apps/av2_obu_packager/CMakeLists.txt deleted file mode 100644 index 38d6eac..0000000 --- a/apps/av2_obu_packager/CMakeLists.txt +++ /dev/null @@ -1,22 +0,0 @@ -add_executable(av2_obu_packager - main.cpp - packaging_strategy.cpp - av2_codec_config.cpp - mp4_writer.cpp - av2_packager.cpp -) - -target_link_libraries(av2_obu_packager - PRIVATE - av2_obu # Brings spdlog and nlohmann_json transitively - CLI11::CLI11 - libisomediafile # ISOBMFF reference SW from MPEG -) - -target_compile_options(av2_obu_packager PRIVATE - $<$:-Wall -Wextra -Wpedantic> - $<$:/W4> -) - -# Install the executable -install(TARGETS av2_obu_packager RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/apps/av2_obu_packager/README.md b/apps/av2_obu_packager/README.md deleted file mode 100644 index 78465dc..0000000 --- a/apps/av2_obu_packager/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# av2_obu_packager - -Package AV2 elementary streams into MP4/ISOBMFF containers. - -## Build - -Requires enabling packager support during CMake configuration: - -```bash -cmake -S . -B mybuild -DBUILD_PACKAGER=ON -cmake --build mybuild -j -``` - -This will fetch MPEG's libisomedia reference implementation automatically. - -## Usage - -```bash -# Package AV2 bitstream into MP4 -./av2_obu_packager package input.bin -o output.mp4 --fps 30 -``` - -## Status - -Work in progress - basic setup implemented. diff --git a/apps/av2_obu_packager/av2_codec_config.cpp b/apps/av2_obu_packager/av2_codec_config.cpp deleted file mode 100644 index 8e856c1..0000000 --- a/apps/av2_obu_packager/av2_codec_config.cpp +++ /dev/null @@ -1,107 +0,0 @@ -/* - * 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_codec_config.h" - -#include - -#include - -namespace av2_obu { - -AV2CodecConfigurationBox& AV2CodecConfigurationBox::set_from_sequence_header( - const AV2SequenceHeader& sh) { - configurationVersion = 1; - seq_profile_idc = sh.seq_profile_idc; - seq_level_idx = sh.seq_level_idx; - seq_tier = sh.seq_tier; - chroma_format_idc = static_cast(sh.chroma_format_idc); - bit_depth_idc = static_cast(sh.bit_depth_idc); - monotonic_output_order_flag = sh.monotonic_output_order_flag; - still_picture = sh.still_picture; - film_grain_params_present = sh.film_grain_params_present; - seq_initial_display_delay_present_flag = sh.seq_initial_display_delay_present_flag; - seq_initial_display_delay_minus_1 = sh.seq_initial_display_delay_minus_1; - max_frame_width_minus_1 = static_cast(sh.max_frame_width_minus_1); - max_frame_height_minus_1 = static_cast(sh.max_frame_height_minus_1); - return *this; -} - -bool AV2CodecConfigurationBox::append_config_obu(const BaseOBU& obu) { - if (!input_file_) { - spdlog::error("append_config_obu: no input file bound"); - return false; - } - - const auto& pos = obu.position(); - size_t total = pos.size_field_len + pos.header_len + pos.payload_size; - - auto saved_pos = input_file_->tellg(); - - input_file_->clear(); - input_file_->seekg(pos.start_pos); - if (!*input_file_) { - spdlog::error("Failed to seek to OBU at offset {}", static_cast(pos.start_pos)); - input_file_->clear(); - input_file_->seekg(saved_pos); - return false; - } - - ConfigEntry entry; - entry.obu = &obu; - entry.bytes.resize(total); - if (!input_file_->read(reinterpret_cast(entry.bytes.data()), - static_cast(total))) { - spdlog::error("Failed to read {} bytes for OBU at offset {}", total, - static_cast(pos.start_pos)); - input_file_->clear(); - input_file_->seekg(saved_pos); - return false; - } - - config_obus.push_back(std::move(entry)); - input_file_->clear(); - input_file_->seekg(saved_pos); - return true; -} - -std::vector AV2CodecConfigurationBox::serialize() const { - BitstreamWriter w; - w.write_bits(configurationVersion, 8); - w.write_bits(seq_profile_idc, 5); - w.write_bits(seq_level_idx, 5); - w.write_bits(seq_tier, 1); - w.write_bits(chroma_format_idc, 3); - w.write_bits(bit_depth_idc, 3); - w.write_bits(monotonic_output_order_flag, 1); - w.write_bits(still_picture, 1); - w.write_bits(film_grain_params_present, 1); - w.write_bits(seq_initial_display_delay_present_flag, 1); - w.write_bits(seq_initial_display_delay_minus_1, 4); - w.write_bits(0, 7); // reserved1 - w.write_bits(max_frame_width_minus_1, 16); - w.write_bits(max_frame_height_minus_1, 16); - - std::vector out = w.take(); - if (out.size() != 9) { - spdlog::error("av2C prefix size = {} bytes (expected 9)", out.size()); - return {}; - } - - for (const auto& entry : config_obus) { - out.insert(out.end(), entry.bytes.begin(), entry.bytes.end()); - } - spdlog::debug("Serialized av2C: {} bytes (9 prefix + {} configOBUs)", out.size(), - config_obus.size()); - return out; -} - -} // namespace av2_obu diff --git a/apps/av2_obu_packager/av2_codec_config.h b/apps/av2_obu_packager/av2_codec_config.h deleted file mode 100644 index 226e4be..0000000 --- a/apps/av2_obu_packager/av2_codec_config.h +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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 -#include -#include - -#include -#include - -namespace av2_obu { - -// AV2CodecConfigurationBox ('av2C') is the codec config box for AV2 in ISOBMFF. -struct AV2CodecConfigurationBox { - uint32_t configurationVersion = 1; - - uint32_t seq_profile_idc = 0; // serialized as 5 bits - uint32_t seq_level_idx = 0; // 5 bits - uint32_t seq_tier = 0; // 1 bit - - uint32_t chroma_format_idc = 0; // 3 bits - uint32_t bit_depth_idc = 0; // 3 bits - - uint32_t monotonic_output_order_flag = 0; // 1 bit - uint32_t still_picture = 0; // 1 bit - uint32_t film_grain_params_present = 0; // 1 bit - uint32_t seq_initial_display_delay_present_flag = 0; // 1 bit - uint32_t seq_initial_display_delay_minus_1 = 0; // 4 bits - // reserved1 (7 bits) is implicit - - uint32_t max_frame_width_minus_1 = 0; // 16 bits - uint32_t max_frame_height_minus_1 = 0; // 16 bits - - // One entry per OBU embedded in configOBUs[]. Holds the OBU pointer for - // inspection (type, position, JSON) and the pre-read Annex B-framed bytes. - struct ConfigEntry { - const BaseOBU* obu = nullptr; - std::vector bytes; // leb128 size + header + payload - }; - - // Ordered list. The first entry must be the active Sequence Header OBU - // per av2-isobmff. Non-owning OBU pointers — caller keeps OBUParser alive. - std::vector config_obus; - - // Default constructor for tests; append_config_obu() requires bind_input_file() - // to have been called first (or use the file-binding constructor below). - AV2CodecConfigurationBox() = default; - - // Bind to an input bitstream. The ifstream must outlive this object. - explicit AV2CodecConfigurationBox(std::ifstream& input_file) : input_file_(&input_file) {} - - void bind_input_file(std::ifstream& input_file) { input_file_ = &input_file; } - - // Populate scalar fields from a parsed sequence header. The SH OBU and any - // additional configOBUs entries are appended separately via append_config_obu. - AV2CodecConfigurationBox& set_from_sequence_header(const AV2SequenceHeader& sh); - - // Read obu's Annex B-framed bytes from the bound input file (preserving - // stream position) and append a ConfigEntry. Returns false on I/O error or - // if no input file has been bound. - bool append_config_obu(const BaseOBU& obu); - - // Bit-pack scalars + concatenate config_obus[].bytes. Pure const operation. - std::vector serialize() const; - -private: - std::ifstream* input_file_ = nullptr; -}; - -} // namespace av2_obu diff --git a/apps/av2_obu_packager/av2_packager.cpp b/apps/av2_obu_packager/av2_packager.cpp deleted file mode 100644 index 9b2afad..0000000 --- a/apps/av2_obu_packager/av2_packager.cpp +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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_packager.h" - -#include - -#include - -#include "av2_codec_config.h" -#include -#include -#include -#include - -namespace av2_obu { - -Av2Packager::Av2Packager(const std::string& input_path, const PackagingStrategy& strategy) - : input_path_(input_path), strategy_(strategy) {} - -bool Av2Packager::package(const OBUParser& parser, const std::string& output_path) { - input_ifs_.open(input_path_, std::ios::binary); - if (!input_ifs_) { - spdlog::error("Failed to open {} for byte extraction", input_path_); - return false; - } - - if (!setup_video_track(parser)) return false; - - // Resolve TU range: [start, start + num_samples) clamped to available TUs. - const auto& tus = parser.temporal_units(); - if (strategy_.start_tu >= tus.size()) { - spdlog::error("--start-tu {} out of range (have {} TUs)", strategy_.start_tu, tus.size()); - return false; - } - if (strategy_.start_tu > 0 && !tus[strategy_.start_tu].is_sync_sample()) { - spdlog::warn("--start-tu {} is not a sync sample; output may not be cleanly decodable", - strategy_.start_tu); - } - const uint32_t end_tu = - strategy_.num_samples == 0 - ? static_cast(tus.size()) - : std::min(strategy_.start_tu + strategy_.num_samples, - static_cast(tus.size())); - spdlog::info("Writing TUs [{}, {}) of {} total", strategy_.start_tu, end_tu, tus.size()); - - // 5.1: write only the first TU in the selected range. 5.2 will loop. - if (!write_tu(tus[strategy_.start_tu])) return false; - - return writer_.finalize(output_path); -} - -bool Av2Packager::setup_video_track(const OBUParser& parser) { - const SequenceHeaderOBU* first_sh = find_first_sequence_header(parser); - if (!first_sh) { - spdlog::error("No Sequence Header OBU found"); - return false; - } - const AV2SequenceHeader& sh = first_sh->sequence_header(); - - AV2CodecConfigurationBox av2c(input_ifs_); - av2c.set_from_sequence_header(sh); - if (!av2c.append_config_obu(*first_sh)) { - spdlog::error("Failed to append SH to av2C configOBUs"); - return false; - } - - uint32_t w_full = static_cast(sh.max_frame_width_minus_1) + 1; - uint32_t h_full = static_cast(sh.max_frame_height_minus_1) + 1; - if (w_full > 0xFFFF || h_full > 0xFFFF) { - spdlog::warn("Frame dimensions {}x{} exceed 16-bit av2C field; truncating", w_full, h_full); - } - uint16_t width = static_cast(w_full & 0xFFFF); - uint16_t height = static_cast(h_full & 0xFFFF); - - return writer_.add_video_track(strategy_.timescale, width, height, av2c); -} - -bool Av2Packager::write_tu(const TemporalUnit& tu) { - std::vector bytes; - if (!assemble_sample_bytes(tu, bytes)) return false; - return writer_.add_sample(bytes, strategy_.default_sample_duration, tu.is_sync_sample()); -} - -bool Av2Packager::assemble_sample_bytes(const TemporalUnit& tu, std::vector& out) { - out.clear(); - const bool keep_td = !strategy_.drop_temporal_delimiters; - for (const auto* obu : tu.sample_obus(keep_td)) { - const auto& pos = obu->position(); - size_t total = pos.size_field_len + pos.header_len + pos.payload_size; - size_t prev = out.size(); - out.resize(prev + total); - - input_ifs_.clear(); - input_ifs_.seekg(pos.start_pos); - if (!input_ifs_.read(reinterpret_cast(out.data() + prev), - static_cast(total))) { - spdlog::error("Failed to read {} bytes for OBU at offset {}", total, - static_cast(pos.start_pos)); - return false; - } - } - return true; -} - -const SequenceHeaderOBU* Av2Packager::find_first_sequence_header(const OBUParser& parser) { - for (const auto& obu : parser.obus()) { - if (obu->type() == OBUType::SEQUENCE_HEADER) { - if (auto* sh = dynamic_cast(obu.get())) return sh; - } - } - return nullptr; -} - -void log_stream_summary(const OBUParser& parser) { - const auto stats = parser.get_statistics(); - spdlog::debug("Stream summary:"); - spdlog::debug(" OBUs: {} ({} bytes)", stats.total_obus, stats.total_bytes); - spdlog::debug(" Sequence headers: {}{}", stats.sequence_headers.count, - stats.sequence_headers.has_changes ? " (with changes)" : " (identical)"); - spdlog::debug(" TDs: {}", stats.temporal.td_count); - spdlog::debug(" Frames: {} ({} key)", stats.frames.total_frames, stats.frames.keyframe_count); - spdlog::debug(" Single layer: {}", stats.layers.is_single_layer ? "yes" : "no"); - spdlog::debug(" CI timing_info: {}", - stats.content_interpretation.has_timing_info ? "present" : "absent"); - - size_t sync_count = 0; - for (const auto& tu : parser.temporal_units()) { - if (tu.is_sync_sample()) sync_count++; - } - spdlog::debug(" TUs: {} ({} sync samples)", parser.temporal_units().size(), sync_count); -} - -} // namespace av2_obu diff --git a/apps/av2_obu_packager/av2_packager.h b/apps/av2_obu_packager/av2_packager.h deleted file mode 100644 index eb4d97f..0000000 --- a/apps/av2_obu_packager/av2_packager.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 -#include -#include - -#include "mp4_writer.h" -#include "packaging_strategy.h" - -namespace av2_obu { - -class OBUParser; -class TemporalUnit; -class SequenceHeaderOBU; -class BaseOBU; - -// Av2Packager turns a parsed AV2 elementary bitstream into an ISOBMFF .mp4 -// using the bound PackagingStrategy. Owns the input ifstream and the Mp4Writer. -class Av2Packager { -public: - Av2Packager(const std::string& input_path, const PackagingStrategy& strategy); - - // Drive the whole flow: install sample entry from the first SH, write each - // TU as a sample, finalize. - bool package(const OBUParser& parser, const std::string& output_path); - -private: - bool setup_video_track(const OBUParser& parser); - bool write_tu(const TemporalUnit& tu); - bool assemble_sample_bytes(const TemporalUnit& tu, std::vector& out); - - static const SequenceHeaderOBU* find_first_sequence_header(const OBUParser& parser); - - std::string input_path_; - PackagingStrategy strategy_; - std::ifstream input_ifs_; - Mp4Writer writer_; -}; - -// Verbose-only log of stream characteristics. Does not influence packaging. -void log_stream_summary(const class OBUParser& parser); - -} // namespace av2_obu diff --git a/apps/av2_obu_packager/main.cpp b/apps/av2_obu_packager/main.cpp deleted file mode 100644 index 4beb7b7..0000000 --- a/apps/av2_obu_packager/main.cpp +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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 -#include -#include - -#include "av2_packager.h" -#include "packaging_strategy.h" -#include -#include - -using namespace av2_obu; - -static void setup_logging(bool verbose) { - auto console = spdlog::stdout_color_mt("console"); - spdlog::set_default_logger(console); - spdlog::set_level(verbose ? spdlog::level::debug : spdlog::level::info); - av2_obu::set_log_level(verbose ? spdlog::level::debug : spdlog::level::info); - spdlog::set_pattern("[%H:%M:%S.%e] [%^%l%$] %v"); -} - -int main(int argc, char** argv) { - CLI::App app{"AV2 OBU Packager — Package AV2 bitstreams into MP4 containers"}; - app.set_version_flag("--version", av2_obu::build_version()); - - std::string input; - std::string output; - UserOptions opts; - bool verbose = false; - - app.add_option("input", input, "Input AV2 bitstream (.bin, .obu, .av2)")->required(); - app.add_option("-o,--output", output, "Output MP4 file")->required(); - app.add_option("--fps", opts.frame_rate, "Frame rate (default: 30.0)"); - app.add_option("--samples-per-chunk", opts.samples_per_chunk, - "Samples per chunk in stsc (default: 30)"); - app.add_option("--start-tu", opts.start_tu, - "Start packaging at this TU index (0-based; should be a sync sample)"); - app.add_option("--num-samples", opts.num_samples, - "Maximum samples to write (0 = all from start)"); - app.add_flag("--keep-td,!--drop-td", opts.drop_temporal_delimiters, - "Keep temporal delimiters in samples (default: drop)"); - app.add_flag("-v,--verbose", verbose, "Enable verbose/debug logging"); - - CLI11_PARSE(app, argc, argv); - setup_logging(verbose); - - spdlog::info("=== AV2 OBU Packager ==="); - spdlog::info("Input: {}", input); - spdlog::info("Output: {}", output); - - // --- Parse the bitstream --- - OBUParser parser; - parser.set_include_temporal_delimiters(!opts.drop_temporal_delimiters); - if (!parser.parse_file(input)) { - spdlog::error("Failed to parse bitstream"); - return 1; - } - - // --- Derive packaging strategy from stats + user options --- - const PackagingStrategy strategy = determine_strategy(parser.get_statistics(), opts); - log_stream_summary(parser); // verbose-only - - // --- Pre-flight check --- - if (parser.temporal_units().empty()) { - spdlog::error("No temporal units; nothing to package"); - return 1; - } - - // --- Package --- - Av2Packager packager(input, strategy); - if (!packager.package(parser, output)) { - spdlog::error("Packaging failed"); - return 1; - } - - spdlog::info("Done: {}", output); - return 0; -} diff --git a/apps/av2_obu_packager/mp4_writer.cpp b/apps/av2_obu_packager/mp4_writer.cpp deleted file mode 100644 index 1b7f81d..0000000 --- a/apps/av2_obu_packager/mp4_writer.cpp +++ /dev/null @@ -1,187 +0,0 @@ -/* - * 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 "mp4_writer.h" - -#include - -#include - -namespace av2_obu { - -namespace { - -constexpr uint32_t kBrandAv02 = MP4_FOUR_CHAR_CODE('a', 'v', '0', '2'); -constexpr uint32_t kBrandIso6 = MP4_FOUR_CHAR_CODE('i', 's', 'o', '6'); -constexpr uint32_t kAtomTypeAv2C = MP4_FOUR_CHAR_CODE('a', 'v', '2', 'C'); -constexpr uint32_t kSampleEntryTypeAv02 = MP4_FOUR_CHAR_CODE('a', 'v', '0', '2'); - -// Allocate an MP4Handle and copy bytes into it. -MP4Handle make_handle(const void* data, size_t size) { - MP4Handle h = nullptr; - if (MP4NewHandle(static_cast(size), &h) != MP4NoErr || !h) return nullptr; - if (size > 0) std::memcpy(*h, data, size); - return h; -} - -} // namespace - -Mp4Writer::Mp4Writer() { - // Profile/level fields are MPEG-4 systems-era and irrelevant for AV2 video; - // 0xff means "no profile" per libisomedia conventions. - MP4Err err = MP4NewMovie(&movie_, /*initialODID=*/1, - /*OD*/ 0xff, /*scene*/ 0xff, - /*audio*/ 0xff, /*visual*/ 0xff, - /*graphics*/ 0xff); - if (err != MP4NoErr) { - spdlog::error("MP4NewMovie failed (err={})", err); - movie_ = nullptr; - return; - } - - ISOSetMovieBrand(movie_, kBrandAv02, /*minor=*/0); - ISOSetMovieCompatibleBrand(movie_, kBrandAv02); - ISOSetMovieCompatibleBrand(movie_, kBrandIso6); -} - -Mp4Writer::~Mp4Writer() { - if (sample_entry_) MP4DisposeHandle(sample_entry_); - if (movie_) MP4DisposeMovie(movie_); -} - -bool Mp4Writer::add_video_track(uint32_t timescale, uint16_t width, uint16_t height, - const AV2CodecConfigurationBox& av2c) { - if (!movie_) return false; - - MP4Err err = MP4NewMovieTrack(movie_, MP4NewTrackIsVisual, &track_); - if (err != MP4NoErr || !track_) { - spdlog::error("MP4NewMovieTrack failed (err={})", err); - return false; - } - - err = MP4NewTrackMedia(track_, &media_, MP4VisualHandlerType, timescale, /*dataRef=*/nullptr); - if (err != MP4NoErr || !media_) { - spdlog::error("MP4NewTrackMedia failed (err={})", err); - return false; - } - - // Build the av2C extension atom from the box bytes. - std::vector av2c_bytes = av2c.serialize(); - if (av2c_bytes.empty()) { - spdlog::error("av2C serialization produced empty payload"); - return false; - } - - MP4Handle av2c_payload = make_handle(av2c_bytes.data(), av2c_bytes.size()); - if (!av2c_payload) { - spdlog::error("MP4NewHandle(av2C payload) failed"); - return false; - } - - MP4GenericAtom av2c_atom = nullptr; - err = MP4NewForeignAtom(&av2c_atom, kAtomTypeAv2C, av2c_payload); - // Ownership of the payload bytes transfers to the atom on success. - // (libisomedia takes a copy; we still dispose ours to be safe — but only - // if the call succeeded with a different owner, otherwise we leak.) - if (err != MP4NoErr || !av2c_atom) { - spdlog::error("MP4NewForeignAtom('av2C') failed (err={})", err); - MP4DisposeHandle(av2c_payload); - return false; - } - - // Create the sample entry and attach the av2C atom. - err = MP4NewHandle(0, &sample_entry_); - if (err != MP4NoErr || !sample_entry_) { - spdlog::error("MP4NewHandle(sample_entry) failed (err={})", err); - return false; - } - - err = ISONewGeneralSampleDescription(track_, sample_entry_, /*dataRefIdx=*/1, - kSampleEntryTypeAv02, av2c_atom); - if (err != MP4NoErr) { - spdlog::error("ISONewGeneralSampleDescription failed (err={})", err); - return false; - } - - err = ISOSetSampleDescriptionDimensions(sample_entry_, width, height); - if (err != MP4NoErr) { - spdlog::error("ISOSetSampleDescriptionDimensions failed (err={})", err); - return false; - } - - return true; -} - -bool Mp4Writer::add_sample(const std::vector& bytes, uint32_t duration, bool is_sync) { - if (!media_ || !sample_entry_) return false; - pending_sizes_.push_back(static_cast(bytes.size())); - pending_data_.insert(pending_data_.end(), bytes.begin(), bytes.end()); - pending_durations_.push_back(duration); - if (is_sync) { - // 1-based index relative to this chunk. - pending_sync_indices_.push_back(static_cast(pending_sizes_.size())); - } - return true; -} - -bool Mp4Writer::flush_chunk() { - if (pending_sizes_.empty()) return true; - if (!media_) return false; - - uint32_t n = static_cast(pending_sizes_.size()); - - MP4Handle data = make_handle(pending_data_.data(), pending_data_.size()); - MP4Handle sizes = make_handle(pending_sizes_.data(), n * sizeof(uint32_t)); - MP4Handle durations = make_handle(pending_durations_.data(), n * sizeof(uint32_t)); - MP4Handle sync = pending_sync_indices_.empty() - ? nullptr - : make_handle(pending_sync_indices_.data(), - pending_sync_indices_.size() * sizeof(uint32_t)); - - // First chunk passes sample_entry_; later chunks pass NULL to re-use it. - MP4Handle entry_for_call = first_chunk_ ? sample_entry_ : nullptr; - - MP4Err err = MP4AddMediaSamples(media_, data, n, durations, sizes, entry_for_call, - /*decodingOffsetsH=*/nullptr, sync); - - MP4DisposeHandle(data); - MP4DisposeHandle(sizes); - MP4DisposeHandle(durations); - if (sync) MP4DisposeHandle(sync); - - if (err != MP4NoErr) { - spdlog::error("MP4AddMediaSamples failed (err={}, samples={})", err, n); - return false; - } - - first_chunk_ = false; - pending_data_.clear(); - pending_sizes_.clear(); - pending_durations_.clear(); - pending_sync_indices_.clear(); - spdlog::debug("Flushed chunk: {} samples", n); - return true; -} - -bool Mp4Writer::finalize(const std::string& output_path) { - if (!movie_) return false; - if (!flush_chunk()) return false; - - MP4Err err = MP4WriteMovieToFile(movie_, output_path.c_str()); - if (err != MP4NoErr) { - spdlog::error("MP4WriteMovieToFile failed (err={})", err); - return false; - } - spdlog::info("Wrote {}", output_path); - return true; -} - -} // namespace av2_obu diff --git a/apps/av2_obu_packager/mp4_writer.h b/apps/av2_obu_packager/mp4_writer.h deleted file mode 100644 index 9870681..0000000 --- a/apps/av2_obu_packager/mp4_writer.h +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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 -#include -#include - -// libisomedia -#include -#include - -#include "av2_codec_config.h" - -namespace av2_obu { - -// Mp4Writer wraps libisomedia to produce an AV2-in-ISOBMFF .mp4. Owns the -class Mp4Writer { -public: - Mp4Writer(); - ~Mp4Writer(); - - Mp4Writer(const Mp4Writer&) = delete; - Mp4Writer& operator=(const Mp4Writer&) = delete; - - // Create one video track and install its sample entry from av2c. - bool add_video_track(uint32_t timescale, uint16_t width, uint16_t height, - const AV2CodecConfigurationBox& av2c); - - // Buffer a sample for the next chunk flush. duration is in track timescale units - // Bytes are copied into an internal buffer; caller's vector can be freed after the call returns. - bool add_sample(const std::vector& bytes, uint32_t duration, bool is_sync); - - // Flush all currently buffered samples to libisomedia (one MP4AddMediaSamples call = one chunk in the stsc box). - // No-op if buffer is empty. Auto-triggers by samples_per_chunk. - bool flush_chunk(); - - // Flush any pending samples and write the .mp4 to disk. - bool finalize(const std::string& output_path); - -private: - MP4Movie movie_ = nullptr; - MP4Track track_ = nullptr; - MP4Media media_ = nullptr; - MP4Handle sample_entry_ = nullptr; - - // First chunk passes sample_entry_ to MP4AddMediaSamples; subsequent chunks pass NULL to re-use it (per MP4Movies.h) - bool first_chunk_ = true; - - // Pending-chunk buffer. - std::vector pending_data_; // concatenated sample bytes - std::vector pending_sizes_; // per-sample byte sizes - std::vector pending_durations_; - std::vector pending_sync_indices_; // 1-based indices within this chunk -}; - -} // namespace av2_obu diff --git a/apps/av2_obu_packager/packaging_strategy.cpp b/apps/av2_obu_packager/packaging_strategy.cpp deleted file mode 100644 index f937d46..0000000 --- a/apps/av2_obu_packager/packaging_strategy.cpp +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 "packaging_strategy.h" - -#include - -namespace av2_obu { - -PackagingStrategy determine_strategy(const OBUParser::Statistics& stats, - const UserOptions& user_opts) { - PackagingStrategy s; - - // Pass-through user opts - s.drop_temporal_delimiters = user_opts.drop_temporal_delimiters; - s.samples_per_chunk = user_opts.samples_per_chunk; - s.start_tu = user_opts.start_tu; - s.num_samples = user_opts.num_samples; - - // Timing — CLI takes precedence today. CI timing_info() will land in Step 8. - s.frame_rate = user_opts.frame_rate; - s.timescale = static_cast(user_opts.frame_rate * 1000); - s.default_sample_duration = 1000; - s.timing_source = PackagingStrategy::TimingSource::kCli; - - // Sample entry mode: any SH change → multiple entries. - s.sample_entry_mode = stats.sequence_headers.has_changes - ? PackagingStrategy::SampleEntryMode::kMultiple - : PackagingStrategy::SampleEntryMode::kSingle; - - // any_non_monotonic and ctts wiring land in Step 5.3 — needs SH access - // beyond what Statistics currently exposes. - - spdlog::debug("Strategy: sample_entry_mode={}", - s.sample_entry_mode == PackagingStrategy::SampleEntryMode::kMultiple ? "kMultiple" - : "kSingle"); - spdlog::debug("Strategy: timescale={}, duration={}, source=CLI", s.timescale, - s.default_sample_duration); - spdlog::debug("Strategy: drop_TDs={}, samples_per_chunk={}", - s.drop_temporal_delimiters ? "yes" : "no", s.samples_per_chunk); - - return s; -} - -} // namespace av2_obu diff --git a/apps/av2_obu_packager/packaging_strategy.h b/apps/av2_obu_packager/packaging_strategy.h deleted file mode 100644 index 6b89522..0000000 --- a/apps/av2_obu_packager/packaging_strategy.h +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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 -#include - -#include - -namespace av2_obu { - -// FOURCC packed big-endian (matches ISOBMFF on-wire byte order: a is MSB). -inline constexpr uint32_t fourcc(char a, char b, char c, char d) { - return (static_cast(static_cast(a)) << 24) | - (static_cast(static_cast(b)) << 16) | - (static_cast(static_cast(c)) << 8) | - static_cast(static_cast(d)); -} - -// Packaging strategy: derived from bitstream analysis and user options. -struct PackagingStrategy { - enum class SampleEntryMode { - kSingle, // one sample entry for the whole file - kMultiple // new entry on each new SH (Step 6) - }; - - enum class TimingSource { - kCli, // --fps from CLI (highest precedence) - kCiTimingInfo, // CI OBU timing_info() (Step 8) - kDefault // 30 fps fallback - }; - - // === Pass-through / user-driven === - bool drop_temporal_delimiters = true; - - // === Sample entry (Step 6) === - SampleEntryMode sample_entry_mode = SampleEntryMode::kSingle; - - // === Timing === - double frame_rate = 30.0; - uint32_t timescale = 30000; - uint32_t default_sample_duration = 1000; - TimingSource timing_source = TimingSource::kDefault; - - // === ctts (Step 5.3) === - // Set true if any SH in the stream has monotonic_output_order_flag == 0. - // Triggers MP4UseSignedCompositionTimeOffsets in Mp4Writer. - bool any_non_monotonic = false; - - // === Chunking (Step 5.4) === - // Auto-flush trigger inside Mp4Writer::add_sample. Default ≈ fps. - uint32_t samples_per_chunk = 30; - - // === TU range selection (testing / trimming) === - uint32_t start_tu = 0; // 0-based TU index to start from - uint32_t num_samples = 0; // 0 = all (from start_tu to end) - - // === File-type / brands (Step 9) === - uint32_t major_brand = fourcc('a', 'v', '0', '2'); - std::vector compatible_brands = { - fourcc('a', 'v', '0', '2'), - fourcc('i', 's', 'o', '6'), - }; - - // === Fragmentation hooks (deferred — v2 / future) === - // Spec §CMAF will pin down what to put here. For now, defaults disable - // fragmentation entirely; Mp4Writer ignores these fields. - struct Fragmentation { - bool enabled = false; - uint32_t fragment_duration_ms = 0; // target fragment duration; 0 = unset - bool emit_sidx = false; // produce SegmentIndexBox for DASH - uint32_t subsegs_per_sidx = 0; // sidx layout - bool daisy_chain_sidx = false; // chained vs root sidx - } fragmentation; - - // === Encryption / CENC hooks (deferred — v2 / future) === - // Spec §CommonEncryption will pin down what to put here. For now, defaults - // disable encryption entirely; Mp4Writer ignores these fields. - struct Encryption { - bool enabled = false; - uint32_t scheme = 0; // 'cenc' / 'cbc1' / 'cens' / 'cbcs' / 'sve1' - std::vector kid; // 16-byte Key ID - uint32_t pattern_crypt_block_count = 0; // cbcs / cens pattern - uint32_t pattern_skip_block_count = 0; - // Key delivery, per-sample IVs, subsample mapping, PSSH boxes — out of - // strategy scope; would be supplied by an EncryptionStrategy / KeySource - // when CENC support lands. - } encryption; -}; - -// User options from CLI (raw input, no analysis applied). -struct UserOptions { - double frame_rate = 30.0; - bool drop_temporal_delimiters = true; - uint32_t samples_per_chunk = 30; - uint32_t start_tu = 0; - uint32_t num_samples = 0; -}; - -// Apply user options + analysis to produce a strategy. -PackagingStrategy determine_strategy(const OBUParser::Statistics& stats, - const UserOptions& user_opts); - -} // namespace av2_obu diff --git a/include/av2_obu/obus/content_interpretation_obu.h b/include/av2_obu/obus/content_interpretation_obu.h index ea309ed..73e0a91 100644 --- a/include/av2_obu/obus/content_interpretation_obu.h +++ b/include/av2_obu/obus/content_interpretation_obu.h @@ -34,6 +34,11 @@ class ContentInterpretationOBU : public BaseOBU { // Getters for parsed fields uint32_t get_scan_type_idc() const { return scan_type_idc_; } bool has_color_description() const { return color_description_present_flag_ != 0; } + uint32_t color_description_idc() const { return color_description_idc_; } + uint32_t color_primaries() const { return color_primaries_; } + uint32_t transfer_characteristics() const { return transfer_characteristics_; } + uint32_t matrix_coefficients() const { return matrix_coefficients_; } + uint32_t full_range_flag() const { return full_range_flag_; } bool has_chroma_sample_position() const { return chroma_sample_position_present_flag_ != 0; } bool has_aspect_ratio_info() const { return aspect_ratio_info_present_flag_ != 0; } bool has_timing_info() const { return timing_info_present_flag_ != 0; } diff --git a/src/obus/sequence_header_obu.cpp b/src/obus/sequence_header_obu.cpp index 3491745..850e27e 100644 --- a/src/obus/sequence_header_obu.cpp +++ b/src/obus/sequence_header_obu.cpp @@ -24,9 +24,16 @@ bool SequenceHeaderOBU::parse_payload(std::ifstream& ifs) { spdlog::debug("Parsing AV2 sequence header payload ({} bytes)", position_.payload_size); + // Snapshot the raw payload bytes so callers (e.g. OBUParser::get_statistics) can + // detect SH-byte changes across the stream by direct comparison. + raw_payload_.resize(position_.payload_size); + if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { + spdlog::error("Failed to read sequence header payload"); + return false; + } + try { - // Create bitstream reader from the payload - BitstreamReader br(ifs, position_.payload_size); + BitstreamReader br(raw_payload_); // Parse using the full AV2 sequence header parser if (!seq_header_.parse(br)) { From ebde4b53134849810214062fcd865b2ac0b4cccd Mon Sep 17 00:00:00 2001 From: Dimitri Podborski Date: Mon, 22 Jun 2026 15:11:31 -0700 Subject: [PATCH 07/10] initial muxer --- apps/av2_mux/CMakeLists.txt | 25 +++ apps/av2_mux/README.md | 35 ++++ apps/av2_mux/av2_codec_config.cpp | 108 ++++++++++ apps/av2_mux/av2_codec_config.h | 65 ++++++ apps/av2_mux/av2_muxer.cpp | 288 ++++++++++++++++++++++++++ apps/av2_mux/av2_muxer.h | 59 ++++++ apps/av2_mux/colr_info.cpp | 121 +++++++++++ apps/av2_mux/colr_info.h | 40 ++++ apps/av2_mux/display_order_lifter.cpp | 47 +++++ apps/av2_mux/display_order_lifter.h | 60 ++++++ apps/av2_mux/frame_header_helpers.cpp | 79 +++++++ apps/av2_mux/frame_header_helpers.h | 26 +++ apps/av2_mux/main.cpp | 107 ++++++++++ apps/av2_mux/mp4_writer.cpp | 253 ++++++++++++++++++++++ apps/av2_mux/mp4_writer.h | 74 +++++++ apps/av2_mux/mux_log.cpp | 26 +++ apps/av2_mux/mux_log.h | 30 +++ apps/av2_mux/mux_strategy.cpp | 109 ++++++++++ apps/av2_mux/mux_strategy.h | 97 +++++++++ 19 files changed, 1649 insertions(+) create mode 100644 apps/av2_mux/CMakeLists.txt create mode 100644 apps/av2_mux/README.md create mode 100644 apps/av2_mux/av2_codec_config.cpp create mode 100644 apps/av2_mux/av2_codec_config.h create mode 100644 apps/av2_mux/av2_muxer.cpp create mode 100644 apps/av2_mux/av2_muxer.h create mode 100644 apps/av2_mux/colr_info.cpp create mode 100644 apps/av2_mux/colr_info.h create mode 100644 apps/av2_mux/display_order_lifter.cpp create mode 100644 apps/av2_mux/display_order_lifter.h create mode 100644 apps/av2_mux/frame_header_helpers.cpp create mode 100644 apps/av2_mux/frame_header_helpers.h create mode 100644 apps/av2_mux/main.cpp create mode 100644 apps/av2_mux/mp4_writer.cpp create mode 100644 apps/av2_mux/mp4_writer.h create mode 100644 apps/av2_mux/mux_log.cpp create mode 100644 apps/av2_mux/mux_log.h create mode 100644 apps/av2_mux/mux_strategy.cpp create mode 100644 apps/av2_mux/mux_strategy.h diff --git a/apps/av2_mux/CMakeLists.txt b/apps/av2_mux/CMakeLists.txt new file mode 100644 index 0000000..bb0555a --- /dev/null +++ b/apps/av2_mux/CMakeLists.txt @@ -0,0 +1,25 @@ +add_executable(av2_mux + main.cpp + mux_strategy.cpp + av2_codec_config.cpp + mp4_writer.cpp + av2_muxer.cpp + display_order_lifter.cpp + frame_header_helpers.cpp + colr_info.cpp + mux_log.cpp +) + +target_link_libraries(av2_mux + PRIVATE + av2_obu # Brings spdlog and nlohmann_json transitively + CLI11::CLI11 + libisomediafile # ISOBMFF reference SW from MPEG +) + +target_compile_options(av2_mux PRIVATE + $<$:-Wall -Wextra -Wpedantic> + $<$:/W4> +) + +install(TARGETS av2_mux RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/apps/av2_mux/README.md b/apps/av2_mux/README.md new file mode 100644 index 0000000..a06a736 --- /dev/null +++ b/apps/av2_mux/README.md @@ -0,0 +1,35 @@ +# av2_mux + +Mux AV2 elementary bitstreams into MP4/ISOBMFF containers. + +## Build + +Requires enabling the container tools during CMake configuration: + +```bash +cmake -S . -B mybuild -DBUILD_CONTAINER_TOOLS=ON +cmake --build mybuild -j +``` + +This fetches MPEG's libisomedia reference implementation automatically. + +## Usage + +```bash +# Mux an AV2 elementary stream into MP4 +./av2_mux input.obu -o output.mp4 + +# Override frame rate +./av2_mux input.obu -o output.mp4 --fps 60 + +# Force colour metadata when the bitstream has no CI/LCR/OPS color info +./av2_mux input.obu -o output.mp4 --colr-override 1:13:1:0 + +# Control samples-per-chunk in stsc +./av2_mux input.obu -o output.mp4 --samples-per-chunk 60 + +# Verbose mode prints every meaningful muxing step +./av2_mux input.obu -o output.mp4 -v +``` + +See `--help` for the full flag list. diff --git a/apps/av2_mux/av2_codec_config.cpp b/apps/av2_mux/av2_codec_config.cpp new file mode 100644 index 0000000..6c1e9e5 --- /dev/null +++ b/apps/av2_mux/av2_codec_config.cpp @@ -0,0 +1,108 @@ +/* + * 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_codec_config.h" +#include "mux_log.h" + +#include + +#include + +namespace av2_obu { + +AV2CodecConfigurationBox& AV2CodecConfigurationBox::set_from_sequence_header( + const AV2SequenceHeader& sh) { + configurationVersion = 1; + seq_profile_idc = sh.seq_profile_idc; + seq_level_idx = sh.seq_level_idx; + seq_tier = sh.seq_tier; + chroma_format_idc = static_cast(sh.chroma_format_idc); + bit_depth_idc = static_cast(sh.bit_depth_idc); + monotonic_output_order_flag = sh.monotonic_output_order_flag; + still_picture = sh.still_picture; + film_grain_params_present = sh.film_grain_params_present; + seq_initial_display_delay_present_flag = sh.seq_initial_display_delay_present_flag; + seq_initial_display_delay_minus_1 = sh.seq_initial_display_delay_minus_1; + max_frame_width_minus_1 = static_cast(sh.max_frame_width_minus_1); + max_frame_height_minus_1 = static_cast(sh.max_frame_height_minus_1); + return *this; +} + +bool AV2CodecConfigurationBox::append_config_obu(const BaseOBU& obu) { + if (!input_file_) { + MUX_ERROR("append_config_obu: no input file bound"); + return false; + } + + const auto& pos = obu.position(); + size_t total = pos.size_field_len + pos.header_len + pos.payload_size; + + auto saved_pos = input_file_->tellg(); + + input_file_->clear(); + input_file_->seekg(pos.start_pos); + if (!*input_file_) { + MUX_ERROR("Failed to seek to OBU at offset {}", static_cast(pos.start_pos)); + input_file_->clear(); + input_file_->seekg(saved_pos); + return false; + } + + ConfigEntry entry; + entry.obu = &obu; + entry.bytes.resize(total); + if (!input_file_->read(reinterpret_cast(entry.bytes.data()), + static_cast(total))) { + MUX_ERROR("Failed to read {} bytes for OBU at offset {}", total, + static_cast(pos.start_pos)); + input_file_->clear(); + input_file_->seekg(saved_pos); + return false; + } + + config_obus.push_back(std::move(entry)); + input_file_->clear(); + input_file_->seekg(saved_pos); + return true; +} + +std::vector AV2CodecConfigurationBox::serialize() const { + BitstreamWriter w; + w.write_bits(configurationVersion, 8); + w.write_bits(seq_profile_idc, 5); + w.write_bits(seq_level_idx, 5); + w.write_bits(seq_tier, 1); + w.write_bits(chroma_format_idc, 3); + w.write_bits(bit_depth_idc, 3); + w.write_bits(monotonic_output_order_flag, 1); + w.write_bits(still_picture, 1); + w.write_bits(film_grain_params_present, 1); + w.write_bits(seq_initial_display_delay_present_flag, 1); + w.write_bits(seq_initial_display_delay_minus_1, 4); + w.write_bits(0, 7); // reserved1 + w.write_bits(max_frame_width_minus_1, 16); + w.write_bits(max_frame_height_minus_1, 16); + + std::vector out = w.take(); + if (out.size() != 9) { + MUX_ERROR("av2C prefix size = {} bytes (expected 9)", out.size()); + return {}; + } + + for (const auto& entry : config_obus) { + out.insert(out.end(), entry.bytes.begin(), entry.bytes.end()); + } + MUX_DEBUG("Serialized av2C: {} bytes (9 prefix + {} configOBUs)", out.size(), + config_obus.size()); + return out; +} + +} // namespace av2_obu diff --git a/apps/av2_mux/av2_codec_config.h b/apps/av2_mux/av2_codec_config.h new file mode 100644 index 0000000..4dd877b --- /dev/null +++ b/apps/av2_mux/av2_codec_config.h @@ -0,0 +1,65 @@ +/* + * 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 +#include +#include + +#include +#include + +namespace av2_obu { + +// AV2CodecConfigurationBox ('av2C'): the codec config box for AV2 in ISOBMFF. +struct AV2CodecConfigurationBox { + uint32_t configurationVersion = 1; + + uint32_t seq_profile_idc = 0; // 5 bits + uint32_t seq_level_idx = 0; // 5 bits + uint32_t seq_tier = 0; // 1 bit + + uint32_t chroma_format_idc = 0; // 3 bits + uint32_t bit_depth_idc = 0; // 3 bits + + uint32_t monotonic_output_order_flag = 0; // 1 bit + uint32_t still_picture = 0; // 1 bit + uint32_t film_grain_params_present = 0; // 1 bit + uint32_t seq_initial_display_delay_present_flag = 0; // 1 bit + uint32_t seq_initial_display_delay_minus_1 = 0; // 4 bits + + uint32_t max_frame_width_minus_1 = 0; // 16 bits + uint32_t max_frame_height_minus_1 = 0; // 16 bits + + // One entry per OBU in configOBUs[]: keeps the parsed OBU pointer plus its + // pre-read Annex B-framed bytes. OBU pointer is non-owning. + struct ConfigEntry { + const BaseOBU* obu = nullptr; + std::vector bytes; + }; + + // Per av2-isobmff the first entry must be the active Sequence Header OBU. + std::vector config_obus; + + AV2CodecConfigurationBox() = default; + explicit AV2CodecConfigurationBox(std::ifstream& input_file) : input_file_(&input_file) {} + void bind_input_file(std::ifstream& input_file) { input_file_ = &input_file; } + + AV2CodecConfigurationBox& set_from_sequence_header(const AV2SequenceHeader& sh); + bool append_config_obu(const BaseOBU& obu); + std::vector serialize() const; + +private: + std::ifstream* input_file_ = nullptr; +}; + +} // namespace av2_obu diff --git a/apps/av2_mux/av2_muxer.cpp b/apps/av2_mux/av2_muxer.cpp new file mode 100644 index 0000000..1044599 --- /dev/null +++ b/apps/av2_mux/av2_muxer.cpp @@ -0,0 +1,288 @@ +/* + * 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_muxer.h" +#include "mux_log.h" + +#include + +#include +#include + +#include "av2_codec_config.h" +#include "colr_info.h" +#include "frame_header_helpers.h" +#include +#include +#include +#include +#include + +namespace av2_obu { + +Av2Muxer::Av2Muxer(const std::string& input_path, const MuxStrategy& strategy) + : input_path_(input_path), strategy_(strategy) {} + +bool Av2Muxer::package(const OBUParser& parser, const std::string& output_path) { + input_ifs_.open(input_path_, std::ios::binary); + if (!input_ifs_) { + MUX_ERROR("Failed to open {} for byte extraction", input_path_); + return false; + } + + if (!setup_video_track(parser)) return false; + + const auto& tus = parser.temporal_units(); + if (strategy_.start_tu >= tus.size()) { + MUX_ERROR("--start-tu {} out of range (have {} TUs)", strategy_.start_tu, tus.size()); + return false; + } + if (strategy_.start_tu > 0 && !tus[strategy_.start_tu].is_sync_sample()) { + MUX_WARN("--start-tu {} is not a sync sample; output may not be cleanly decodable", + strategy_.start_tu); + } + const uint32_t end_tu = + strategy_.num_samples == 0 + ? static_cast(tus.size()) + : std::min(strategy_.start_tu + strategy_.num_samples, + static_cast(tus.size())); + MUX_DEBUG("Writing TUs [{}, {}) of {} total", strategy_.start_tu, end_tu, tus.size()); + + // ISOBMFF §8.6.1.3 forbids ctts when CTS == DTS for every sample, so emit it + // only after computing offsets and observing at least one non-zero. + std::vector ctts_offsets = compute_composition_offsets(tus, strategy_.start_tu, end_tu); + bool need_ctts = std::any_of(ctts_offsets.begin(), ctts_offsets.end(), + [](int32_t v) { return v != 0; }); + if (need_ctts) { + MUX_DEBUG("ctts: enabling signed composition offsets ({} non-zero)", + std::count_if(ctts_offsets.begin(), ctts_offsets.end(), + [](int32_t v) { return v != 0; })); + if (!writer_.enable_signed_composition_offsets()) return false; + } else if (doh_lifter_) { + MUX_DEBUG("ctts: omitted (non-monotonic SH but all CTS == DTS)"); + } else { + MUX_DEBUG("ctts: omitted (monotonic_output_order_flag=1)"); + } + + uint32_t num_samples = 0; + uint32_t sync_samples = 0; + for (uint32_t i = strategy_.start_tu; i < end_tu; ++i) { + int32_t ctts = need_ctts ? ctts_offsets[i - strategy_.start_tu] : 0; + if (!write_tu(tus[i], ctts)) { + MUX_ERROR("Failed to write TU {}", i); + return false; + } + ++num_samples; + if (tus[i].is_sync_sample()) ++sync_samples; + } + + if (!writer_.finalize(output_path)) return false; + + std::string ctts_str = need_ctts ? "ctts=present" : "ctts=absent"; + std::string colr_str = + last_colr_ ? fmt::format("colr=nclx {}/{}/{}/{}", last_colr_->colour_primaries, + last_colr_->transfer_characteristics, + last_colr_->matrix_coefficients, + last_colr_->full_range_flag) + : "colr=absent"; + MUX_INFO("Wrote {} ({} samples, {} sync, {:.3f} fps, timescale={}, {}, {})", output_path, + num_samples, sync_samples, strategy_.frame_rate, strategy_.timescale, ctts_str, + colr_str); + return true; +} + +bool Av2Muxer::setup_video_track(const OBUParser& parser) { + const SequenceHeaderOBU* first_sh = find_first_sequence_header(parser); + if (!first_sh) { + MUX_ERROR("No Sequence Header OBU found"); + return false; + } + const AV2SequenceHeader& sh = first_sh->sequence_header(); + + // Multi-sample-entry on SH change is not implemented yet; warn so users know + // later SHs will be silently lost from av2C. + const auto& sh_stats = parser.get_statistics().sequence_headers; + if (sh_stats.has_changes) { + MUX_WARN( + "Bitstream contains {} sequence header(s) with differing bytes (changes at OBU indices: " + "{}). Multi-sample-entry support is not implemented; only the first SH (seq_header_id={}) " + "will be carried in av2C. Samples that depend on a later SH may decode incorrectly.", + sh_stats.count, [&] { + std::string out; + for (size_t i = 0; i < sh_stats.change_positions.size(); ++i) { + if (i) out += ", "; + out += std::to_string(sh_stats.change_positions[i]); + } + return out; + }(), + sh.seq_header_id); + } + + strategy_.any_non_monotonic = (sh.monotonic_output_order_flag == 0); + if (strategy_.any_non_monotonic) { + if (!check_doh_lifter_supported(parser)) return false; + doh_lifter_ = std::make_unique(sh.inter_config.OrderHintBits); + } + + AV2CodecConfigurationBox av2c(input_ifs_); + av2c.set_from_sequence_header(sh); + if (!av2c.append_config_obu(*first_sh)) { + MUX_ERROR("Failed to append SH to av2C configOBUs"); + return false; + } + + uint32_t w_full = static_cast(sh.max_frame_width_minus_1) + 1; + uint32_t h_full = static_cast(sh.max_frame_height_minus_1) + 1; + if (w_full > 0xFFFF || h_full > 0xFFFF) { + MUX_WARN("Frame dimensions {}x{} exceed 16-bit av2C field; truncating", w_full, h_full); + } + uint16_t width = static_cast(w_full & 0xFFFF); + uint16_t height = static_cast(h_full & 0xFFFF); + + if (!writer_.add_video_track(strategy_.timescale, width, height, av2c)) return false; + writer_.set_samples_per_chunk(strategy_.samples_per_chunk); + + std::optional colr = + strategy_.colr_override ? strategy_.colr_override : extract_colr_info(parser); + if (colr) { + if (!writer_.add_colr_nclx(*colr)) return false; + MUX_DEBUG("colr/nclx ({}): cp={} tc={} mc={} full_range={}", + strategy_.colr_override ? "override" : "extracted from bitstream", + colr->colour_primaries, colr->transfer_characteristics, colr->matrix_coefficients, + colr->full_range_flag); + } else { + MUX_DEBUG("No CI/LCR/OPS color info found and no --colr-override; omitting colr box"); + } + last_colr_ = colr; + + return true; +} + +bool Av2Muxer::check_doh_lifter_supported(const OBUParser& parser) { + // The simple modular-unwrap lifter is only correct for a narrow class of + // streams; refuse anything outside it rather than silently producing bad CTS. + // See display_order_lifter.h for the full list of preconditions. + const auto stats = parser.get_statistics(); + if (!stats.layers.is_single_layer) { + MUX_ERROR("Non-monotonic stream uses {} mlayer(s) and {} xlayer(s); the v1 DOH lifter " + "supports single-layer streams only. Refusing to produce wrong ctts.", + stats.layers.mlayer_ids.size(), stats.layers.xlayer_ids.size()); + return false; + } + uint32_t clk_count = 0; + for (const auto& obu : parser.obus()) { + if (obu->type() == OBUType::CLK) ++clk_count; + } + if (clk_count > 1) { + MUX_ERROR("Non-monotonic stream contains {} CLK frames (multi-CVS); the v1 DOH lifter " + "does not reset across CVS boundaries.", clk_count); + return false; + } + for (const auto& obu : parser.obus()) { + if (obu->type() == OBUType::BRIDGE_FRAME) { + MUX_ERROR("Non-monotonic stream contains a Bridge frame at OBU offset {}; its " + "DispOrderHint is RefOrderHint[bridge_frame_ref_idx], which the v1 lifter " + "cannot resolve without reference-state tracking.", + static_cast(obu->position().start_pos)); + return false; + } + const FrameHeaderInfo* fh = frame_header_of(obu.get()); + if (!fh) continue; + if (is_sef(obu->type()) && fh->derive_sef_order_hint == 1) { + MUX_ERROR("Non-monotonic stream contains a derived-DOH SEF at OBU offset {} " + "(derive_sef_order_hint=1); its order_hint comes from the reference " + "buffer, which the v1 lifter cannot resolve.", + static_cast(obu->position().start_pos)); + return false; + } + } + return true; +} + +bool Av2Muxer::write_tu(const TemporalUnit& tu, int32_t composition_offset) { + std::vector bytes; + if (!assemble_sample_bytes(tu, bytes)) return false; + return writer_.add_sample(bytes, strategy_.default_sample_duration, tu.is_sync_sample(), + composition_offset); +} + +std::vector Av2Muxer::compute_composition_offsets( + const std::vector& tus, uint32_t start, uint32_t end) { + std::vector offsets; + if (!doh_lifter_) return offsets; + offsets.reserve(end - start); + const int64_t dur = static_cast(strategy_.default_sample_duration); + for (uint32_t i = start; i < end; ++i) { + int32_t off = 0; + if (const BaseOBU* out = find_output_frame(tus[i])) { + if (const FrameHeaderInfo* fh = frame_header_of(out)) { + int64_t lifted_doh = doh_lifter_->lift(fh->order_hint); + int64_t cts = lifted_doh * dur; + int64_t dts = static_cast(i - start) * dur; + off = static_cast(cts - dts); + } + } + // Hidden-only TUs: no output picture, leave CTS == DTS. + offsets.push_back(off); + } + return offsets; +} + +bool Av2Muxer::assemble_sample_bytes(const TemporalUnit& tu, std::vector& out) { + out.clear(); + const bool keep_td = !strategy_.drop_temporal_delimiters; + for (const auto* obu : tu.sample_obus(keep_td)) { + const auto& pos = obu->position(); + size_t total = pos.size_field_len + pos.header_len + pos.payload_size; + size_t prev = out.size(); + out.resize(prev + total); + + input_ifs_.clear(); + input_ifs_.seekg(pos.start_pos); + if (!input_ifs_.read(reinterpret_cast(out.data() + prev), + static_cast(total))) { + MUX_ERROR("Failed to read {} bytes for OBU at offset {}", total, + static_cast(pos.start_pos)); + return false; + } + } + return true; +} + +const SequenceHeaderOBU* Av2Muxer::find_first_sequence_header(const OBUParser& parser) { + for (const auto& obu : parser.obus()) { + if (obu->type() == OBUType::SEQUENCE_HEADER) { + if (auto* sh = dynamic_cast(obu.get())) return sh; + } + } + return nullptr; +} + +void log_stream_summary(const OBUParser& parser) { + const auto stats = parser.get_statistics(); + MUX_DEBUG("Stream summary:"); + MUX_DEBUG(" OBUs: {} ({} bytes)", stats.total_obus, stats.total_bytes); + MUX_DEBUG(" Sequence headers: {}{}", stats.sequence_headers.count, + stats.sequence_headers.has_changes ? " (with changes)" : " (identical)"); + MUX_DEBUG(" TDs: {}", stats.temporal.td_count); + MUX_DEBUG(" Frames: {} ({} key)", stats.frames.total_frames, stats.frames.keyframe_count); + MUX_DEBUG(" Single layer: {}", stats.layers.is_single_layer ? "yes" : "no"); + MUX_DEBUG(" CI timing_info: {}", + stats.content_interpretation.has_timing_info ? "present" : "absent"); + + size_t sync_count = 0; + for (const auto& tu : parser.temporal_units()) { + if (tu.is_sync_sample()) sync_count++; + } + MUX_DEBUG(" TUs: {} ({} sync samples)", parser.temporal_units().size(), sync_count); +} + +} // namespace av2_obu diff --git a/apps/av2_mux/av2_muxer.h b/apps/av2_mux/av2_muxer.h new file mode 100644 index 0000000..64ab3a1 --- /dev/null +++ b/apps/av2_mux/av2_muxer.h @@ -0,0 +1,59 @@ +/* + * 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 +#include +#include +#include +#include + +#include "colr_info.h" +#include "display_order_lifter.h" +#include "mp4_writer.h" +#include "mux_strategy.h" + +namespace av2_obu { + +class OBUParser; +class TemporalUnit; +class SequenceHeaderOBU; +class BaseOBU; + +// Drives mux of one elementary AV2 bitstream into one ISOBMFF .mp4. +class Av2Muxer { +public: + Av2Muxer(const std::string& input_path, const MuxStrategy& strategy); + + bool package(const OBUParser& parser, const std::string& output_path); + +private: + bool setup_video_track(const OBUParser& parser); + bool check_doh_lifter_supported(const OBUParser& parser); + bool write_tu(const TemporalUnit& tu, int32_t composition_offset); + bool assemble_sample_bytes(const TemporalUnit& tu, std::vector& out); + std::vector compute_composition_offsets(const std::vector& tus, + uint32_t start, uint32_t end); + + static const SequenceHeaderOBU* find_first_sequence_header(const OBUParser& parser); + + std::string input_path_; + MuxStrategy strategy_; + std::ifstream input_ifs_; + Mp4Writer writer_; + std::unique_ptr doh_lifter_; + std::optional last_colr_; +}; + +void log_stream_summary(const class OBUParser& parser); + +} // namespace av2_obu diff --git a/apps/av2_mux/colr_info.cpp b/apps/av2_mux/colr_info.cpp new file mode 100644 index 0000000..f7480c6 --- /dev/null +++ b/apps/av2_mux/colr_info.cpp @@ -0,0 +1,121 @@ +/* + * 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 "colr_info.h" + +#include +#include + +#include +#include +#include +#include +#include + +namespace av2_obu { + +namespace { + +// We can collect a few well-known CICP presets here +const std::map& named_colr_profiles() { + static const std::map kProfiles = { + {"bt709", ColrInfo{1, 1, 1, 0}}, // HD SDR, video range + {"bt601-ntsc", ColrInfo{6, 6, 6, 0}}, // SD NTSC (SMPTE 170M), video range + {"bt2020-pq", ColrInfo{9, 16, 9, 0}}, // HDR10: BT.2020 + PQ + BT.2020 NCL + {"bt2020-hlg", ColrInfo{9, 18, 9, 0}}, // HLG: BT.2020 + ARIB STD-B67 + BT.2020 NCL + }; + return kProfiles; +} + +ColrInfo make(uint32_t cp, uint32_t tc, uint32_t mc, uint32_t fr) { + return ColrInfo{cp, tc, mc, fr}; +} + +std::optional from_ci(const OBUParser& parser) { + for (const auto& obu : parser.obus()) { + if (obu->type() != OBUType::CONTENT_INTERPRETATION) continue; + auto* ci = dynamic_cast(obu.get()); + if (!ci || !ci->has_color_description()) continue; + return make(ci->color_primaries(), ci->transfer_characteristics(), ci->matrix_coefficients(), + ci->full_range_flag()); + } + return std::nullopt; +} + +std::optional from_lcr(const OBUParser& parser) { + std::optional best; + uint32_t best_xid = std::numeric_limits::max(); + for (const auto& obu : parser.obus()) { + if (obu->type() != OBUType::LAYER_CONFIGURATION_RECORD) continue; + auto* lcr = dynamic_cast(obu.get()); + if (!lcr) continue; + const auto& gp = lcr->global_payloads(); + const auto& ids = lcr->xlayer_ids(); + for (size_t i = 0; i < gp.size() && i < ids.size(); ++i) { + const auto& xi = gp[i].xlayer_info; + if (!xi.color_info_present) continue; + if (ids[i] < best_xid) { + best_xid = ids[i]; + best = make(xi.color_info.color_primaries, xi.color_info.transfer_characteristics, + xi.color_info.matrix_coefficients, xi.color_info.full_range_flag); + } + } + if (!best && lcr->local_xlayer_info().color_info_present) { + const auto& ci = lcr->local_xlayer_info().color_info; + best = make(ci.color_primaries, ci.transfer_characteristics, ci.matrix_coefficients, + ci.full_range_flag); + } + } + return best; +} + +std::optional from_ops(const OBUParser& parser) { + for (const auto& obu : parser.obus()) { + if (obu->type() != OBUType::OPERATING_POINT_SET) continue; + auto* ops = dynamic_cast(obu.get()); + if (!ops) continue; + for (const auto& op : ops->operating_points()) { + if (!op.has_color_info) continue; + return make(op.color_info.color_primaries, op.color_info.transfer_characteristics, + op.color_info.matrix_coefficients, op.color_info.full_range_flag); + } + } + return std::nullopt; +} + +} // namespace + +std::optional extract_colr_info(const OBUParser& parser) { + if (auto v = from_ci(parser)) return v; + if (auto v = from_lcr(parser)) return v; + if (auto v = from_ops(parser)) return v; + return std::nullopt; +} + +std::optional colr_profile_by_name(const std::string& name) { + const auto& m = named_colr_profiles(); + if (auto it = m.find(name); it != m.end()) return it->second; + return std::nullopt; +} + +std::string supported_colr_profile_names() { + std::string out; + bool first = true; + for (const auto& kv : named_colr_profiles()) { + if (!first) out += ", "; + out += kv.first; + first = false; + } + return out; +} + +} // namespace av2_obu + diff --git a/apps/av2_mux/colr_info.h b/apps/av2_mux/colr_info.h new file mode 100644 index 0000000..1ca8dfe --- /dev/null +++ b/apps/av2_mux/colr_info.h @@ -0,0 +1,40 @@ +/* + * 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 +#include +#include + +namespace av2_obu { + +class OBUParser; + +// CICP color metadata for the ISOBMFF 'colr' / nclx box. +struct ColrInfo { + uint32_t colour_primaries; + uint32_t transfer_characteristics; + uint32_t matrix_coefficients; + uint32_t full_range_flag; // 0 or 1 +}; + +// Search order: CI OBU > LCR (lowest xlayer with color info) > OPS. +// Returns nullopt if no source carries color metadata. +std::optional extract_colr_info(const OBUParser& parser); + +// Resolves a CICP profile name (e.g. "bt709") to its ColrInfo. nullopt for unknown names +std::optional colr_profile_by_name(const std::string& name); + +// Comma-separated list of profile names recognised by colr_profile_by_name(). +std::string supported_colr_profile_names(); + +} // namespace av2_obu diff --git a/apps/av2_mux/display_order_lifter.cpp b/apps/av2_mux/display_order_lifter.cpp new file mode 100644 index 0000000..ea94c22 --- /dev/null +++ b/apps/av2_mux/display_order_lifter.cpp @@ -0,0 +1,47 @@ +/* + * 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 "display_order_lifter.h" + +namespace av2_obu { + +DisplayOrderLifter::DisplayOrderLifter(uint32_t order_hint_bits) { + mod_ = (order_hint_bits == 0) ? 1 : (int64_t{1} << order_hint_bits); + half_ = mod_ >> 1; +} + +void DisplayOrderLifter::reset_cvs() { + // Wrap state is implicit in max_seen_; nothing to reset across CVS joins. +} + +int64_t DisplayOrderLifter::lift(uint32_t order_hint_lsbs) { + int64_t oh = static_cast(order_hint_lsbs); + int64_t candidate = oh + wrap_; + if (any_) { + while (candidate < max_seen_ - half_) { + candidate += mod_; + wrap_ += mod_; + } + while (candidate > max_seen_ + half_) { + candidate -= mod_; + wrap_ -= mod_; + } + } + if (!anchored_) { + anchor_ = candidate; + anchored_ = true; + } + if (!any_ || candidate > max_seen_) max_seen_ = candidate; + any_ = true; + return candidate - anchor_; +} + +} // namespace av2_obu diff --git a/apps/av2_mux/display_order_lifter.h b/apps/av2_mux/display_order_lifter.h new file mode 100644 index 0000000..2d2f246 --- /dev/null +++ b/apps/av2_mux/display_order_lifter.h @@ -0,0 +1,60 @@ +/* + * 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 + +namespace av2_obu { + +// Modular-unwrap lifter for OrderHintLsbs → relative DispOrderHint, used to +// compute per-sample ctts offsets. +// +// This is NOT a full implementation of get_disp_order_hint() +// (05_Syntax_structures.bs:2649). It tracks no reference-frame state, ignores +// dependency maps, does not reset at CLK boundaries, and reads only the +// per-frame OrderHintLsbs. It produces correct output exactly when the input +// stream meets all of the following: +// * Single-layer (one xlayer, one mlayer). +// * Single CVS (one CLK at the start, no further CLKs that reset DOH). +// * No derived-DOH SEFs (derive_sef_order_hint must be 0 if any SEFs exist). +// * No Bridge frames (their DOH is RefOrderHint[bridge_frame_ref_idx], +// which we cannot compute without ref-state tracking). +// * Reorder distance between consecutive output frames < (1 << OrderHintBits) / 2. +// +// Av2Muxer enforces the first four with hard errors. The reorder-distance +// bound is the only soft assumption; it holds for hierarchical-B and +// low-delay GoPs at typical OrderHintBits ≥ 4. +// +// A spec-faithful replacement (with reference state and dep maps) is planned +// once Phase 2 (parser cross-check vs avmdec) validates the per-frame fields +// the full algorithm depends on. +class DisplayOrderLifter { + public: + explicit DisplayOrderLifter(uint32_t order_hint_bits); + + // Clears wrap state at a CVS boundary; does not re-anchor. + void reset_cvs(); + + // Returns the lifted DOH relative to the first lifted value of the stream. + int64_t lift(uint32_t order_hint_lsbs); + + private: + int64_t mod_; + int64_t half_; + int64_t wrap_ = 0; + int64_t max_seen_ = 0; + bool any_ = false; + int64_t anchor_ = 0; + bool anchored_ = false; +}; + +} // namespace av2_obu diff --git a/apps/av2_mux/frame_header_helpers.cpp b/apps/av2_mux/frame_header_helpers.cpp new file mode 100644 index 0000000..c8fe953 --- /dev/null +++ b/apps/av2_mux/frame_header_helpers.cpp @@ -0,0 +1,79 @@ +/* + * 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 "frame_header_helpers.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace av2_obu { + +const FrameHeaderInfo* frame_header_of(const BaseOBU* obu) { + if (!obu) return nullptr; + switch (obu->type()) { + case OBUType::CLK: + if (auto* p = dynamic_cast(obu)) return &p->frame_header(); + return nullptr; + case OBUType::OLK: + if (auto* p = dynamic_cast(obu)) return &p->frame_header(); + return nullptr; + case OBUType::RAS_FRAME: + if (auto* p = dynamic_cast(obu)) return &p->frame_header(); + return nullptr; + case OBUType::SWITCH: + if (auto* p = dynamic_cast(obu)) return &p->frame_header(); + return nullptr; + case OBUType::BRIDGE_FRAME: + if (auto* p = dynamic_cast(obu)) return &p->frame_header(); + return nullptr; + case OBUType::REGULAR_TILE_GROUP: + if (auto* p = dynamic_cast(obu)) return &p->frame_header(); + return nullptr; + case OBUType::LEADING_TILE_GROUP: + if (auto* p = dynamic_cast(obu)) return &p->frame_header(); + return nullptr; + case OBUType::REGULAR_TIP: + if (auto* p = dynamic_cast(obu)) return &p->frame_header(); + return nullptr; + case OBUType::LEADING_TIP: + if (auto* p = dynamic_cast(obu)) return &p->frame_header(); + return nullptr; + case OBUType::REGULAR_SEF: + if (auto* p = dynamic_cast(obu)) return &p->frame_header(); + return nullptr; + case OBUType::LEADING_SEF: + if (auto* p = dynamic_cast(obu)) return &p->frame_header(); + return nullptr; + default: + return nullptr; + } +} + +const BaseOBU* find_output_frame(const TemporalUnit& tu) { + for (const auto* obu : tu.obus()) { + const FrameHeaderInfo* fh = frame_header_of(obu); + if (fh && fh->is_output_frame) return obu; + } + return nullptr; +} + +} // namespace av2_obu diff --git a/apps/av2_mux/frame_header_helpers.h b/apps/av2_mux/frame_header_helpers.h new file mode 100644 index 0000000..c5a3abf --- /dev/null +++ b/apps/av2_mux/frame_header_helpers.h @@ -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 + +namespace av2_obu { + +class BaseOBU; +class TemporalUnit; +struct FrameHeaderInfo; + +// Returns the FrameHeaderInfo of any coded-frame OBU, or nullptr. +const FrameHeaderInfo* frame_header_of(const BaseOBU* obu); + +// First output-frame OBU in tu, or nullptr. +const BaseOBU* find_output_frame(const TemporalUnit& tu); + +} // namespace av2_obu diff --git a/apps/av2_mux/main.cpp b/apps/av2_mux/main.cpp new file mode 100644 index 0000000..b407da9 --- /dev/null +++ b/apps/av2_mux/main.cpp @@ -0,0 +1,107 @@ +/* + * 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 +#include +#include + +#include "av2_muxer.h" +#include "mux_log.h" +#include "mux_strategy.h" +#include +#include + +using namespace av2_obu; + +// -v flips only the named "av2_mux" logger to debug; the default logger +// (used by libav2_obu's per-bit-field tracing) stays at info to keep the packaging-step trace readable. +static void setup_logging(bool verbose) { + auto console = spdlog::stdout_color_mt("console"); + spdlog::set_default_logger(console); + spdlog::set_level(spdlog::level::info); + av2_obu::set_log_level(spdlog::level::info); + + mux_log().set_level(verbose ? spdlog::level::debug : spdlog::level::info); + + spdlog::set_pattern("[%H:%M:%S.%e] [%^%l%$] %v"); + mux_log().set_pattern("[%H:%M:%S.%e] [%^%l%$] %v"); +} + +int main(int argc, char** argv) { + CLI::App app{"av2_mux — mux AV2 elementary bitstreams into MP4/ISOBMFF containers"}; + app.set_version_flag("--version", av2_obu::build_version()); + + std::string input; + std::string output; + UserOptions opts; + bool verbose = false; + + app.add_option("input", input, "Input AV2 bitstream (.bin, .obu, .av2)")->required(); + app.add_option("-o,--output", output, "Output MP4 file")->required(); + CLI::Option* fps_opt = + app.add_option("--fps", opts.frame_rate, + "Frame rate override (precedence: --fps > CI timing_info > default 30)"); + app.add_option("--samples-per-chunk", opts.samples_per_chunk, + "Samples per chunk in stsc (default: 30)"); + app.add_option("--start-tu", opts.start_tu, + "Start packaging at this TU index (0-based; should be a sync sample)"); + app.add_option("--num-samples", opts.num_samples, + "Maximum samples to write (0 = all from start)"); + bool keep_td = false; + app.add_flag( + "--keep-td", keep_td, + "Keep Temporal Delimiter OBUs inside samples. NOT RECOMMENDED — av2-isobmff says TDs " + "SHOULD NOT appear in samples; the sample boundary is the TU boundary."); + const std::string colr_help = + "Force CICP color metadata (per ITU-T H.273 / ISO/IEC 23091-2) into the colr/nclx box. " + "Accepts either a profile name [" + supported_colr_profile_names() + "] or a raw " + "'cp:tc:mc:fr' tuple where cp=ColourPrimaries, tc=TransferCharacteristics, " + "mc=MatrixCoefficients, fr=VideoFullRangeFlag (0=video range, 1=full range)."; + app.add_option("--colr-override", opts.colr_override, colr_help); + app.add_flag("-v,--verbose", verbose, "Enable verbose/debug logging"); + + CLI11_PARSE(app, argc, argv); + setup_logging(verbose); + opts.frame_rate_explicit = (fps_opt->count() > 0); + opts.drop_temporal_delimiters = !keep_td; + if (keep_td) { + MUX_WARN( + "--keep-td set: each sample will start with a Temporal Delimiter OBU. The output is still " + "decodable, but av2-isobmff recommends against this — the TU boundary is meant to be the " + "sample boundary."); + } + if (!opts.colr_override.empty() && !parse_colr_override(opts.colr_override)) { + return 2; // parse_colr_override already logged the diagnostic + } + + MUX_DEBUG("Muxing {} -> {}", input, output); + OBUParser parser; + parser.set_include_temporal_delimiters(!opts.drop_temporal_delimiters); + if (!parser.parse_file(input)) { + MUX_ERROR("Failed to parse bitstream: {}", input); + return 1; + } + + const MuxStrategy strategy = determine_strategy(parser.get_statistics(), opts); + log_stream_summary(parser); + + if (parser.temporal_units().empty()) { + MUX_ERROR("No temporal units; nothing to package"); + return 1; + } + + Av2Muxer muxer(input, strategy); + if (!muxer.package(parser, output)) { + MUX_ERROR("Muxing failed"); + return 1; + } + return 0; +} diff --git a/apps/av2_mux/mp4_writer.cpp b/apps/av2_mux/mp4_writer.cpp new file mode 100644 index 0000000..ce50acb --- /dev/null +++ b/apps/av2_mux/mp4_writer.cpp @@ -0,0 +1,253 @@ +/* + * 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 "mp4_writer.h" +#include "mux_log.h" + +#include + +#include + +namespace av2_obu { + +namespace { + +constexpr uint32_t kBrandAv02 = MP4_FOUR_CHAR_CODE('a', 'v', '0', '2'); +constexpr uint32_t kBrandIso6 = MP4_FOUR_CHAR_CODE('i', 's', 'o', '6'); +constexpr uint32_t kAtomTypeAv2C = MP4_FOUR_CHAR_CODE('a', 'v', '2', 'C'); +constexpr uint32_t kSampleEntryTypeAv02 = MP4_FOUR_CHAR_CODE('a', 'v', '0', '2'); +constexpr uint32_t kAtomTypeColr = MP4_FOUR_CHAR_CODE('c', 'o', 'l', 'r'); +constexpr uint32_t kColourTypeNclx = MP4_FOUR_CHAR_CODE('n', 'c', 'l', 'x'); + +// Allocate an MP4Handle and copy bytes into it. +MP4Handle make_handle(const void* data, size_t size) { + MP4Handle h = nullptr; + if (MP4NewHandle(static_cast(size), &h) != MP4NoErr || !h) return nullptr; + if (size > 0) std::memcpy(*h, data, size); + return h; +} + +} // namespace + +Mp4Writer::Mp4Writer() { + // 0xff in each profile slot = "no profile" per libisomedia conventions. + MP4Err err = MP4NewMovie(&movie_, /*initialODID=*/1, + /*OD*/ 0xff, /*scene*/ 0xff, + /*audio*/ 0xff, /*visual*/ 0xff, + /*graphics*/ 0xff); + if (err != MP4NoErr) { + MUX_ERROR("MP4NewMovie failed (err={})", err); + movie_ = nullptr; + return; + } + + ISOSetMovieBrand(movie_, kBrandAv02, /*minor=*/0); + ISOSetMovieCompatibleBrand(movie_, kBrandAv02); + ISOSetMovieCompatibleBrand(movie_, kBrandIso6); +} + +Mp4Writer::~Mp4Writer() { + if (sample_entry_) MP4DisposeHandle(sample_entry_); + if (movie_) MP4DisposeMovie(movie_); +} + +bool Mp4Writer::add_video_track(uint32_t timescale, uint16_t width, uint16_t height, + const AV2CodecConfigurationBox& av2c) { + if (!movie_) return false; + + MP4Err err = MP4NewMovieTrack(movie_, MP4NewTrackIsVisual, &track_); + if (err != MP4NoErr || !track_) { + MUX_ERROR("MP4NewMovieTrack failed (err={})", err); + return false; + } + + err = MP4NewTrackMedia(track_, &media_, MP4VisualHandlerType, timescale, /*dataRef=*/nullptr); + if (err != MP4NoErr || !media_) { + MUX_ERROR("MP4NewTrackMedia failed (err={})", err); + return false; + } + + std::vector av2c_bytes = av2c.serialize(); + if (av2c_bytes.empty()) { + MUX_ERROR("av2C serialization produced empty payload"); + return false; + } + + MP4Handle av2c_payload = make_handle(av2c_bytes.data(), av2c_bytes.size()); + if (!av2c_payload) { + MUX_ERROR("MP4NewHandle(av2C payload) failed"); + return false; + } + + MP4GenericAtom av2c_atom = nullptr; + err = MP4NewForeignAtom(&av2c_atom, kAtomTypeAv2C, av2c_payload); + if (err != MP4NoErr || !av2c_atom) { + MUX_ERROR("MP4NewForeignAtom('av2C') failed (err={})", err); + MP4DisposeHandle(av2c_payload); + return false; + } + + err = MP4NewHandle(0, &sample_entry_); + if (err != MP4NoErr || !sample_entry_) { + MUX_ERROR("MP4NewHandle(sample_entry) failed (err={})", err); + return false; + } + + err = ISONewGeneralSampleDescription(track_, sample_entry_, /*dataRefIdx=*/1, + kSampleEntryTypeAv02, av2c_atom); + if (err != MP4NoErr) { + MUX_ERROR("ISONewGeneralSampleDescription failed (err={})", err); + return false; + } + + err = ISOSetSampleDescriptionDimensions(sample_entry_, width, height); + if (err != MP4NoErr) { + MUX_ERROR("ISOSetSampleDescriptionDimensions failed (err={})", err); + return false; + } + + return true; +} + +bool Mp4Writer::add_colr_nclx(const ColrInfo& info) { + if (!sample_entry_) return false; + + // TODO(https://github.com/MPEGGroup/isobmff/issues/76): replace with a libisomedia helper once landed. + // ColourInformationBox + uint8_t payload[11]; + payload[0] = static_cast((kColourTypeNclx >> 24) & 0xFF); + payload[1] = static_cast((kColourTypeNclx >> 16) & 0xFF); + payload[2] = static_cast((kColourTypeNclx >> 8) & 0xFF); + payload[3] = static_cast(kColourTypeNclx & 0xFF); + payload[4] = static_cast((info.colour_primaries >> 8) & 0xFF); + payload[5] = static_cast(info.colour_primaries & 0xFF); + payload[6] = static_cast((info.transfer_characteristics >> 8) & 0xFF); + payload[7] = static_cast(info.transfer_characteristics & 0xFF); + payload[8] = static_cast((info.matrix_coefficients >> 8) & 0xFF); + payload[9] = static_cast(info.matrix_coefficients & 0xFF); + payload[10] = static_cast((info.full_range_flag & 0x1) << 7); + + MP4Handle h = make_handle(payload, sizeof(payload)); + if (!h) { + MUX_ERROR("MP4NewHandle(colr payload) failed"); + return false; + } + + MP4GenericAtom atom = nullptr; + MP4Err err = MP4NewForeignAtom(&atom, kAtomTypeColr, h); + if (err != MP4NoErr || !atom) { + MUX_ERROR("MP4NewForeignAtom('colr') failed (err={})", err); + MP4DisposeHandle(h); + return false; + } + + err = ISOAddAtomToSampleDescription(sample_entry_, atom); + if (err != MP4NoErr) { + MUX_ERROR("ISOAddAtomToSampleDescription('colr') failed (err={})", err); + return false; + } + + MUX_DEBUG("Attached colr/nclx (cp={} tc={} mc={} fr={})", info.colour_primaries, + info.transfer_characteristics, info.matrix_coefficients, + info.full_range_flag); + return true; +} + +bool Mp4Writer::add_sample(const std::vector& bytes, uint32_t duration, bool is_sync, + int32_t composition_offset) { + if (!media_ || !sample_entry_) return false; + pending_sizes_.push_back(static_cast(bytes.size())); + pending_data_.insert(pending_data_.end(), bytes.begin(), bytes.end()); + pending_durations_.push_back(duration); + if (is_sync) { + pending_sync_indices_.push_back(static_cast(pending_sizes_.size())); + } + if (signed_ctts_enabled_) pending_ctts_offsets_.push_back(composition_offset); + if (samples_per_chunk_ > 0 && pending_sizes_.size() >= samples_per_chunk_) { + return flush_chunk(); + } + return true; +} + +bool Mp4Writer::enable_signed_composition_offsets() { + if (!media_) return false; + if (!pending_sizes_.empty()) { + MUX_ERROR("enable_signed_composition_offsets() called after samples were buffered"); + return false; + } + MP4Err err = MP4UseSignedCompositionTimeOffsets(media_); + if (err != MP4NoErr) { + MUX_ERROR("MP4UseSignedCompositionTimeOffsets failed (err={})", err); + return false; + } + signed_ctts_enabled_ = true; + return true; +} + +bool Mp4Writer::flush_chunk() { + if (pending_sizes_.empty()) return true; + if (!media_) return false; + + uint32_t n = static_cast(pending_sizes_.size()); + + MP4Handle data = make_handle(pending_data_.data(), pending_data_.size()); + MP4Handle sizes = make_handle(pending_sizes_.data(), n * sizeof(uint32_t)); + MP4Handle durations = make_handle(pending_durations_.data(), n * sizeof(uint32_t)); + MP4Handle sync = pending_sync_indices_.empty() + ? nullptr + : make_handle(pending_sync_indices_.data(), + pending_sync_indices_.size() * sizeof(uint32_t)); + // Stored as u32 in the handle; libisomedia interprets as int32 when signed ctts is enabled. + MP4Handle ctts = nullptr; + if (signed_ctts_enabled_) { + ctts = make_handle(pending_ctts_offsets_.data(), n * sizeof(int32_t)); + } + + MP4Handle entry_for_call = first_chunk_ ? sample_entry_ : nullptr; + + MP4Err err = MP4AddMediaSamples(media_, data, n, durations, sizes, entry_for_call, + /*decodingOffsetsH=*/ctts, sync); + + MP4DisposeHandle(data); + MP4DisposeHandle(sizes); + MP4DisposeHandle(durations); + if (sync) MP4DisposeHandle(sync); + if (ctts) MP4DisposeHandle(ctts); + + if (err != MP4NoErr) { + MUX_ERROR("MP4AddMediaSamples failed (err={}, samples={})", err, n); + return false; + } + + first_chunk_ = false; + pending_data_.clear(); + pending_sizes_.clear(); + pending_durations_.clear(); + pending_sync_indices_.clear(); + pending_ctts_offsets_.clear(); + MUX_DEBUG("Flushed chunk: {} samples", n); + return true; +} + +bool Mp4Writer::finalize(const std::string& output_path) { + if (!movie_) return false; + if (!flush_chunk()) return false; + + MP4Err err = MP4WriteMovieToFile(movie_, output_path.c_str()); + if (err != MP4NoErr) { + MUX_ERROR("MP4WriteMovieToFile failed (err={})", err); + return false; + } + MUX_DEBUG("MP4WriteMovieToFile: {}", output_path); + return true; +} + +} // namespace av2_obu diff --git a/apps/av2_mux/mp4_writer.h b/apps/av2_mux/mp4_writer.h new file mode 100644 index 0000000..48a4cbd --- /dev/null +++ b/apps/av2_mux/mp4_writer.h @@ -0,0 +1,74 @@ +/* + * 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 +#include +#include + +#include +#include + +#include "av2_codec_config.h" +#include "colr_info.h" + +namespace av2_obu { + +// Codec-agnostic libisomedia wrapper for a single AV2 video track. +class Mp4Writer { +public: + Mp4Writer(); + ~Mp4Writer(); + + Mp4Writer(const Mp4Writer&) = delete; + Mp4Writer& operator=(const Mp4Writer&) = delete; + + bool add_video_track(uint32_t timescale, uint16_t width, uint16_t height, + const AV2CodecConfigurationBox& av2c); + + // Must be called after add_video_track() and before the first add_sample(). + bool add_colr_nclx(const ColrInfo& info); + + // Must be called before any add_sample() + bool enable_signed_composition_offsets(); + + // 0 disables auto-flush; caller drives chunk boundaries via flush_chunk()/finalize(). + void set_samples_per_chunk(uint32_t n) { samples_per_chunk_ = n; } + + bool add_sample(const std::vector& bytes, uint32_t duration, bool is_sync, + int32_t composition_offset = 0); + + // One flush_chunk() = one MP4AddMediaSamples() call = one stsc chunk. + bool flush_chunk(); + + bool finalize(const std::string& output_path); + +private: + MP4Movie movie_ = nullptr; + MP4Track track_ = nullptr; + MP4Media media_ = nullptr; + MP4Handle sample_entry_ = nullptr; + + // First chunk supplies sample_entry_; later chunks pass NULL to reuse it. + bool first_chunk_ = true; + + std::vector pending_data_; + std::vector pending_sizes_; + std::vector pending_durations_; + std::vector pending_sync_indices_; + std::vector pending_ctts_offsets_; + + bool signed_ctts_enabled_ = false; + uint32_t samples_per_chunk_ = 0; +}; + +} // namespace av2_obu diff --git a/apps/av2_mux/mux_log.cpp b/apps/av2_mux/mux_log.cpp new file mode 100644 index 0000000..e755313 --- /dev/null +++ b/apps/av2_mux/mux_log.cpp @@ -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/. + */ + +#include "mux_log.h" + +#include + +namespace av2_obu { + +spdlog::logger& mux_log() { + static auto logger = []() { + if (auto existing = spdlog::get("av2_mux")) return existing; + return spdlog::stdout_color_mt("av2_mux"); + }(); + return *logger; +} + +} // namespace av2_obu diff --git a/apps/av2_mux/mux_log.h b/apps/av2_mux/mux_log.h new file mode 100644 index 0000000..bb207fc --- /dev/null +++ b/apps/av2_mux/mux_log.h @@ -0,0 +1,30 @@ +/* + * 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 + +#include + +namespace av2_obu { + +// Named logger so the muxer's verbosity is independent of the libav2_obu +// parser logger (which emits per-bit-field debug spam at the same level). +spdlog::logger& mux_log(); + +} // namespace av2_obu + +#define MUX_TRACE(...) av2_obu::mux_log().trace(__VA_ARGS__) +#define MUX_DEBUG(...) av2_obu::mux_log().debug(__VA_ARGS__) +#define MUX_INFO(...) av2_obu::mux_log().info(__VA_ARGS__) +#define MUX_WARN(...) av2_obu::mux_log().warn(__VA_ARGS__) +#define MUX_ERROR(...) av2_obu::mux_log().error(__VA_ARGS__) diff --git a/apps/av2_mux/mux_strategy.cpp b/apps/av2_mux/mux_strategy.cpp new file mode 100644 index 0000000..2f0c858 --- /dev/null +++ b/apps/av2_mux/mux_strategy.cpp @@ -0,0 +1,109 @@ +/* + * 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 "mux_strategy.h" +#include "mux_log.h" + +#include + +#include + +namespace av2_obu { + +std::optional parse_colr_override(const std::string& spec) { + if (spec.empty()) return std::nullopt; + if (auto profile = colr_profile_by_name(spec)) return profile; + + std::stringstream ss(spec); + std::string tok; + std::vector v; + while (std::getline(ss, tok, ':')) { + try { + v.push_back(static_cast(std::stoul(tok))); + } catch (...) { + MUX_ERROR("--colr-override: '{}' is neither a known profile [{}] nor a numeric tuple", + spec, supported_colr_profile_names()); + return std::nullopt; + } + } + if (v.size() != 4) { + MUX_ERROR("--colr-override: expected 4 CICP values 'cp:tc:mc:fr' or a profile name [{}]; " + "got '{}'", + supported_colr_profile_names(), spec); + return std::nullopt; + } + return ColrInfo{v[0], v[1], v[2], v[3]}; +} + +MuxStrategy determine_strategy(const OBUParser::Statistics& stats, + const UserOptions& user_opts) { + MuxStrategy s; + + s.drop_temporal_delimiters = user_opts.drop_temporal_delimiters; + s.samples_per_chunk = user_opts.samples_per_chunk; + s.start_tu = user_opts.start_tu; + s.num_samples = user_opts.num_samples; + s.colr_override = parse_colr_override(user_opts.colr_override); + + // Timing precedence: --fps > CI timing_info > default 30 fps. + const auto& ci = stats.content_interpretation; + const bool ci_usable = ci.has_timing_info && ci.timing_info.time_scale > 0 && + ci.timing_info.num_units_in_display_tick > 0; + if (user_opts.frame_rate_explicit) { + s.frame_rate = user_opts.frame_rate; + s.timescale = static_cast(user_opts.frame_rate * 1000); + s.default_sample_duration = 1000; + s.timing_source = MuxStrategy::TimingSource::kCli; + } else if (ci_usable) { + const auto& t = ci.timing_info; + s.timescale = t.time_scale; + if (t.equal_picture_interval) { + uint64_t dur = static_cast(t.num_ticks_per_picture_minus_1 + 1) * + t.num_units_in_display_tick; + s.default_sample_duration = + dur > 0xFFFFFFFFull ? 0xFFFFFFFFu : static_cast(dur); + } else { + // Variable per-frame timing not yet supported; fall back to display-tick. + s.default_sample_duration = t.num_units_in_display_tick; + MUX_WARN("CI timing_info has equal_picture_interval=0; using num_units_in_display_tick " + "as constant duration"); + } + s.frame_rate = static_cast(t.time_scale) / + static_cast(s.default_sample_duration); + s.timing_source = MuxStrategy::TimingSource::kCiTimingInfo; + } else { + s.frame_rate = 30.0; + s.timescale = 30000; + s.default_sample_duration = 1000; + s.timing_source = MuxStrategy::TimingSource::kDefault; + } + + s.sample_entry_mode = stats.sequence_headers.has_changes + ? MuxStrategy::SampleEntryMode::kMultiple + : MuxStrategy::SampleEntryMode::kSingle; + + const char* src = "default"; + switch (s.timing_source) { + case MuxStrategy::TimingSource::kCli: src = "CLI"; break; + case MuxStrategy::TimingSource::kCiTimingInfo: src = "CI timing_info"; break; + case MuxStrategy::TimingSource::kDefault: src = "default"; break; + } + MUX_DEBUG("Timing: {:.3f} fps (timescale={}, duration={}, source={})", s.frame_rate, + s.timescale, s.default_sample_duration, src); + MUX_DEBUG("Strategy: sample_entry_mode={}, drop_TDs={}, samples_per_chunk={}", + s.sample_entry_mode == MuxStrategy::SampleEntryMode::kMultiple ? "kMultiple" + : "kSingle", + s.drop_temporal_delimiters ? "yes" : "no", s.samples_per_chunk); + + return s; +} + +} // namespace av2_obu diff --git a/apps/av2_mux/mux_strategy.h b/apps/av2_mux/mux_strategy.h new file mode 100644 index 0000000..70ee4cf --- /dev/null +++ b/apps/av2_mux/mux_strategy.h @@ -0,0 +1,97 @@ +/* + * 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 +#include +#include +#include + +#include "colr_info.h" +#include + +namespace av2_obu { + +// Big-endian fourcc; matches ISOBMFF on-wire byte order. +inline constexpr uint32_t fourcc(char a, char b, char c, char d) { + return (static_cast(static_cast(a)) << 24) | + (static_cast(static_cast(b)) << 16) | + (static_cast(static_cast(c)) << 8) | + static_cast(static_cast(d)); +} + +struct MuxStrategy { + enum class SampleEntryMode { kSingle, kMultiple }; + enum class TimingSource { kCli, kCiTimingInfo, kDefault }; + + bool drop_temporal_delimiters = true; + SampleEntryMode sample_entry_mode = SampleEntryMode::kSingle; + + double frame_rate = 30.0; + uint32_t timescale = 30000; + uint32_t default_sample_duration = 1000; + TimingSource timing_source = TimingSource::kDefault; + + // True iff any SH has monotonic_output_order_flag == 0; gates ctts emission. + bool any_non_monotonic = false; + + uint32_t samples_per_chunk = 30; + + uint32_t start_tu = 0; + uint32_t num_samples = 0; // 0 = all from start_tu + + uint32_t major_brand = fourcc('a', 'v', '0', '2'); + std::vector compatible_brands = { + fourcc('a', 'v', '0', '2'), + fourcc('i', 's', 'o', '6'), + }; + + // If set, overrides any CI/LCR/OPS color metadata from the bitstream. + std::optional colr_override; + + // Hooks for fragmented MP4 / DASH; not yet wired through. + struct Fragmentation { + bool enabled = false; + uint32_t fragment_duration_ms = 0; + bool emit_sidx = false; + uint32_t subsegs_per_sidx = 0; + bool daisy_chain_sidx = false; + } fragmentation; + + // Hooks for CENC; not yet wired through. + struct Encryption { + bool enabled = false; + uint32_t scheme = 0; + std::vector kid; + uint32_t pattern_crypt_block_count = 0; + uint32_t pattern_skip_block_count = 0; + } encryption; +}; + +struct UserOptions { + double frame_rate = 30.0; + bool frame_rate_explicit = false; + bool drop_temporal_delimiters = true; + uint32_t samples_per_chunk = 30; + uint32_t start_tu = 0; + uint32_t num_samples = 0; + std::string colr_override; +}; + +// Accepts either a CICP profile name (see colr_info.h::supported_colr_profile_names()) +// or a raw 'cp:tc:mc:fr' tuple per ITU-T H.273. +std::optional parse_colr_override(const std::string& spec); + +MuxStrategy determine_strategy(const OBUParser::Statistics& stats, + const UserOptions& user_opts); + +} // namespace av2_obu From 877b04acbdc0a6cbac9c2fcfdc9abb0b41dd230c Mon Sep 17 00:00:00 2001 From: Dimitri Podborski Date: Mon, 22 Jun 2026 17:48:17 -0700 Subject: [PATCH 08/10] address #5 --- .../obus/layer_configuration_record_obu.h | 5 +++++ src/obus/layer_configuration_record_obu.cpp | 22 +++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/include/av2_obu/obus/layer_configuration_record_obu.h b/include/av2_obu/obus/layer_configuration_record_obu.h index 5f5b81c..c19853f 100644 --- a/include/av2_obu/obus/layer_configuration_record_obu.h +++ b/include/av2_obu/obus/layer_configuration_record_obu.h @@ -35,6 +35,7 @@ class LayerConfigurationRecordOBU : public BaseOBU { uint32_t max_level_idx = 0; uint32_t tier_flag = 0; uint32_t max_mlayer_count = 0; + uint32_t lsptli_reserved_2bits = 0; }; // Aggregate info (global LCR) @@ -142,6 +143,8 @@ class LayerConfigurationRecordOBU : public BaseOBU { uint32_t lcr_doh_constraint_flag_ = 0; uint32_t lcr_enforce_tile_alignment_flag_ = 0; uint32_t lcr_global_atlas_id_ = 0; + uint32_t lcr_global_reserved_zero_3bits_ = 0; + uint32_t lcr_global_reserved_zero_5bits_ = 0; AggregateInfo aggregate_info_; std::vector xlayer_ids_; std::vector xlayer_ptls_; @@ -153,6 +156,8 @@ class LayerConfigurationRecordOBU : public BaseOBU { uint32_t lcr_profile_tier_level_info_present_flag_ = 0; uint32_t lcr_local_atlas_id_present_flag_ = 0; uint32_t lcr_local_atlas_id_ = 0; + uint32_t lcr_local_reserved_zero_3bits_ = 0; + uint32_t lcr_local_reserved_zero_5bits_ = 0; XLayerPTL local_ptl_; XLayerInfo local_xlayer_info_; }; diff --git a/src/obus/layer_configuration_record_obu.cpp b/src/obus/layer_configuration_record_obu.cpp index b82624d..169baf1 100644 --- a/src/obus/layer_configuration_record_obu.cpp +++ b/src/obus/layer_configuration_record_obu.cpp @@ -26,7 +26,7 @@ static LCROBU::XLayerPTL parse_ptl(BitstreamReader& br, uint32_t xlayer_id) { ptl.max_level_idx = br.read_bits(5); ptl.tier_flag = br.read_bit(); ptl.max_mlayer_count = br.read_bits(3); - br.read_bits(2); // lsptli_reserved_2bits + ptl.lsptli_reserved_2bits = br.read_bits(2); return ptl; } @@ -169,9 +169,9 @@ bool LayerConfigurationRecordOBU::parse_payload(std::ifstream& ifs) { if (lcr_global_atlas_id_present_flag_) { lcr_global_atlas_id_ = br.read_bits(3); } else { - br.read_bits(3); // reserved + lcr_global_reserved_zero_3bits_ = br.read_bits(3); } - br.read_bits(5); // reserved + lcr_global_reserved_zero_5bits_ = br.read_bits(5); if (lcr_aggregate_info_present_flag_) { aggregate_info_.config_idc = br.read_bits(6); @@ -232,9 +232,9 @@ bool LayerConfigurationRecordOBU::parse_payload(std::ifstream& ifs) { if (lcr_local_atlas_id_present_flag_) { lcr_local_atlas_id_ = br.read_bits(3); } else { - br.read_bits(3); // reserved + lcr_local_reserved_zero_3bits_ = br.read_bits(3); } - br.read_bits(5); // reserved + lcr_local_reserved_zero_5bits_ = br.read_bits(5); local_xlayer_info_ = parse_xlayer_info(br, false, lcr_local_atlas_id_present_flag_); } @@ -327,7 +327,10 @@ json LayerConfigurationRecordOBU::to_json() const { j["lcr_enforce_tile_alignment_flag"] = lcr_enforce_tile_alignment_flag_; if (lcr_global_atlas_id_present_flag_) { j["lcr_global_atlas_id"] = lcr_global_atlas_id_; + } else { + j["lcr_global_reserved_zero_3bits"] = lcr_global_reserved_zero_3bits_; } + j["lcr_global_reserved_zero_5bits"] = lcr_global_reserved_zero_5bits_; if (lcr_aggregate_info_present_flag_) { j["aggregate_info"] = {{"config_idc", aggregate_info_.config_idc}, @@ -343,7 +346,8 @@ json LayerConfigurationRecordOBU::to_json() const { {"seq_profile_idc", ptl.seq_profile_idc}, {"max_level_idx", ptl.max_level_idx}, {"tier_flag", ptl.tier_flag}, - {"max_mlayer_count", ptl.max_mlayer_count}}); + {"max_mlayer_count", ptl.max_mlayer_count}, + {"lsptli_reserved_2bits", ptl.lsptli_reserved_2bits}}); } j["xlayer_ptl"] = ptls; } @@ -371,11 +375,15 @@ json LayerConfigurationRecordOBU::to_json() const { j["local_ptl"] = {{"seq_profile_idc", local_ptl_.seq_profile_idc}, {"max_level_idx", local_ptl_.max_level_idx}, {"tier_flag", local_ptl_.tier_flag}, - {"max_mlayer_count", local_ptl_.max_mlayer_count}}; + {"max_mlayer_count", local_ptl_.max_mlayer_count}, + {"lsptli_reserved_2bits", local_ptl_.lsptli_reserved_2bits}}; } if (lcr_local_atlas_id_present_flag_) { j["lcr_local_atlas_id"] = lcr_local_atlas_id_; + } else { + j["lcr_local_reserved_zero_3bits"] = lcr_local_reserved_zero_3bits_; } + j["lcr_local_reserved_zero_5bits"] = lcr_local_reserved_zero_5bits_; j["xlayer_info"] = xlayer_info_to_json(local_xlayer_info_); } From b308dbb9bcc53b7b80f3a0716671f00cfa94c086 Mon Sep 17 00:00:00 2001 From: Dimitri Podborski Date: Mon, 22 Jun 2026 17:53:34 -0700 Subject: [PATCH 09/10] make sure that consumers can disable our logger --- include/av2_obu/core/logging.h | 43 +++ src/core/av2_sequence_header.cpp | 59 +++-- src/core/av2_types.cpp | 32 ++- src/core/base_obu.cpp | 33 +-- src/core/bitstream_reader.cpp | 57 ++-- src/core/frame_header_info.cpp | 5 +- src/core/obu_parser.cpp | 23 +- src/core/tile_group_header.cpp | 5 +- src/obus/atlas_segment_obu.cpp | 11 +- src/obus/bridge_frame_obu.cpp | 11 +- src/obus/buffer_removal_timing_obu.cpp | 7 +- src/obus/clk_obu.cpp | 11 +- src/obus/content_interpretation_obu.cpp | 49 ++-- src/obus/fgm_obu.cpp | 7 +- src/obus/layer_configuration_record_obu.cpp | 9 +- src/obus/leading_sef_obu.cpp | 11 +- src/obus/leading_tile_group_obu.cpp | 11 +- src/obus/leading_tip_obu.cpp | 11 +- src/obus/metadata_group_obu.cpp | 23 +- src/obus/metadata_obu.cpp | 13 +- src/obus/metadata_unit.cpp | 277 ++++++++++---------- src/obus/msdo_obu.cpp | 7 +- src/obus/multi_frame_header_obu.cpp | 9 +- src/obus/olk_obu.cpp | 11 +- src/obus/operating_point_set_obu.cpp | 9 +- src/obus/padding_obu.cpp | 3 +- src/obus/qm_obu.cpp | 7 +- src/obus/ras_frame_obu.cpp | 11 +- src/obus/regular_sef_obu.cpp | 11 +- src/obus/regular_tile_group_obu.cpp | 11 +- src/obus/regular_tip_obu.cpp | 11 +- src/obus/sequence_header_obu.cpp | 13 +- src/obus/switch_obu.cpp | 11 +- src/obus/temporal_delimiter_obu.cpp | 3 +- src/obus/tile_group_obu.cpp | 3 +- src/obus/unknown_obu.cpp | 5 +- 36 files changed, 457 insertions(+), 376 deletions(-) create mode 100644 include/av2_obu/core/logging.h diff --git a/include/av2_obu/core/logging.h b/include/av2_obu/core/logging.h new file mode 100644 index 0000000..1a335af --- /dev/null +++ b/include/av2_obu/core/logging.h @@ -0,0 +1,43 @@ +/* + * 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 + +#include + +namespace av2_obu { + +// Library-private logger. Routes all libav2_obu output through a dedicated named spdlog logger "av2_obu" +// Consumer applications keep full control of the spdlog default logger and their own log streams. +// Consumers can: +// * silence library output: av2_obu::set_log_level(spdlog::level::off) +// * route to their own sink: spdlog::logger& l = av2_obu::log(); +// l.sinks() = { my_sink }; +// * leave consumer logging untouched while doing either of the above. +spdlog::logger& log(); + +// Convenience wrappers around the named logger's level. set_log_level() is +// kept here so callers don't have to pull the larger types header just to silence the library. +void set_log_level(spdlog::level::level_enum level); +spdlog::level::level_enum get_log_level(); + +} // namespace av2_obu + +// Macros that route through the library logger. Library code must use these +// instead of spdlog::xxx(...) — they expand to no-ops at compile time when +// SPDLOG_ACTIVE_LEVEL excludes the corresponding level. +#define LIB_TRACE(...) SPDLOG_LOGGER_TRACE(&av2_obu::log(), __VA_ARGS__) +#define LIB_DEBUG(...) SPDLOG_LOGGER_DEBUG(&av2_obu::log(), __VA_ARGS__) +#define LIB_INFO(...) SPDLOG_LOGGER_INFO(&av2_obu::log(), __VA_ARGS__) +#define LIB_WARN(...) SPDLOG_LOGGER_WARN(&av2_obu::log(), __VA_ARGS__) +#define LIB_ERROR(...) SPDLOG_LOGGER_ERROR(&av2_obu::log(), __VA_ARGS__) diff --git a/src/core/av2_sequence_header.cpp b/src/core/av2_sequence_header.cpp index 9c29fbe..281753d 100644 --- a/src/core/av2_sequence_header.cpp +++ b/src/core/av2_sequence_header.cpp @@ -10,6 +10,7 @@ */ #include +#include #include @@ -21,7 +22,7 @@ namespace av2_obu { // ==================== TimingInfo ==================== bool TimingInfo::parse(BitstreamReader& br) { - spdlog::debug("Parsing timing_info"); + LIB_DEBUG("Parsing timing_info"); num_units_in_display_tick = br.read_bits(32); time_scale = br.read_bits(32); @@ -31,8 +32,8 @@ bool TimingInfo::parse(BitstreamReader& br) { num_ticks_per_picture_minus_1 = br.read_uvlc(); } - spdlog::debug(" num_units_in_display_tick = {}", num_units_in_display_tick); - spdlog::debug(" time_scale = {}", time_scale); + LIB_DEBUG(" num_units_in_display_tick = {}", num_units_in_display_tick); + LIB_DEBUG(" time_scale = {}", time_scale); return true; } @@ -50,15 +51,15 @@ json TimingInfo::to_json() const { // ==================== SeqDecoderModelInfo ==================== bool SeqDecoderModelInfo::parse(BitstreamReader& br) { - spdlog::debug("Parsing seq_decoder_model_info"); + LIB_DEBUG("Parsing seq_decoder_model_info"); decoder_buffer_delay = br.read_uvlc(); encoder_buffer_delay = br.read_uvlc(); low_delay_mode_flag = br.read_bit(); - spdlog::debug(" decoder_buffer_delay = {}", decoder_buffer_delay); - spdlog::debug(" encoder_buffer_delay = {}", encoder_buffer_delay); - spdlog::debug(" low_delay_mode_flag = {}", low_delay_mode_flag); + LIB_DEBUG(" decoder_buffer_delay = {}", decoder_buffer_delay); + LIB_DEBUG(" encoder_buffer_delay = {}", encoder_buffer_delay); + LIB_DEBUG(" low_delay_mode_flag = {}", low_delay_mode_flag); return true; } @@ -73,7 +74,7 @@ json SeqDecoderModelInfo::to_json() const { bool SequencePartitionConfig::parse(BitstreamReader& br, bool single_picture_header_flag, bool Monochrome) { - spdlog::debug("Parsing sequence_partition_config"); + LIB_DEBUG("Parsing sequence_partition_config"); use_256x256_superblock = br.read_bit(); if (!use_256x256_superblock) { @@ -109,8 +110,8 @@ bool SequencePartitionConfig::parse(BitstreamReader& br, bool single_picture_hea MaxPbAspectRatio = 8; } - spdlog::debug(" use_256x256_superblock = {}", use_256x256_superblock); - spdlog::debug(" use_128x128_superblock = {}", use_128x128_superblock); + LIB_DEBUG(" use_256x256_superblock = {}", use_256x256_superblock); + LIB_DEBUG(" use_128x128_superblock = {}", use_128x128_superblock); return true; } @@ -151,7 +152,7 @@ static void parse_seg_info(BitstreamReader& br, uint32_t numSegments) { // ==================== SequenceSegmentConfig ==================== bool SequenceSegmentConfig::parse(BitstreamReader& br) { - spdlog::debug("Parsing sequence_segment_config"); + LIB_DEBUG("Parsing sequence_segment_config"); enable_ext_seg = br.read_bit(); MaxSegments = enable_ext_seg ? 16 : 8; @@ -162,7 +163,7 @@ bool SequenceSegmentConfig::parse(BitstreamReader& br) { parse_seg_info(br, MaxSegments); } - spdlog::debug(" enable_ext_seg = {}, MaxSegments = {}", enable_ext_seg, MaxSegments); + LIB_DEBUG(" enable_ext_seg = {}, MaxSegments = {}", enable_ext_seg, MaxSegments); return true; } @@ -177,7 +178,7 @@ json SequenceSegmentConfig::to_json() const { // ==================== SequenceIntraConfig ==================== bool SequenceIntraConfig::parse(BitstreamReader& br, bool Monochrome) { - spdlog::debug("Parsing sequence_intra_config"); + LIB_DEBUG("Parsing sequence_intra_config"); enable_dip = br.read_bit(); enable_intra_edge_filter = br.read_bit(); @@ -209,7 +210,7 @@ json SequenceIntraConfig::to_json() const { // ==================== SequenceInterConfig ==================== bool SequenceInterConfig::parse(BitstreamReader& br, bool single_picture_header_flag) { - spdlog::debug("Parsing sequence_inter_config (single_picture_header={})", + LIB_DEBUG("Parsing sequence_inter_config (single_picture_header={})", single_picture_header_flag); seq_enabled_motion_modes.resize(MOTION_MODES, 0); @@ -353,7 +354,7 @@ bool SequenceInterConfig::parse(BitstreamReader& br, bool single_picture_header_ enable_short_refresh_frame_flags = br.read_bit(); } - spdlog::debug(" NumRefFrames = {}, OrderHintBits = {}", NumRefFrames, OrderHintBits); + LIB_DEBUG(" NumRefFrames = {}, OrderHintBits = {}", NumRefFrames, OrderHintBits); return true; } @@ -402,7 +403,7 @@ json SequenceInterConfig::to_json() const { // ==================== SequenceSCCConfig ==================== bool SequenceSCCConfig::parse(BitstreamReader& br, bool single_picture_header_flag) { - spdlog::debug("Parsing sequence_scc_config"); + LIB_DEBUG("Parsing sequence_scc_config"); if (single_picture_header_flag) { seq_force_screen_content_tools = SELECT_SCREEN_CONTENT_TOOLS; @@ -427,8 +428,8 @@ bool SequenceSCCConfig::parse(BitstreamReader& br, bool single_picture_header_fl } } - spdlog::debug(" seq_force_screen_content_tools = {}", seq_force_screen_content_tools); - spdlog::debug(" seq_force_integer_mv = {}", seq_force_integer_mv); + LIB_DEBUG(" seq_force_screen_content_tools = {}", seq_force_screen_content_tools); + LIB_DEBUG(" seq_force_integer_mv = {}", seq_force_integer_mv); return true; } @@ -444,7 +445,7 @@ json SequenceSCCConfig::to_json() const { bool SequenceTransformQuantEntropyConfig::parse(BitstreamReader& br, bool single_picture_header_flag, bool Monochrome) { - spdlog::debug("Parsing sequence_transform_quant_entropy_config"); + LIB_DEBUG("Parsing sequence_transform_quant_entropy_config"); enable_fsc = br.read_bit(); @@ -568,7 +569,7 @@ json SequenceTransformQuantEntropyConfig::to_json() const { bool SequenceFilterConfig::parse(BitstreamReader& br, bool single_picture_header_flag, bool /* Monochrome */, BlockSize seq_sb_size) { - spdlog::debug("Parsing sequence_filter_config"); + LIB_DEBUG("Parsing sequence_filter_config"); disable_loopfilters_across_tiles = br.read_bit(); enable_cdef = br.read_bit(); @@ -743,7 +744,7 @@ static void parse_tile_params(BitstreamReader& br, uint32_t frameWidth, uint32_t bool SequenceTileConfig::parse(BitstreamReader& br, uint32_t frameWidth, uint32_t frameHeight, bool use_256x256_superblock, bool use_128x128_superblock, uint32_t seq_level_idx, uint32_t seq_tier) { - spdlog::debug("Parsing sequence_tile_config"); + LIB_DEBUG("Parsing sequence_tile_config"); seq_tile_info_present_flag = br.read_bit(); if (seq_tile_info_present_flag) { @@ -796,7 +797,7 @@ void AV2SequenceHeader::set_chroma_format_and_bit_depth() { // ==================== AV2SequenceHeader (Main) ==================== bool AV2SequenceHeader::parse(BitstreamReader& br) { - spdlog::debug("Parsing AV2 Sequence Header"); + LIB_DEBUG("Parsing AV2 Sequence Header"); seq_header_id = br.read_uvlc(); seq_profile_idc = br.read_bits(5); @@ -813,10 +814,10 @@ bool AV2SequenceHeader::parse(BitstreamReader& br) { bit_depth_idc = br.read_uvlc(); set_chroma_format_and_bit_depth(); - spdlog::debug(" seq_header_id = {}", seq_header_id); - spdlog::debug(" seq_profile_idc = {}", seq_profile_idc); - spdlog::debug(" single_picture_header_flag = {}", single_picture_header_flag); - spdlog::debug(" chroma_format_idc = {}, bit_depth_idc = {}, BitDepth = {}", chroma_format_idc, + LIB_DEBUG(" seq_header_id = {}", seq_header_id); + LIB_DEBUG(" seq_profile_idc = {}", seq_profile_idc); + LIB_DEBUG(" single_picture_header_flag = {}", single_picture_header_flag); + LIB_DEBUG(" chroma_format_idc = {}, bit_depth_idc = {}, BitDepth = {}", chroma_format_idc, bit_depth_idc, BitDepth); if (single_picture_header_flag) { @@ -860,8 +861,8 @@ bool AV2SequenceHeader::parse(BitstreamReader& br) { n = frame_height_bits_minus_1 + 1; max_frame_height_minus_1 = br.read_bits(n); - spdlog::debug(" max_frame_width = {}", max_frame_width_minus_1 + 1); - spdlog::debug(" max_frame_height = {}", max_frame_height_minus_1 + 1); + LIB_DEBUG(" max_frame_width = {}", max_frame_width_minus_1 + 1); + LIB_DEBUG(" max_frame_height = {}", max_frame_height_minus_1 + 1); // Cropping window seq_cropping_window_present_flag = br.read_bit(); @@ -993,7 +994,7 @@ bool AV2SequenceHeader::parse(BitstreamReader& br) { // It belongs to obu_payload() and is parsed by BaseOBU::parse_obu_trailing_bits(). // We should fix this: https://github.com/AOMediaCodec/av2-spec-internal/issues/488 - spdlog::debug("Successfully parsed AV2 Sequence Header"); + LIB_DEBUG("Successfully parsed AV2 Sequence Header"); return true; } diff --git a/src/core/av2_types.cpp b/src/core/av2_types.cpp index 1e43ddd..13d8e14 100644 --- a/src/core/av2_types.cpp +++ b/src/core/av2_types.cpp @@ -12,27 +12,31 @@ #include #include +#include + #include +#include namespace av2_obu { -// ========== LOGGING CONTROL ========== - -void set_log_level(spdlog::level::level_enum level) { - spdlog::set_level(level); +// ========== LOGGING ========== + +spdlog::logger& log() { + static const std::shared_ptr kLogger = []() { + auto existing = spdlog::get("av2_obu"); + if (existing) return existing; + auto sink = std::make_shared(); + auto l = std::make_shared("av2_obu", sink); + l->set_level(spdlog::level::warn); + spdlog::register_logger(l); + return l; + }(); + return *kLogger; } -spdlog::level::level_enum get_log_level() { - return spdlog::get_level(); -} +void set_log_level(spdlog::level::level_enum level) { log().set_level(level); } -// Initialize library with default log level (warn) -namespace { -struct LogInit { - LogInit() { spdlog::set_level(spdlog::level::warn); } -}; -static LogInit log_init; -} // namespace +spdlog::level::level_enum get_log_level() { return log().level(); } // ========== TYPE MAPPINGS ========== diff --git a/src/core/base_obu.cpp b/src/core/base_obu.cpp index ebaeddf..8be8701 100644 --- a/src/core/base_obu.cpp +++ b/src/core/base_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include @@ -62,7 +63,7 @@ json OBUPosition::to_json() const { bool OBUHeader::parse(std::ifstream& ifs) { char byte1; if (!ifs.read(&byte1, 1)) { - spdlog::error("Failed to read OBU header byte"); + LIB_ERROR("Failed to read OBU header byte"); return false; } @@ -72,23 +73,23 @@ bool OBUHeader::parse(std::ifstream& ifs) { obu_type_ = (byte1 >> 2) & 0x1F; obu_tlayer_id_ = byte1 & 0x3; - spdlog::debug("OBU header: type={}, tlayer={}, ext={}", obu_type_, obu_tlayer_id_, + LIB_DEBUG("OBU header: type={}, tlayer={}, ext={}", obu_type_, obu_tlayer_id_, obu_extension_flag_); if (obu_extension_flag_) { char byte2; if (!ifs.read(&byte2, 1)) { - spdlog::error("Failed to read OBU extension byte"); + LIB_ERROR("Failed to read OBU extension byte"); return false; } // byte 2: [mlayer_id(3) | xlayer_id(5)] obu_mlayer_id_ = (byte2 >> 5) & 0x07; obu_xlayer_id_ = byte2 & 0x1F; - spdlog::debug(" Extension: mlayer={}, xlayer={}", obu_mlayer_id_, obu_xlayer_id_); + LIB_DEBUG(" Extension: mlayer={}, xlayer={}", obu_mlayer_id_, obu_xlayer_id_); } if (!is_valid_obu_type(obu_type_)) { - spdlog::warn("Unknown OBU type: {}", obu_type_); + LIB_WARN("Unknown OBU type: {}", obu_type_); } return ifs.good(); @@ -106,7 +107,7 @@ json OBUHeader::to_json() const { std::unique_ptr BaseOBU::create(std::ifstream& ifs, const OBUPosition& pos, ParseMode mode, const AV2SequenceHeader* seq_header) { - spdlog::debug("Creating OBU at position {}", static_cast(pos.start_pos)); + LIB_DEBUG("Creating OBU at position {}", static_cast(pos.start_pos)); std::unique_ptr obu; @@ -114,7 +115,7 @@ std::unique_ptr BaseOBU::create(std::ifstream& ifs, const OBUPosition& OBUHeader temp_header; ifs.seekg(pos.header_pos); if (!temp_header.parse(ifs)) { - spdlog::error("Failed to parse OBU header at position {}", + LIB_ERROR("Failed to parse OBU header at position {}", static_cast(pos.header_pos)); return nullptr; } @@ -199,7 +200,7 @@ std::unique_ptr BaseOBU::create(std::ifstream& ifs, const OBUPosition& obu = std::make_unique(pos); break; default: - spdlog::warn("Creating UnknownOBU for type {}", to_string(type)); + LIB_WARN("Creating UnknownOBU for type {}", to_string(type)); obu = std::make_unique(pos); break; } @@ -210,14 +211,14 @@ std::unique_ptr BaseOBU::create(std::ifstream& ifs, const OBUPosition& ifs.seekg(pos.header_pos); try { if (!obu->parse(ifs)) { - spdlog::error("Failed to parse {} at position {}", to_string(type), + LIB_ERROR("Failed to parse {} at position {}", to_string(type), static_cast(pos.header_pos)); // Seek to end of OBU so parsing can continue with the next OBU ifs.seekg(pos.end_pos); return nullptr; } } catch (const std::exception& e) { - spdlog::error("Exception parsing {} at position {}: {}", to_string(type), + LIB_ERROR("Exception parsing {} at position {}: {}", to_string(type), static_cast(pos.header_pos), e.what()); // Seek to end of OBU so parsing can continue with the next OBU ifs.seekg(pos.end_pos); @@ -238,12 +239,12 @@ bool BaseOBU::parse(std::ifstream& ifs) { // Parse payload (implemented by derived classes) if (!parse_payload(ifs)) { - spdlog::error("Failed to parse payload for {}", type_name()); + LIB_ERROR("Failed to parse payload for {}", type_name()); return false; } parsed_ = true; - spdlog::debug("Successfully parsed {} (size={})", type_name(), position_.payload_size); + LIB_DEBUG("Successfully parsed {} (size={})", type_name(), position_.payload_size); return true; } @@ -270,13 +271,13 @@ bool BaseOBU::skip_payload(std::ifstream& ifs) { bool BaseOBU::parse_obu_trailing_bits(BitstreamReader& br) { size_t remainingPayloadBits = br.bits_remaining(); if (remainingPayloadBits == 0) { - spdlog::warn("{}: no remaining bits for trailing_bits()", type_name()); + LIB_WARN("{}: no remaining bits for trailing_bits()", type_name()); return false; } if (is_extensible_obu(type())) { obu_extension_flag_ = br.read_bit(); - spdlog::debug("{}: obu_extension_flag = {}, remaining = {} bits", type_name(), + LIB_DEBUG("{}: obu_extension_flag = {}, remaining = {} bits", type_name(), obu_extension_flag_, remainingPayloadBits - 1); if (obu_extension_flag_) { @@ -287,13 +288,13 @@ bool BaseOBU::parse_obu_trailing_bits(BitstreamReader& br) { } else { size_t trailingBits = remainingPayloadBits - 1; if (trailingBits > 0 && !br.read_trailing_bits(trailingBits)) { - spdlog::warn("{}: invalid trailing bits ({} bits)", type_name(), trailingBits); + LIB_WARN("{}: invalid trailing bits ({} bits)", type_name(), trailingBits); return false; } } } else { if (!br.read_trailing_bits(remainingPayloadBits)) { - spdlog::warn("{}: invalid trailing bits ({} bits)", type_name(), remainingPayloadBits); + LIB_WARN("{}: invalid trailing bits ({} bits)", type_name(), remainingPayloadBits); return false; } } diff --git a/src/core/bitstream_reader.cpp b/src/core/bitstream_reader.cpp index ead4645..6fdb7ac 100644 --- a/src/core/bitstream_reader.cpp +++ b/src/core/bitstream_reader.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -21,7 +22,7 @@ namespace av2_obu { BitstreamReader::BitstreamReader(std::ifstream& ifs, size_t byte_count) { data_.resize(byte_count); if (!ifs.read(reinterpret_cast(data_.data()), byte_count)) { - spdlog::error("Failed to read {} bytes for bitstream reader", byte_count); + LIB_ERROR("Failed to read {} bytes for bitstream reader", byte_count); throw std::runtime_error("Failed to read bitstream data"); } bit_pos_ = 0; @@ -33,12 +34,12 @@ uint64_t BitstreamReader::read_bits(uint32_t n) { if (n == 0) return 0; if (n > 64) { - spdlog::error("Cannot read more than 64 bits at once (requested: {})", n); + LIB_ERROR("Cannot read more than 64 bits at once (requested: {})", n); throw std::runtime_error("Invalid bit read size"); } if (!has_bits(n)) { - spdlog::error("Not enough bits remaining (need {}, have {})", n, bits_remaining()); + LIB_ERROR("Not enough bits remaining (need {}, have {})", n, bits_remaining()); throw std::runtime_error("Bitstream underflow"); } @@ -54,7 +55,7 @@ uint64_t BitstreamReader::read_bits(uint32_t n) { bit_pos_++; } - spdlog::debug("read_bits({}) = {}", n, result); + LIB_DEBUG("read_bits({}) = {}", n, result); return result; } @@ -62,7 +63,7 @@ uint64_t BitstreamReader::read_le(uint32_t n) { if (n == 0) return 0; if (n > 8) { - spdlog::error("Cannot read more than 8 bytes at once (requested: {})", n); + LIB_ERROR("Cannot read more than 8 bytes at once (requested: {})", n); throw std::runtime_error("Invalid le() byte count"); } @@ -72,7 +73,7 @@ uint64_t BitstreamReader::read_le(uint32_t n) { t += (static_cast(byte) << (i * 8)); } - spdlog::debug("read_le({}) = {}", n, t); + LIB_DEBUG("read_le({}) = {}", n, t); return t; } @@ -80,7 +81,7 @@ int32_t BitstreamReader::read_su(uint32_t n) { if (n == 0) return 0; if (n > 32) { - spdlog::error("Cannot read more than 32 bits for su() (requested: {})", n); + LIB_ERROR("Cannot read more than 32 bits for su() (requested: {})", n); throw std::runtime_error("Invalid su() bit count"); } @@ -94,7 +95,7 @@ int32_t BitstreamReader::read_su(uint32_t n) { result = value; } - spdlog::debug("read_su({}) = {} (raw value={})", n, result, value); + LIB_DEBUG("read_su({}) = {} (raw value={})", n, result, value); return result; } @@ -109,20 +110,20 @@ uint64_t BitstreamReader::read_uvlc() { leading_zeros++; if (leading_zeros >= 32) { - spdlog::debug("read_uvlc() = {} (max value)", (1ULL << 32) - 1); + LIB_DEBUG("read_uvlc() = {} (max value)", (1ULL << 32) - 1); return (1ULL << 32) - 1; } } if (leading_zeros == 0) { - spdlog::debug("read_uvlc() = 0"); + LIB_DEBUG("read_uvlc() = 0"); return 0; } uint64_t value = read_bits(leading_zeros); uint64_t result = value + (1ULL << leading_zeros) - 1; - spdlog::debug("read_uvlc() = {} (leadingZeros={}, value={})", result, leading_zeros, value); + LIB_DEBUG("read_uvlc() = {} (leadingZeros={}, value={})", result, leading_zeros, value); return result; } @@ -131,21 +132,21 @@ int64_t BitstreamReader::read_svlc() { if (value == 0) return 0; int64_t half = static_cast((value + 1) >> 1); int64_t result = (value & 1) ? half : -half; - spdlog::debug("read_svlc() = {} (uvlc={})", result, value); + LIB_DEBUG("read_svlc() = {} (uvlc={})", result, value); return result; } uint32_t BitstreamReader::read_leb128() { // This syntax element will only be present when the bitstream position is byte aligned if (bit_pos_ % 8 != 0) { - spdlog::warn("read_leb128() called at non-byte-aligned position (bit {})", bit_pos_); + LIB_WARN("read_leb128() called at non-byte-aligned position (bit {})", bit_pos_); } uint32_t value = 0; uint32_t byte_count = 0; for (uint32_t i = 0; i < 8; ++i) { // Max 8 bytes for uint32_t if (!has_bits(8)) { - spdlog::error("Not enough bytes for LEB128 decoding"); + LIB_ERROR("Not enough bytes for LEB128 decoding"); throw std::runtime_error("Bitstream underflow during LEB128 read"); } @@ -159,7 +160,7 @@ uint32_t BitstreamReader::read_leb128() { } } - spdlog::debug("read_leb128() = {} ({} bytes)", value, byte_count); + LIB_DEBUG("read_leb128() = {} ({} bytes)", value, byte_count); leb128_bytes_ = byte_count; return value; } @@ -171,14 +172,14 @@ uint32_t BitstreamReader::read_ns(uint32_t n) { uint32_t v = static_cast(read_bits(w - 1)); if (v < m) { - spdlog::debug("read_ns({}) = {} (early exit)", n, v); + LIB_DEBUG("read_ns({}) = {} (early exit)", n, v); return v; } uint32_t extra_bit = read_bit(); uint32_t result = (v << 1) - m + extra_bit; - spdlog::debug("read_ns({}) = {} (w={}, m={}, v={}, extra={})", n, result, w, m, v, extra_bit); + LIB_DEBUG("read_ns({}) = {} (w={}, m={}, v={}, extra={})", n, result, w, m, v, extra_bit); return result; } @@ -188,13 +189,13 @@ uint32_t BitstreamReader::read_rg(uint32_t n) { if (rg_bit == 0) { uint32_t remainder = static_cast(read_bits(n)); uint32_t result = (q << n) + remainder; - spdlog::debug("read_rg({}) = {} (q={}, remainder={})", n, result, q, remainder); + LIB_DEBUG("read_rg({}) = {} (q={}, remainder={})", n, result, q, remainder); return result; } } // Overflow case - spec returns -1, but we throw to match error handling pattern - spdlog::error("Rice-Golomb overflow: no zero bit found in 32 attempts"); + LIB_ERROR("Rice-Golomb overflow: no zero bit found in 32 attempts"); throw std::runtime_error("Rice-Golomb decoding overflow"); } @@ -202,31 +203,31 @@ uint32_t BitstreamReader::read_tu(uint32_t mx) { for (uint32_t idx = 0; idx < mx; idx++) { uint32_t tu_bit = read_bit(); if (tu_bit == 0) { - spdlog::debug("read_tu({}) = {} (found 0 at idx={})", mx, idx, idx); + LIB_DEBUG("read_tu({}) = {} (found 0 at idx={})", mx, idx, idx); return idx; } } // Reached maximum - final 0 is omitted - spdlog::debug("read_tu({}) = {} (max reached)", mx, mx); + LIB_DEBUG("read_tu({}) = {} (max reached)", mx, mx); return mx; } bool BitstreamReader::read_trailing_bits(size_t nbBits) { if (nbBits == 0 || nbBits > 8) { - spdlog::warn("Invalid trailing_bits count: {} (expected 1-8)", nbBits); + LIB_WARN("Invalid trailing_bits count: {} (expected 1-8)", nbBits); return false; } if (!has_bits(nbBits)) { - spdlog::warn("Not enough bits for trailing_bits (need {}, have {})", nbBits, bits_remaining()); + LIB_WARN("Not enough bits for trailing_bits (need {}, have {})", nbBits, bits_remaining()); return false; } // trailing_one_bit f(1) — must be 1 uint32_t trailing_one = read_bit(); if (trailing_one != 1) { - spdlog::warn("trailing_bits: expected leading 1 bit, got 0"); + LIB_WARN("trailing_bits: expected leading 1 bit, got 0"); return false; } @@ -234,25 +235,25 @@ bool BitstreamReader::read_trailing_bits(size_t nbBits) { for (size_t i = 1; i < nbBits; i++) { uint32_t zero_bit = read_bit(); if (zero_bit != 0) { - spdlog::warn("trailing_bits: expected 0 at position {}, got 1", i); + LIB_WARN("trailing_bits: expected 0 at position {}, got 1", i); return false; } } - spdlog::debug("trailing_bits({}) parsed successfully", nbBits); + LIB_DEBUG("trailing_bits({}) parsed successfully", nbBits); return true; } void BitstreamReader::byte_align() { if (bit_pos_ % 8 != 0) { bit_pos_ = ((bit_pos_ / 8) + 1) * 8; - spdlog::debug("byte_align() -> bit_pos={}", bit_pos_); + LIB_DEBUG("byte_align() -> bit_pos={}", bit_pos_); } } void BitstreamReader::skip_bits(size_t n) { if (!has_bits(n)) { - spdlog::error("Not enough bits to skip (need {}, have {})", n, bits_remaining()); + LIB_ERROR("Not enough bits to skip (need {}, have {})", n, bits_remaining()); throw std::runtime_error("Bitstream underflow during skip"); } bit_pos_ += n; diff --git a/src/core/frame_header_info.cpp b/src/core/frame_header_info.cpp index 36ebd78..47b9380 100644 --- a/src/core/frame_header_info.cpp +++ b/src/core/frame_header_info.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -266,7 +267,7 @@ bool FrameHeaderInfo::parse_lightweight(BitstreamReader& br, OBUType obu_type, // For lightweight, we can't resolve this without full state // Parse will continue but ref_frame_idx won't be populated NumTotalRefs = 0; - spdlog::debug("Implicit ref frame map — ref_frame_idx not available in lightweight mode"); + LIB_DEBUG("Implicit ref frame map — ref_frame_idx not available in lightweight mode"); } ref_frame_idx.resize(NumTotalRefs); @@ -389,7 +390,7 @@ bool FrameHeaderInfo::parse_deep(BitstreamReader& /*br*/, OBUType /*obu_type*/, // cdef_params(), lr_params(), ccso_params(), read_tx_mode(), // frame_reference_mode(), skip_mode_params(), global_motion_params(), // film_grain_config() - spdlog::debug("Deep frame header parsing not yet implemented"); + LIB_DEBUG("Deep frame header parsing not yet implemented"); return true; } diff --git a/src/core/obu_parser.cpp b/src/core/obu_parser.cpp index 6c4c3ab..46f837d 100644 --- a/src/core/obu_parser.cpp +++ b/src/core/obu_parser.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -25,7 +26,7 @@ namespace av2_obu { bool OBUParser::parse_file(const std::string& filename) { std::ifstream ifs(filename, std::ios::binary); if (!ifs) { - spdlog::error("Failed to open file: {}", filename); + LIB_ERROR("Failed to open file: {}", filename); return false; } @@ -34,7 +35,7 @@ bool OBUParser::parse_file(const std::string& filename) { std::streampos file_size = ifs.tellg(); ifs.seekg(0, std::ios::beg); - spdlog::debug("Parsing AV2 file: {} ({} bytes)", filename, static_cast(file_size)); + LIB_DEBUG("Parsing AV2 file: {} ({} bytes)", filename, static_cast(file_size)); // Clear previous state obus_.clear(); @@ -44,13 +45,13 @@ bool OBUParser::parse_file(const std::string& filename) { // Scan the file if (!scan_file(ifs)) { - spdlog::error("Failed to parse file: {}", filename); + LIB_ERROR("Failed to parse file: {}", filename); return false; } build_temporal_units(); - spdlog::debug("Successfully parsed {} OBUs, {} temporal units", obus_.size(), + LIB_DEBUG("Successfully parsed {} OBUs, {} temporal units", obus_.size(), temporal_units_.size()); return true; } @@ -70,7 +71,7 @@ bool OBUParser::scan_file(std::ifstream& ifs) { break; } - spdlog::debug("--- OBU {} at position {} ---", obu_index, static_cast(record_begin)); + LIB_DEBUG("--- OBU {} at position {} ---", obu_index, static_cast(record_begin)); OBUPosition pos; pos.start_pos = record_begin; @@ -79,7 +80,7 @@ bool OBUParser::scan_file(std::ifstream& ifs) { uint32_t total_size_val; uint32_t size_field_len = read_annex_b_size(ifs, total_size_val); if (size_field_len == 0) { - spdlog::error("Failed to read size field at position {}", + LIB_ERROR("Failed to read size field at position {}", static_cast(record_begin)); return false; } @@ -91,7 +92,7 @@ bool OBUParser::scan_file(std::ifstream& ifs) { OBUHeader temp_header; std::streampos header_start = ifs.tellg(); if (!temp_header.parse(ifs)) { - spdlog::error("Failed to parse header at position {}", static_cast(header_start)); + LIB_ERROR("Failed to parse header at position {}", static_cast(header_start)); return false; } pos.header_len = static_cast(ifs.tellg() - header_start); @@ -101,14 +102,14 @@ bool OBUParser::scan_file(std::ifstream& ifs) { // In AV2, the size field includes the header if (total_size_val < pos.header_len) { - spdlog::error("Total size ({}) is less than header length ({})", total_size_val, + LIB_ERROR("Total size ({}) is less than header length ({})", total_size_val, pos.header_len); return false; } pos.payload_size = total_size_val - pos.header_len; pos.end_pos = pos.payload_pos + static_cast(pos.payload_size); - spdlog::debug("Position info: start={}, header={}, payload={}, end={}, payload_size={}", + LIB_DEBUG("Position info: start={}, header={}, payload={}, end={}, payload_size={}", static_cast(pos.start_pos), static_cast(pos.header_pos), static_cast(pos.payload_pos), static_cast(pos.end_pos), pos.payload_size); @@ -116,7 +117,7 @@ bool OBUParser::scan_file(std::ifstream& ifs) { // Create OBU object auto obu = BaseOBU::create(ifs, pos, parse_mode_, active_sequence_header_); if (!obu) { - spdlog::warn("Skipping OBU at position {} (parse failed)", static_cast(record_begin)); + LIB_WARN("Skipping OBU at position {} (parse failed)", static_cast(record_begin)); ifs.seekg(pos.end_pos); obu_index++; continue; @@ -333,7 +334,7 @@ void OBUParser::build_temporal_units() { temporal_units_.push_back(std::move(current_tu)); } - spdlog::debug("Built {} temporal units", temporal_units_.size()); + LIB_DEBUG("Built {} temporal units", temporal_units_.size()); } } // namespace av2_obu diff --git a/src/core/tile_group_header.cpp b/src/core/tile_group_header.cpp index 1e46f46..7928cdc 100644 --- a/src/core/tile_group_header.cpp +++ b/src/core/tile_group_header.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -31,7 +32,7 @@ bool TileGroupHeader::parse_lightweight(BitstreamReader& br, OBUType obu_type, // Parse frame header if present if (frame_header_present_flag) { if (!frame_header.parse_lightweight(br, obu_type, sh)) { - spdlog::error("Failed to parse frame header in tile group"); + LIB_ERROR("Failed to parse frame header in tile group"); return false; } } @@ -49,7 +50,7 @@ bool TileGroupHeader::parse_deep(BitstreamReader& /*br*/, OBUType /*obu_type*/, // deep frame header parse to know TileCols, TileRows, etc. // TODO: Parse bru_tile_active flags // TODO: Parse tile_group_payload (per-tile arithmetic-coded block data) - spdlog::debug("Deep tile group parsing not yet implemented"); + LIB_DEBUG("Deep tile group parsing not yet implemented"); return true; } diff --git a/src/obus/atlas_segment_obu.cpp b/src/obus/atlas_segment_obu.cpp index 9f60528..fd0880f 100644 --- a/src/obus/atlas_segment_obu.cpp +++ b/src/obus/atlas_segment_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -88,11 +89,11 @@ static void parse_label_segment_info(BitstreamReader& br, AtlasSegmentOBU::Label } // namespace bool AtlasSegmentOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Parsing ATLAS_SEGMENT payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing ATLAS_SEGMENT payload ({} bytes)", position_.payload_size); raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read ATLAS_SEGMENT payload"); + LIB_ERROR("Failed to read ATLAS_SEGMENT payload"); return false; } @@ -161,7 +162,7 @@ bool AtlasSegmentOBU::parse_payload(std::ifstream& ifs) { break; } default: - spdlog::warn("ATLAS_SEGMENT: unknown mode_idc={}", static_cast(mode_idc_)); + LIB_WARN("ATLAS_SEGMENT: unknown mode_idc={}", static_cast(mode_idc_)); return false; } @@ -171,11 +172,11 @@ bool AtlasSegmentOBU::parse_payload(std::ifstream& ifs) { return false; } catch (const std::runtime_error& e) { - spdlog::error("ATLAS_SEGMENT parse error: {}", e.what()); + LIB_ERROR("ATLAS_SEGMENT parse error: {}", e.what()); return false; } - spdlog::debug("ATLAS_SEGMENT: id={}, mode={}", atlas_segment_id_, mode_name(mode_idc_)); + LIB_DEBUG("ATLAS_SEGMENT: id={}, mode={}", atlas_segment_id_, mode_name(mode_idc_)); return true; } diff --git a/src/obus/bridge_frame_obu.cpp b/src/obus/bridge_frame_obu.cpp index 6f08d15..149be76 100644 --- a/src/obus/bridge_frame_obu.cpp +++ b/src/obus/bridge_frame_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -18,25 +19,25 @@ namespace av2_obu { bool BridgeFrameOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Parsing BRIDGE_FRAME payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing BRIDGE_FRAME payload ({} bytes)", position_.payload_size); raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read BRIDGE_FRAME payload"); + LIB_ERROR("Failed to read BRIDGE_FRAME payload"); return false; } if (active_seq_header_) { BitstreamReader br(raw_payload_); if (!frame_header_.parse_lightweight(br, OBUType::BRIDGE_FRAME, *active_seq_header_)) { - spdlog::error("Failed to parse BRIDGE_FRAME frame header"); + LIB_ERROR("Failed to parse BRIDGE_FRAME frame header"); return false; } - spdlog::debug("BRIDGE_FRAME: order_hint={}, refresh_flags=0x{:02x}, {}x{}", + LIB_DEBUG("BRIDGE_FRAME: order_hint={}, refresh_flags=0x{:02x}, {}x{}", frame_header_.order_hint, frame_header_.refresh_frame_flags, frame_header_.FrameWidth, frame_header_.FrameHeight); } else { - spdlog::warn("BRIDGE_FRAME: no active sequence header — frame header not parsed"); + LIB_WARN("BRIDGE_FRAME: no active sequence header — frame header not parsed"); } return true; diff --git a/src/obus/buffer_removal_timing_obu.cpp b/src/obus/buffer_removal_timing_obu.cpp index bccca3b..2983f40 100644 --- a/src/obus/buffer_removal_timing_obu.cpp +++ b/src/obus/buffer_removal_timing_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -17,11 +18,11 @@ namespace av2_obu { bool BufferRemovalTimingOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Parsing BUFFER_REMOVAL_TIMING payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing BUFFER_REMOVAL_TIMING payload ({} bytes)", position_.payload_size); raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read BUFFER_REMOVAL_TIMING payload"); + LIB_ERROR("Failed to read BUFFER_REMOVAL_TIMING payload"); return false; } @@ -49,7 +50,7 @@ bool BufferRemovalTimingOBU::parse_payload(std::ifstream& ifs) { if (!parse_obu_trailing_bits(br)) return false; - spdlog::debug("BRT: ops_dependent={}, br_time={}", br_ops_dependent_flag_, br_time_); + LIB_DEBUG("BRT: ops_dependent={}, br_time={}", br_ops_dependent_flag_, br_time_); return true; } diff --git a/src/obus/clk_obu.cpp b/src/obus/clk_obu.cpp index 5675b0b..918985d 100644 --- a/src/obus/clk_obu.cpp +++ b/src/obus/clk_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -18,12 +19,12 @@ namespace av2_obu { bool CLKOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Parsing CLK payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing CLK payload ({} bytes)", position_.payload_size); // Always read raw payload for potential passthrough raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read CLK payload"); + LIB_ERROR("Failed to read CLK payload"); return false; } @@ -31,10 +32,10 @@ bool CLKOBU::parse_payload(std::ifstream& ifs) { if (active_seq_header_) { BitstreamReader br(raw_payload_); if (!tile_group_header_.parse_lightweight(br, OBUType::CLK, *active_seq_header_)) { - spdlog::error("Failed to parse CLK tile group header"); + LIB_ERROR("Failed to parse CLK tile group header"); return false; } - spdlog::debug("CLK: order_hint={}, refresh_flags=0x{:02x}, {}x{}", + LIB_DEBUG("CLK: order_hint={}, refresh_flags=0x{:02x}, {}x{}", tile_group_header_.frame_header.order_hint, tile_group_header_.frame_header.refresh_frame_flags, tile_group_header_.frame_header.FrameWidth, @@ -46,7 +47,7 @@ bool CLKOBU::parse_payload(std::ifstream& ifs) { tile_group_header_.parse_deep(br, OBUType::CLK, *active_seq_header_); } } else { - spdlog::warn("CLK: no active sequence header — frame header not parsed"); + LIB_WARN("CLK: no active sequence header — frame header not parsed"); } return true; diff --git a/src/obus/content_interpretation_obu.cpp b/src/obus/content_interpretation_obu.cpp index e527a58..c038fa4 100644 --- a/src/obus/content_interpretation_obu.cpp +++ b/src/obus/content_interpretation_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -17,12 +18,12 @@ namespace av2_obu { bool ContentInterpretationOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Parsing CONTENT_INTERPRETATION payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing CONTENT_INTERPRETATION payload ({} bytes)", position_.payload_size); // Read raw payload into memory for bitstream parsing raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read CONTENT_INTERPRETATION payload"); + LIB_ERROR("Failed to read CONTENT_INTERPRETATION payload"); return false; } @@ -38,12 +39,12 @@ bool ContentInterpretationOBU::parse_payload(std::ifstream& ifs) { timing_info_present_flag_ = br.read_bit(); reserved_2bit_ = br.read_bits(2); - spdlog::debug(" scan_type_idc = {}", scan_type_idc_); - spdlog::debug(" color_description_present_flag = {}", color_description_present_flag_); - spdlog::debug(" chroma_sample_position_present_flag = {}", + LIB_DEBUG(" scan_type_idc = {}", scan_type_idc_); + LIB_DEBUG(" color_description_present_flag = {}", color_description_present_flag_); + LIB_DEBUG(" chroma_sample_position_present_flag = {}", chroma_sample_position_present_flag_); - spdlog::debug(" aspect_ratio_info_present_flag = {}", aspect_ratio_info_present_flag_); - spdlog::debug(" timing_info_present_flag = {}", timing_info_present_flag_); + LIB_DEBUG(" aspect_ratio_info_present_flag = {}", aspect_ratio_info_present_flag_); + LIB_DEBUG(" timing_info_present_flag = {}", timing_info_present_flag_); // Initialize defaults color_primaries_ = CP_UNSPECIFIED; @@ -53,33 +54,33 @@ bool ContentInterpretationOBU::parse_payload(std::ifstream& ifs) { // Parse color description if present if (color_description_present_flag_) { color_description_idc_ = br.read_rg(2); - spdlog::debug(" color_description_idc = {}", color_description_idc_); + LIB_DEBUG(" color_description_idc = {}", color_description_idc_); if (color_description_idc_ == 0) { color_primaries_ = br.read_bits(8); transfer_characteristics_ = br.read_bits(8); matrix_coefficients_ = br.read_bits(8); - spdlog::debug(" color_primaries = {}", color_primaries_); - spdlog::debug(" transfer_characteristics = {}", transfer_characteristics_); - spdlog::debug(" matrix_coefficients = {}", matrix_coefficients_); + LIB_DEBUG(" color_primaries = {}", color_primaries_); + LIB_DEBUG(" transfer_characteristics = {}", transfer_characteristics_); + LIB_DEBUG(" matrix_coefficients = {}", matrix_coefficients_); } full_range_flag_ = br.read_bit(); - spdlog::debug(" full_range_flag = {}", full_range_flag_); + LIB_DEBUG(" full_range_flag = {}", full_range_flag_); } // Parse chroma sample position if present if (chroma_sample_position_present_flag_) { chroma_sample_position_top_ = br.read_uvlc(); - spdlog::debug(" chroma_sample_position_top = {}", chroma_sample_position_top_); + LIB_DEBUG(" chroma_sample_position_top = {}", chroma_sample_position_top_); if (scan_type_idc_ != 1) { chroma_sample_position_bottom_ = br.read_uvlc(); - spdlog::debug(" chroma_sample_position_bottom = {}", chroma_sample_position_bottom_); + LIB_DEBUG(" chroma_sample_position_bottom = {}", chroma_sample_position_bottom_); } else { chroma_sample_position_bottom_ = chroma_sample_position_top_; - spdlog::debug(" chroma_sample_position_bottom = {} (copied from top)", + LIB_DEBUG(" chroma_sample_position_bottom = {} (copied from top)", chroma_sample_position_bottom_); } } else { @@ -90,22 +91,22 @@ bool ContentInterpretationOBU::parse_payload(std::ifstream& ifs) { // Parse aspect ratio info if present if (aspect_ratio_info_present_flag_) { aspect_ratio_idc_ = br.read_bits(8); - spdlog::debug(" aspect_ratio_idc = {}", aspect_ratio_idc_); + LIB_DEBUG(" aspect_ratio_idc = {}", aspect_ratio_idc_); if (aspect_ratio_idc_ == 255) { // Extended SAR - explicit width and height sar_width_ = br.read_uvlc(); sar_height_ = br.read_uvlc(); - spdlog::debug(" sar_width = {}", sar_width_); - spdlog::debug(" sar_height = {}", sar_height_); + LIB_DEBUG(" sar_width = {}", sar_width_); + LIB_DEBUG(" sar_height = {}", sar_height_); } else if (aspect_ratio_idc_ < 17) { // Predefined aspect ratio from table sar_width_ = ASPECT_RATIO_WIDTH[aspect_ratio_idc_]; sar_height_ = ASPECT_RATIO_HEIGHT[aspect_ratio_idc_]; - spdlog::debug(" sar_width = {} (from table)", sar_width_); - spdlog::debug(" sar_height = {} (from table)", sar_height_); + LIB_DEBUG(" sar_width = {} (from table)", sar_width_); + LIB_DEBUG(" sar_height = {} (from table)", sar_height_); } else { - spdlog::warn(" Invalid aspect_ratio_idc = {} (expected 0-16 or 255)", aspect_ratio_idc_); + LIB_WARN(" Invalid aspect_ratio_idc = {} (expected 0-16 or 255)", aspect_ratio_idc_); sar_width_ = 0; sar_height_ = 0; } @@ -114,7 +115,7 @@ bool ContentInterpretationOBU::parse_payload(std::ifstream& ifs) { // Parse timing info if present if (timing_info_present_flag_) { if (!timing_info_.parse(br)) { - spdlog::error("Failed to parse timing_info"); + LIB_ERROR("Failed to parse timing_info"); return false; } } @@ -123,11 +124,11 @@ bool ContentInterpretationOBU::parse_payload(std::ifstream& ifs) { if (!parse_obu_trailing_bits(br)) return false; - spdlog::debug("Successfully parsed CONTENT_INTERPRETATION OBU"); + LIB_DEBUG("Successfully parsed CONTENT_INTERPRETATION OBU"); return true; } catch (const std::exception& e) { - spdlog::error("Error parsing CONTENT_INTERPRETATION payload: {}", e.what()); + LIB_ERROR("Error parsing CONTENT_INTERPRETATION payload: {}", e.what()); return false; } } diff --git a/src/obus/fgm_obu.cpp b/src/obus/fgm_obu.cpp index ec26992..db6c8ba 100644 --- a/src/obus/fgm_obu.cpp +++ b/src/obus/fgm_obu.cpp @@ -10,23 +10,24 @@ */ #include +#include #include namespace av2_obu { bool FGMOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Parsing FGM payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing FGM payload ({} bytes)", position_.payload_size); // Read raw payload raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read FGM payload"); + LIB_ERROR("Failed to read FGM payload"); return false; } // TODO: Implement FGM parsing - spdlog::warn("FGM parsing not yet implemented"); + LIB_WARN("FGM parsing not yet implemented"); return true; } diff --git a/src/obus/layer_configuration_record_obu.cpp b/src/obus/layer_configuration_record_obu.cpp index 169baf1..5863462 100644 --- a/src/obus/layer_configuration_record_obu.cpp +++ b/src/obus/layer_configuration_record_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -134,11 +135,11 @@ static LCROBU::XLayerInfo parse_xlayer_info(BitstreamReader& br, bool is_global, } bool LayerConfigurationRecordOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Parsing LAYER_CONFIGURATION_RECORD payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing LAYER_CONFIGURATION_RECORD payload ({} bytes)", position_.payload_size); raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read LAYER_CONFIGURATION_RECORD payload"); + LIB_ERROR("Failed to read LAYER_CONFIGURATION_RECORD payload"); return false; } @@ -209,7 +210,7 @@ bool LayerConfigurationRecordOBU::parse_payload(std::ifstream& ifs) { } gp.xlayer_info = parse_xlayer_info(sub_br, true, lcr_global_atlas_id_present_flag_); } catch (const std::exception& e) { - spdlog::warn("LCR: failed to parse xlayer {} payload: {}", gp.xlayer_id, e.what()); + LIB_WARN("LCR: failed to parse xlayer {} payload: {}", gp.xlayer_id, e.what()); } } @@ -243,7 +244,7 @@ bool LayerConfigurationRecordOBU::parse_payload(std::ifstream& ifs) { if (!parse_obu_trailing_bits(br)) return false; - spdlog::debug("LCR: {} mode, xlayer_map=0x{:08x}, {} xlayers", is_global_ ? "global" : "local", + LIB_DEBUG("LCR: {} mode, xlayer_map=0x{:08x}, {} xlayers", is_global_ ? "global" : "local", lcr_xlayer_map_, xlayer_ids_.size()); return true; } diff --git a/src/obus/leading_sef_obu.cpp b/src/obus/leading_sef_obu.cpp index 4caeb4c..dda6710 100644 --- a/src/obus/leading_sef_obu.cpp +++ b/src/obus/leading_sef_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -18,25 +19,25 @@ namespace av2_obu { bool LeadingSEFOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Parsing LEADING_SEF payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing LEADING_SEF payload ({} bytes)", position_.payload_size); raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read LEADING_SEF payload"); + LIB_ERROR("Failed to read LEADING_SEF payload"); return false; } if (active_seq_header_) { BitstreamReader br(raw_payload_); if (!frame_header_.parse_lightweight(br, OBUType::LEADING_SEF, *active_seq_header_)) { - spdlog::error("Failed to parse LEADING_SEF frame header"); + LIB_ERROR("Failed to parse LEADING_SEF frame header"); return false; } - spdlog::debug("LEADING_SEF: show_ref={}, order_hint={}, derive_sef_oh={}", + LIB_DEBUG("LEADING_SEF: show_ref={}, order_hint={}, derive_sef_oh={}", frame_header_.frame_to_show_map_idx, frame_header_.order_hint, frame_header_.derive_sef_order_hint); } else { - spdlog::warn("LEADING_SEF: no active sequence header — frame header not parsed"); + LIB_WARN("LEADING_SEF: no active sequence header — frame header not parsed"); } return true; diff --git a/src/obus/leading_tile_group_obu.cpp b/src/obus/leading_tile_group_obu.cpp index 69c4e21..277731a 100644 --- a/src/obus/leading_tile_group_obu.cpp +++ b/src/obus/leading_tile_group_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -18,11 +19,11 @@ namespace av2_obu { bool LeadingTileGroupOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Parsing LEADING_TILE_GROUP payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing LEADING_TILE_GROUP payload ({} bytes)", position_.payload_size); raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read LEADING_TILE_GROUP payload"); + LIB_ERROR("Failed to read LEADING_TILE_GROUP payload"); return false; } @@ -30,11 +31,11 @@ bool LeadingTileGroupOBU::parse_payload(std::ifstream& ifs) { BitstreamReader br(raw_payload_); if (!tile_group_header_.parse_lightweight(br, OBUType::LEADING_TILE_GROUP, *active_seq_header_)) { - spdlog::error("Failed to parse LEADING_TILE_GROUP tile group header"); + LIB_ERROR("Failed to parse LEADING_TILE_GROUP tile group header"); return false; } if (tile_group_header_.frame_header.parsed) { - spdlog::debug("LEADING_TILE_GROUP: order_hint={}, type={}, refresh_flags=0x{:02x}", + LIB_DEBUG("LEADING_TILE_GROUP: order_hint={}, type={}, refresh_flags=0x{:02x}", tile_group_header_.frame_header.order_hint, tile_group_header_.frame_header.FrameType, tile_group_header_.frame_header.refresh_frame_flags); @@ -46,7 +47,7 @@ bool LeadingTileGroupOBU::parse_payload(std::ifstream& ifs) { tile_group_header_.parse_deep(br, OBUType::LEADING_TILE_GROUP, *active_seq_header_); } } else { - spdlog::warn("LEADING_TILE_GROUP: no active sequence header — frame header not parsed"); + LIB_WARN("LEADING_TILE_GROUP: no active sequence header — frame header not parsed"); } return true; diff --git a/src/obus/leading_tip_obu.cpp b/src/obus/leading_tip_obu.cpp index 83e32d4..3fa1bd9 100644 --- a/src/obus/leading_tip_obu.cpp +++ b/src/obus/leading_tip_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -18,25 +19,25 @@ namespace av2_obu { bool LeadingTIPOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Parsing LEADING_TIP payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing LEADING_TIP payload ({} bytes)", position_.payload_size); raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read LEADING_TIP payload"); + LIB_ERROR("Failed to read LEADING_TIP payload"); return false; } if (active_seq_header_) { BitstreamReader br(raw_payload_); if (!frame_header_.parse_lightweight(br, OBUType::LEADING_TIP, *active_seq_header_)) { - spdlog::error("Failed to parse LEADING_TIP frame header"); + LIB_ERROR("Failed to parse LEADING_TIP frame header"); return false; } - spdlog::debug("LEADING_TIP: order_hint={}, refresh_flags=0x{:02x}, immediate={}", + LIB_DEBUG("LEADING_TIP: order_hint={}, refresh_flags=0x{:02x}, immediate={}", frame_header_.order_hint, frame_header_.refresh_frame_flags, frame_header_.immediate_output_frame); } else { - spdlog::warn("LEADING_TIP: no active sequence header — frame header not parsed"); + LIB_WARN("LEADING_TIP: no active sequence header — frame header not parsed"); } return true; diff --git a/src/obus/metadata_group_obu.cpp b/src/obus/metadata_group_obu.cpp index 96ecdb0..f2279f1 100644 --- a/src/obus/metadata_group_obu.cpp +++ b/src/obus/metadata_group_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -18,11 +19,11 @@ namespace av2_obu { bool MetadataGroupOBU::parse_payload(std::ifstream& ifs) { if (position_.payload_size == 0) { - spdlog::warn("Metadata Group OBU has no payload"); + LIB_WARN("Metadata Group OBU has no payload"); return true; } - spdlog::debug("Parsing metadata group payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing metadata group payload ({} bytes)", position_.payload_size); try { BitstreamReader br(ifs, position_.payload_size); @@ -31,31 +32,31 @@ bool MetadataGroupOBU::parse_payload(std::ifstream& ifs) { metadata_is_suffix_ = (byte1 >> 7) & 0x1; metadata_necessity_idc_ = (byte1 >> 5) & 0x3; metadata_application_id_ = byte1 & 0x1F; - spdlog::debug(" metadata_is_suffix: {}", int(metadata_is_suffix_)); - spdlog::debug(" metadata_necessity_idc: {}", int(metadata_necessity_idc_)); - spdlog::debug(" metadata_application_id: {}", int(metadata_application_id_)); + LIB_DEBUG(" metadata_is_suffix: {}", int(metadata_is_suffix_)); + LIB_DEBUG(" metadata_necessity_idc: {}", int(metadata_necessity_idc_)); + LIB_DEBUG(" metadata_application_id: {}", int(metadata_application_id_)); uint32_t metadata_unit_cnt_minus_1 = br.read_leb128(); metadata_unit_cnt_ = metadata_unit_cnt_minus_1 + 1; - spdlog::debug(" metadata_unit_cnt: {}", metadata_unit_cnt_); + LIB_DEBUG(" metadata_unit_cnt: {}", metadata_unit_cnt_); // Parse each metadata unit units_.clear(); for (uint32_t i = 0; i < metadata_unit_cnt_; ++i) { MetadataUnit unit; - spdlog::debug(" Parsing metadata unit {}", i); + LIB_DEBUG(" Parsing metadata unit {}", i); if (!unit.parse_group_header(br, header_.get_xlayer_id())) { - spdlog::warn("Failed to parse metadata unit {} header", i); + LIB_WARN("Failed to parse metadata unit {} header", i); return true; } // Parse metadata unit payload if not cancelled if (!unit.is_cancelled()) { if (!unit.parse_payload(br)) { - spdlog::warn("Failed to parse metadata unit {} payload", i); + LIB_WARN("Failed to parse metadata unit {} payload", i); } // TODO: Parse mup_extension_bytes } @@ -67,11 +68,11 @@ bool MetadataGroupOBU::parse_payload(std::ifstream& ifs) { if (!parse_obu_trailing_bits(br)) return false; - spdlog::debug("Successfully parsed all {} metadata units", units_.size()); + LIB_DEBUG("Successfully parsed all {} metadata units", units_.size()); return true; } catch (const std::exception& e) { - spdlog::warn("Failed to parse metadata group OBU after {} units ({}), continuing with next OBU", + LIB_WARN("Failed to parse metadata group OBU after {} units ({}), continuing with next OBU", units_.size(), e.what()); return true; // Continue parsing rest of file } diff --git a/src/obus/metadata_obu.cpp b/src/obus/metadata_obu.cpp index 6995f09..92002f1 100644 --- a/src/obus/metadata_obu.cpp +++ b/src/obus/metadata_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -18,29 +19,29 @@ namespace av2_obu { bool MetadataOBU::parse_payload(std::ifstream& ifs) { if (position_.payload_size == 0) { - spdlog::warn("Metadata OBU has no payload"); + LIB_WARN("Metadata OBU has no payload"); return true; } - spdlog::debug("Parsing metadata OBU payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing metadata OBU payload ({} bytes)", position_.payload_size); try { BitstreamReader br(ifs, position_.payload_size); // Parse OBU-level field metadata_is_suffix_ = static_cast(br.read_bits(1)); - spdlog::debug(" metadata_is_suffix: {}", int(metadata_is_suffix_)); + LIB_DEBUG(" metadata_is_suffix: {}", int(metadata_is_suffix_)); // Parse short metadata unit header if (!metadata_unit_.parse_simple_header(br)) { - spdlog::warn("Failed to parse metadata unit header"); + LIB_WARN("Failed to parse metadata unit header"); return true; } // Parse metadata unit payload if not cancelled if (!metadata_unit_.is_cancelled()) { if (!metadata_unit_.parse_payload(br)) { - spdlog::warn("Failed to parse metadata unit payload"); + LIB_WARN("Failed to parse metadata unit payload"); } } @@ -51,7 +52,7 @@ bool MetadataOBU::parse_payload(std::ifstream& ifs) { return true; } catch (const std::exception& e) { - spdlog::warn("Failed to parse metadata OBU ({}), continuing with next OBU", e.what()); + LIB_WARN("Failed to parse metadata OBU ({}), continuing with next OBU", e.what()); return true; } } diff --git a/src/obus/metadata_unit.cpp b/src/obus/metadata_unit.cpp index b712830..11c97c3 100644 --- a/src/obus/metadata_unit.cpp +++ b/src/obus/metadata_unit.cpp @@ -10,6 +10,7 @@ */ #include +#include #include @@ -29,11 +30,11 @@ bool MetadataUnit::parse_simple_header(BitstreamReader& br) { muh_payload_size_ = 0; // Not signaled muh_priority_ = 0; // Default - spdlog::debug(" MetadataUnit (simple header):"); - spdlog::debug(" muh_layer_idc: {}", muh_layer_idc_); - spdlog::debug(" muh_cancel_flag: {}", muh_cancel_flag_); - spdlog::debug(" muh_persistence_idc: {}", muh_persistence_idc_); - spdlog::debug(" metadata_type: {} ({})", metadata_type_, to_string(get_metadata_type())); + LIB_DEBUG(" MetadataUnit (simple header):"); + LIB_DEBUG(" muh_layer_idc: {}", muh_layer_idc_); + LIB_DEBUG(" muh_cancel_flag: {}", muh_cancel_flag_); + LIB_DEBUG(" muh_persistence_idc: {}", muh_persistence_idc_); + LIB_DEBUG(" metadata_type: {} ({})", metadata_type_, to_string(get_metadata_type())); return true; } @@ -48,10 +49,10 @@ bool MetadataUnit::parse_group_header(BitstreamReader& br, uint32_t obu_xlayer_i uint32_t headerRemainingBytes = muh_header_size_; - spdlog::debug(" MetadataUnit (group header):"); - spdlog::debug(" metadata_type: {} ({})", metadata_type_, to_string(get_metadata_type())); - spdlog::debug(" muh_header_size: {}", muh_header_size_); - spdlog::debug(" muh_cancel_flag: {}", muh_cancel_flag_); + LIB_DEBUG(" MetadataUnit (group header):"); + LIB_DEBUG(" metadata_type: {} ({})", metadata_type_, to_string(get_metadata_type())); + LIB_DEBUG(" muh_header_size: {}", muh_header_size_); + LIB_DEBUG(" muh_cancel_flag: {}", muh_cancel_flag_); if (!muh_cancel_flag_) { // Read muh_payload_size @@ -75,10 +76,10 @@ bool MetadataUnit::parse_group_header(BitstreamReader& br, uint32_t obu_xlayer_i headerRemainingBytes -= 2; - spdlog::debug(" muh_payload_size: {}", muh_payload_size_); - spdlog::debug(" muh_layer_idc: {}", muh_layer_idc_); - spdlog::debug(" muh_persistence_idc: {}", muh_persistence_idc_); - spdlog::debug(" muh_priority: {}", muh_priority_); + LIB_DEBUG(" muh_payload_size: {}", muh_payload_size_); + LIB_DEBUG(" muh_layer_idc: {}", muh_layer_idc_); + LIB_DEBUG(" muh_persistence_idc: {}", muh_persistence_idc_); + LIB_DEBUG(" muh_priority: {}", muh_priority_); // Handle layer mapping if (muh_layer_idc_ == static_cast(LayerIdc::LAYER_VALUES)) { @@ -86,7 +87,7 @@ bool MetadataUnit::parse_group_header(BitstreamReader& br, uint32_t obu_xlayer_i muh_xlayer_map_ = static_cast(br.read_bits(32)); headerRemainingBytes -= 4; - spdlog::debug(" muh_xlayer_map: 0x{:08X}", muh_xlayer_map_); + LIB_DEBUG(" muh_xlayer_map: 0x{:08X}", muh_xlayer_map_); for (uint32_t n = 0; n < 31; n++) { if (muh_xlayer_map_ & (0x1 << n)) { @@ -110,7 +111,7 @@ bool MetadataUnit::parse_group_header(BitstreamReader& br, uint32_t obu_xlayer_i } if (!muh_header_extension_bytes_.empty()) { - spdlog::debug(" Read {} header extension bytes", muh_header_extension_bytes_.size()); + LIB_DEBUG(" Read {} header extension bytes", muh_header_extension_bytes_.size()); } return true; @@ -118,7 +119,7 @@ bool MetadataUnit::parse_group_header(BitstreamReader& br, uint32_t obu_xlayer_i bool MetadataUnit::parse_payload(BitstreamReader& br) { MetadataType type = get_metadata_type(); - spdlog::debug(" Parsing metadata_unit payload for type: {}", to_string(type)); + LIB_DEBUG(" Parsing metadata_unit payload for type: {}", to_string(type)); // For metadata group units, we know the exact payload size // Track position to ensure we consume the correct number of bytes @@ -127,20 +128,20 @@ bool MetadataUnit::parse_payload(BitstreamReader& br) { if (has_payload_size) { start_bit_pos = static_cast(br.bits_read()); - spdlog::debug(" Payload size: {} bytes ({} bits)", muh_payload_size_, muh_payload_size_ * 8); + LIB_DEBUG(" Payload size: {} bytes ({} bits)", muh_payload_size_, muh_payload_size_ * 8); } switch (type) { case MetadataType::HDR_CLL: - spdlog::debug(" Parsing HDR_CLL metadata"); + LIB_DEBUG(" Parsing HDR_CLL metadata"); max_cll_ = static_cast(br.read_bits(16)); max_fall_ = static_cast(br.read_bits(16)); - spdlog::debug(" max_cll: {}", max_cll_); - spdlog::debug(" max_fall: {}", max_fall_); + LIB_DEBUG(" max_cll: {}", max_cll_); + LIB_DEBUG(" max_fall: {}", max_fall_); break; case MetadataType::HDR_MDCV: - spdlog::debug(" Parsing HDR_MDCV metadata"); + LIB_DEBUG(" Parsing HDR_MDCV metadata"); for (int i = 0; i < 3; i++) { primary_chromaticity_x_[i] = static_cast(br.read_bits(16)); primary_chromaticity_y_[i] = static_cast(br.read_bits(16)); @@ -150,27 +151,27 @@ bool MetadataUnit::parse_payload(BitstreamReader& br) { luminance_max_ = static_cast(br.read_bits(32)); luminance_min_ = static_cast(br.read_bits(32)); - spdlog::debug(" primary_chromaticity (G): ({}, {})", primary_chromaticity_x_[0], + LIB_DEBUG(" primary_chromaticity (G): ({}, {})", primary_chromaticity_x_[0], primary_chromaticity_y_[0]); - spdlog::debug(" primary_chromaticity (B): ({}, {})", primary_chromaticity_x_[1], + LIB_DEBUG(" primary_chromaticity (B): ({}, {})", primary_chromaticity_x_[1], primary_chromaticity_y_[1]); - spdlog::debug(" primary_chromaticity (R): ({}, {})", primary_chromaticity_x_[2], + LIB_DEBUG(" primary_chromaticity (R): ({}, {})", primary_chromaticity_x_[2], primary_chromaticity_y_[2]); - spdlog::debug(" white_point_chromaticity: ({}, {})", white_point_chromaticity_x_, + LIB_DEBUG(" white_point_chromaticity: ({}, {})", white_point_chromaticity_x_, white_point_chromaticity_y_); - spdlog::debug(" luminance_max: {}", luminance_max_); - spdlog::debug(" luminance_min: {}", luminance_min_); + LIB_DEBUG(" luminance_max: {}", luminance_max_); + LIB_DEBUG(" luminance_min: {}", luminance_min_); break; case MetadataType::SCALABILITY: - spdlog::debug(" TODO: Parse SCALABILITY metadata"); + LIB_DEBUG(" TODO: Parse SCALABILITY metadata"); break; case MetadataType::ITUT_T35: - spdlog::debug(" Parsing ITUT_T35 metadata"); + LIB_DEBUG(" Parsing ITUT_T35 metadata"); { itu_t_t35_country_code_ = static_cast(br.read_bits(8)); - spdlog::debug(" itu_t_t35_country_code: 0x{:02x}", itu_t_t35_country_code_); + LIB_DEBUG(" itu_t_t35_country_code: 0x{:02x}", itu_t_t35_country_code_); uint32_t payload_bytes_remaining = 0; if (has_payload_size && muh_payload_size_ > 0) { @@ -179,7 +180,7 @@ bool MetadataUnit::parse_payload(BitstreamReader& br) { if (itu_t_t35_country_code_ == 0xFF) { itu_t_t35_country_code_extension_byte_ = static_cast(br.read_bits(8)); - spdlog::debug(" itu_t_t35_country_code_extension_byte: 0x{:02x}", + LIB_DEBUG(" itu_t_t35_country_code_extension_byte: 0x{:02x}", itu_t_t35_country_code_extension_byte_); if (payload_bytes_remaining > 0) { payload_bytes_remaining--; @@ -193,7 +194,7 @@ bool MetadataUnit::parse_payload(BitstreamReader& br) { uint8_t provider_low = static_cast(br.read_bits(8)); itu_t_t35_terminal_provider_code_ = (static_cast(provider_high) << 8) | provider_low; - spdlog::debug(" itu_t_t35_terminal_provider_code: 0x{:04x}", + LIB_DEBUG(" itu_t_t35_terminal_provider_code: 0x{:04x}", itu_t_t35_terminal_provider_code_); payload_bytes_remaining -= 2; } @@ -204,81 +205,81 @@ bool MetadataUnit::parse_payload(BitstreamReader& br) { for (uint32_t i = 0; i < payload_bytes_remaining; i++) { itu_t_t35_payload_bytes_.push_back(static_cast(br.read_bits(8))); } - spdlog::debug(" itu_t_t35_payload_bytes: {} bytes", itu_t_t35_payload_bytes_.size()); + LIB_DEBUG(" itu_t_t35_payload_bytes: {} bytes", itu_t_t35_payload_bytes_.size()); } else if (!has_payload_size) { // Without known payload size, we can't safely read the rest - spdlog::debug(" itu_t_t35_payload_bytes: (size unknown, not parsed)"); + LIB_DEBUG(" itu_t_t35_payload_bytes: (size unknown, not parsed)"); } } break; case MetadataType::TIMECODE: - spdlog::debug(" Parsing TIMECODE metadata"); + LIB_DEBUG(" Parsing TIMECODE metadata"); counting_type_ = static_cast(br.read_bits(5)); full_timestamp_flag_ = static_cast(br.read_bits(1)); discontinuity_flag_ = static_cast(br.read_bits(1)); cnt_dropped_flag_ = static_cast(br.read_bits(1)); n_frames_ = static_cast(br.read_bits(9)); - spdlog::debug(" counting_type: {}", int(counting_type_)); - spdlog::debug(" full_timestamp_flag: {}", int(full_timestamp_flag_)); - spdlog::debug(" discontinuity_flag: {}", int(discontinuity_flag_)); - spdlog::debug(" cnt_dropped_flag: {}", int(cnt_dropped_flag_)); - spdlog::debug(" n_frames: {}", n_frames_); + LIB_DEBUG(" counting_type: {}", int(counting_type_)); + LIB_DEBUG(" full_timestamp_flag: {}", int(full_timestamp_flag_)); + LIB_DEBUG(" discontinuity_flag: {}", int(discontinuity_flag_)); + LIB_DEBUG(" cnt_dropped_flag: {}", int(cnt_dropped_flag_)); + LIB_DEBUG(" n_frames: {}", n_frames_); if (full_timestamp_flag_) { seconds_value_ = static_cast(br.read_bits(6)); minutes_value_ = static_cast(br.read_bits(6)); hours_value_ = static_cast(br.read_bits(5)); - spdlog::debug(" seconds_value: {}", int(seconds_value_)); - spdlog::debug(" minutes_value: {}", int(minutes_value_)); - spdlog::debug(" hours_value: {}", int(hours_value_)); + LIB_DEBUG(" seconds_value: {}", int(seconds_value_)); + LIB_DEBUG(" minutes_value: {}", int(minutes_value_)); + LIB_DEBUG(" hours_value: {}", int(hours_value_)); } else { seconds_flag_ = static_cast(br.read_bits(1)); - spdlog::debug(" seconds_flag: {}", int(seconds_flag_)); + LIB_DEBUG(" seconds_flag: {}", int(seconds_flag_)); if (seconds_flag_) { seconds_value_ = static_cast(br.read_bits(6)); minutes_flag_ = static_cast(br.read_bits(1)); - spdlog::debug(" seconds_value: {}", int(seconds_value_)); - spdlog::debug(" minutes_flag: {}", int(minutes_flag_)); + LIB_DEBUG(" seconds_value: {}", int(seconds_value_)); + LIB_DEBUG(" minutes_flag: {}", int(minutes_flag_)); if (minutes_flag_) { minutes_value_ = static_cast(br.read_bits(6)); hours_flag_ = static_cast(br.read_bits(1)); - spdlog::debug(" minutes_value: {}", int(minutes_value_)); - spdlog::debug(" hours_flag: {}", int(hours_flag_)); + LIB_DEBUG(" minutes_value: {}", int(minutes_value_)); + LIB_DEBUG(" hours_flag: {}", int(hours_flag_)); if (hours_flag_) { hours_value_ = static_cast(br.read_bits(5)); - spdlog::debug(" hours_value: {}", int(hours_value_)); + LIB_DEBUG(" hours_value: {}", int(hours_value_)); } } } } time_offset_length_ = static_cast(br.read_bits(5)); - spdlog::debug(" time_offset_length: {}", int(time_offset_length_)); + LIB_DEBUG(" time_offset_length: {}", int(time_offset_length_)); if (time_offset_length_ > 0) { time_offset_value_ = static_cast(br.read_bits(time_offset_length_)); - spdlog::debug(" time_offset_value: {}", time_offset_value_); + LIB_DEBUG(" time_offset_value: {}", time_offset_value_); } break; case MetadataType::BANDING_HINTS: - spdlog::debug(" Parsing BANDING_HINTS metadata"); + LIB_DEBUG(" Parsing BANDING_HINTS metadata"); { coding_banding_present_flag_ = static_cast(br.read_bits(1)); source_banding_present_flag_ = static_cast(br.read_bits(1)); - spdlog::debug(" coding_banding_present_flag: {}", int(coding_banding_present_flag_)); - spdlog::debug(" source_banding_present_flag: {}", int(source_banding_present_flag_)); + LIB_DEBUG(" coding_banding_present_flag: {}", int(coding_banding_present_flag_)); + LIB_DEBUG(" source_banding_present_flag: {}", int(source_banding_present_flag_)); if (coding_banding_present_flag_) { banding_hints_flag_ = static_cast(br.read_bits(1)); - spdlog::debug(" banding_hints_flag: {}", int(banding_hints_flag_)); + LIB_DEBUG(" banding_hints_flag: {}", int(banding_hints_flag_)); if (banding_hints_flag_) { three_color_components_ = static_cast(br.read_bits(1)); uint32_t numComponents = three_color_components_ ? 3 : 1; - spdlog::debug(" three_color_components: {} ({} components)", + LIB_DEBUG(" three_color_components: {} ({} components)", int(three_color_components_), numComponents); banding_components_.clear(); @@ -289,17 +290,17 @@ bool MetadataUnit::parse_payload(BitstreamReader& br) { if (comp.banding_in_component_present_flag) { comp.max_band_width_minus_4 = static_cast(br.read_bits(6)); comp.max_band_step_minus_1 = static_cast(br.read_bits(4)); - spdlog::debug(" component[{}]: present, width={}, step={}", plane, + LIB_DEBUG(" component[{}]: present, width={}, step={}", plane, comp.max_band_width_minus_4, comp.max_band_step_minus_1); } else { - spdlog::debug(" component[{}]: not present", plane); + LIB_DEBUG(" component[{}]: not present", plane); } banding_components_.push_back(comp); } band_units_information_present_flag_ = static_cast(br.read_bits(1)); - spdlog::debug(" band_units_information_present_flag: {}", + LIB_DEBUG(" band_units_information_present_flag: {}", int(band_units_information_present_flag_)); if (band_units_information_present_flag_) { @@ -307,16 +308,16 @@ bool MetadataUnit::parse_payload(BitstreamReader& br) { num_band_units_cols_minus_1_ = static_cast(br.read_bits(5)); varying_size_band_units_flag_ = static_cast(br.read_bits(1)); - spdlog::debug(" num_band_units_rows_minus_1: {}", + LIB_DEBUG(" num_band_units_rows_minus_1: {}", int(num_band_units_rows_minus_1_)); - spdlog::debug(" num_band_units_cols_minus_1: {}", + LIB_DEBUG(" num_band_units_cols_minus_1: {}", int(num_band_units_cols_minus_1_)); - spdlog::debug(" varying_size_band_units_flag: {}", + LIB_DEBUG(" varying_size_band_units_flag: {}", int(varying_size_band_units_flag_)); if (varying_size_band_units_flag_) { band_block_in_luma_samples_ = static_cast(br.read_bits(3)); - spdlog::debug(" band_block_in_luma_samples: {}", + LIB_DEBUG(" band_block_in_luma_samples: {}", int(band_block_in_luma_samples_)); // Read vertical sizes @@ -333,7 +334,7 @@ bool MetadataUnit::parse_payload(BitstreamReader& br) { horz_size_in_band_blocks_minus_1_.push_back(horz_size); } - spdlog::debug(" Read {} vertical and {} horizontal band block sizes", + LIB_DEBUG(" Read {} vertical and {} horizontal band block sizes", vert_size_in_band_blocks_minus_1_.size(), horz_size_in_band_blocks_minus_1_.size()); } @@ -351,7 +352,7 @@ bool MetadataUnit::parse_payload(BitstreamReader& br) { uint32_t total_units = (num_band_units_rows_minus_1_ + 1) * (num_band_units_cols_minus_1_ + 1); - spdlog::debug(" Read banding flags for {} band units ({} rows x {} cols)", + LIB_DEBUG(" Read banding flags for {} band units ({} rows x {} cols)", total_units, num_band_units_rows_minus_1_ + 1, num_band_units_cols_minus_1_ + 1); } @@ -361,7 +362,7 @@ bool MetadataUnit::parse_payload(BitstreamReader& br) { break; case MetadataType::ICC_PROFILE: - spdlog::debug(" Parsing ICC_PROFILE metadata"); + LIB_DEBUG(" Parsing ICC_PROFILE metadata"); { // ICC profile is just raw payload bytes // For group OBUs, we know the exact size @@ -370,26 +371,26 @@ bool MetadataUnit::parse_payload(BitstreamReader& br) { for (uint32_t i = 0; i < muh_payload_size_; i++) { icc_profile_data_payload_bytes_.push_back(static_cast(br.read_bits(8))); } - spdlog::debug(" icc_profile_data: {} bytes", icc_profile_data_payload_bytes_.size()); + LIB_DEBUG(" icc_profile_data: {} bytes", icc_profile_data_payload_bytes_.size()); } else { // Without known payload size, we can't safely read the profile - spdlog::debug(" icc_profile_data: (size unknown, not parsed)"); + LIB_DEBUG(" icc_profile_data: (size unknown, not parsed)"); } } break; case MetadataType::SCAN_TYPE: - spdlog::debug(" Parsing SCAN_TYPE metadata"); + LIB_DEBUG(" Parsing SCAN_TYPE metadata"); mps_pic_struct_type_ = static_cast(br.read_bits(5)); mps_source_scan_type_idc_ = static_cast(br.read_bits(2)); mps_duplicate_flag_ = static_cast(br.read_bits(1)); - spdlog::debug(" mps_pic_struct_type: {}", int(mps_pic_struct_type_)); - spdlog::debug(" mps_source_scan_type_idc: {}", int(mps_source_scan_type_idc_)); - spdlog::debug(" mps_duplicate_flag: {}", int(mps_duplicate_flag_)); + LIB_DEBUG(" mps_pic_struct_type: {}", int(mps_pic_struct_type_)); + LIB_DEBUG(" mps_source_scan_type_idc: {}", int(mps_source_scan_type_idc_)); + LIB_DEBUG(" mps_duplicate_flag: {}", int(mps_duplicate_flag_)); break; case MetadataType::HASH: - spdlog::debug(" Parsing HASH metadata (decoded frame hash)"); + LIB_DEBUG(" Parsing HASH metadata (decoded frame hash)"); { // Parse header byte hash_type_ = static_cast(br.read_bits(4)); @@ -398,11 +399,11 @@ bool MetadataUnit::parse_payload(BitstreamReader& br) { is_monochrome_ = static_cast(br.read_bits(1)); hash_reserved_ = static_cast(br.read_bits(1)); - spdlog::debug(" hash_type: {}", int(hash_type_)); - spdlog::debug(" per_plane: {}", int(per_plane_)); - spdlog::debug(" has_grain: {}", int(has_grain_)); - spdlog::debug(" is_monochrome: {}", int(is_monochrome_)); - spdlog::debug(" reserved: {}", int(hash_reserved_)); + LIB_DEBUG(" hash_type: {}", int(hash_type_)); + LIB_DEBUG(" per_plane: {}", int(per_plane_)); + LIB_DEBUG(" has_grain: {}", int(has_grain_)); + LIB_DEBUG(" is_monochrome: {}", int(is_monochrome_)); + LIB_DEBUG(" reserved: {}", int(hash_reserved_)); // Determine number of hashes to read uint32_t num_hashes = 1; // Default: single frame_hash @@ -410,9 +411,9 @@ bool MetadataUnit::parse_payload(BitstreamReader& br) { // Per-plane hashes: numPlanes = is_monochrome ? 1 : 3 uint32_t num_planes = is_monochrome_ ? 1 : 3; num_hashes = num_planes; - spdlog::debug(" num_planes: {}", num_planes); + LIB_DEBUG(" num_planes: {}", num_planes); } else { - spdlog::debug(" Single frame_hash (all planes combined)"); + LIB_DEBUG(" Single frame_hash (all planes combined)"); } // Read hashes (16 bytes each, little-endian) @@ -426,7 +427,7 @@ bool MetadataUnit::parse_payload(BitstreamReader& br) { // Log hash in hex format const char* hash_label = per_plane_ ? "plane_hash" : "frame_hash"; - spdlog::debug( + LIB_DEBUG( " {}[{}]: " "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:" "02x}{:02x}", @@ -437,19 +438,19 @@ bool MetadataUnit::parse_payload(BitstreamReader& br) { break; case MetadataType::TEMPORAL_POINT_INFO: - spdlog::debug(" Parsing TEMPORAL_POINT_INFO metadata"); + LIB_DEBUG(" Parsing TEMPORAL_POINT_INFO metadata"); frame_presentation_time_length_minus_1_ = static_cast(br.read_bits(5)); { uint32_t n = frame_presentation_time_length_minus_1_ + 1; frame_presentation_time_ = static_cast(br.read_bits(n)); - spdlog::debug(" frame_presentation_time_length_minus_1: {}", + LIB_DEBUG(" frame_presentation_time_length_minus_1: {}", int(frame_presentation_time_length_minus_1_)); - spdlog::debug(" frame_presentation_time: {} ({} bits)", frame_presentation_time_, n); + LIB_DEBUG(" frame_presentation_time: {} ({} bits)", frame_presentation_time_, n); } break; default: - spdlog::debug(" Unknown or reserved metadata type"); + LIB_DEBUG(" Unknown or reserved metadata type"); break; } @@ -461,14 +462,14 @@ bool MetadataUnit::parse_payload(BitstreamReader& br) { // Check for non-byte-aligned parsing if (bits_consumed % 8 != 0) { - spdlog::debug( + LIB_DEBUG( " Metadata payload parsing ended at non-byte-aligned position ({} bits, {} remainder)", bits_consumed, bits_consumed % 8); } if (bits_consumed < expected_bits) { uint32_t bits_to_skip = expected_bits - bits_consumed; - spdlog::debug(" Skipping {} remaining payload bits ({} bytes)", bits_to_skip, + LIB_DEBUG(" Skipping {} remaining payload bits ({} bytes)", bits_to_skip, bits_to_skip / 8); // Skip remaining bits @@ -480,90 +481,90 @@ bool MetadataUnit::parse_payload(BitstreamReader& br) { br.read_bits(bits_to_skip); } } else if (bits_consumed > expected_bits) { - spdlog::warn(" Consumed {} bits but expected {} bits - payload parsing may be incorrect", + LIB_WARN(" Consumed {} bits but expected {} bits - payload parsing may be incorrect", bits_consumed, expected_bits); return false; } - spdlog::debug(" Successfully consumed {} bytes of payload", muh_payload_size_); + LIB_DEBUG(" Successfully consumed {} bytes of payload", muh_payload_size_); } return true; } void MetadataUnit::dump() const { - spdlog::debug(" MetadataUnit {{"); - spdlog::debug(" metadata_type: {} ({})", metadata_type_, to_string(get_metadata_type())); - spdlog::debug(" muh_header_size: {}", muh_header_size_); - spdlog::debug(" muh_cancel_flag: {}", muh_cancel_flag_); + LIB_DEBUG(" MetadataUnit {{"); + LIB_DEBUG(" metadata_type: {} ({})", metadata_type_, to_string(get_metadata_type())); + LIB_DEBUG(" muh_header_size: {}", muh_header_size_); + LIB_DEBUG(" muh_cancel_flag: {}", muh_cancel_flag_); if (!muh_cancel_flag_) { if (muh_payload_size_ > 0) { - spdlog::debug(" muh_payload_size: {}", muh_payload_size_); + LIB_DEBUG(" muh_payload_size: {}", muh_payload_size_); } - spdlog::debug(" muh_layer_idc: {}", muh_layer_idc_); - spdlog::debug(" muh_persistence_idc: {}", muh_persistence_idc_); + LIB_DEBUG(" muh_layer_idc: {}", muh_layer_idc_); + LIB_DEBUG(" muh_persistence_idc: {}", muh_persistence_idc_); if (muh_priority_ > 0) { - spdlog::debug(" muh_priority: {}", muh_priority_); + LIB_DEBUG(" muh_priority: {}", muh_priority_); } // Display metadata payload fields MetadataType type = get_metadata_type(); if (type == MetadataType::HDR_CLL) { - spdlog::debug(" max_cll: {}", max_cll_); - spdlog::debug(" max_fall: {}", max_fall_); + LIB_DEBUG(" max_cll: {}", max_cll_); + LIB_DEBUG(" max_fall: {}", max_fall_); } else if (type == MetadataType::HDR_MDCV) { - spdlog::debug(" primary_chromaticity (G): ({}, {})", primary_chromaticity_x_[0], + LIB_DEBUG(" primary_chromaticity (G): ({}, {})", primary_chromaticity_x_[0], primary_chromaticity_y_[0]); - spdlog::debug(" primary_chromaticity (B): ({}, {})", primary_chromaticity_x_[1], + LIB_DEBUG(" primary_chromaticity (B): ({}, {})", primary_chromaticity_x_[1], primary_chromaticity_y_[1]); - spdlog::debug(" primary_chromaticity (R): ({}, {})", primary_chromaticity_x_[2], + LIB_DEBUG(" primary_chromaticity (R): ({}, {})", primary_chromaticity_x_[2], primary_chromaticity_y_[2]); - spdlog::debug(" white_point_chromaticity: ({}, {})", white_point_chromaticity_x_, + LIB_DEBUG(" white_point_chromaticity: ({}, {})", white_point_chromaticity_x_, white_point_chromaticity_y_); - spdlog::debug(" luminance_max: {}", luminance_max_); - spdlog::debug(" luminance_min: {}", luminance_min_); + LIB_DEBUG(" luminance_max: {}", luminance_max_); + LIB_DEBUG(" luminance_min: {}", luminance_min_); } else if (type == MetadataType::ITUT_T35) { - spdlog::debug(" itu_t_t35_country_code: 0x{:02x}", itu_t_t35_country_code_); + LIB_DEBUG(" itu_t_t35_country_code: 0x{:02x}", itu_t_t35_country_code_); if (itu_t_t35_country_code_ == 0xFF) { - spdlog::debug(" itu_t_t35_country_code_extension_byte: 0x{:02x}", + LIB_DEBUG(" itu_t_t35_country_code_extension_byte: 0x{:02x}", itu_t_t35_country_code_extension_byte_); } if (itu_t_t35_terminal_provider_code_ != 0) { - spdlog::debug(" itu_t_t35_terminal_provider_code: 0x{:04x}", + LIB_DEBUG(" itu_t_t35_terminal_provider_code: 0x{:04x}", itu_t_t35_terminal_provider_code_); } if (!itu_t_t35_payload_bytes_.empty()) { - spdlog::debug(" itu_t_t35_payload_bytes: {} bytes", itu_t_t35_payload_bytes_.size()); + LIB_DEBUG(" itu_t_t35_payload_bytes: {} bytes", itu_t_t35_payload_bytes_.size()); } } else if (type == MetadataType::TIMECODE) { - spdlog::debug(" counting_type: {}", int(counting_type_)); - spdlog::debug(" full_timestamp_flag: {}", int(full_timestamp_flag_)); - spdlog::debug(" discontinuity_flag: {}", int(discontinuity_flag_)); - spdlog::debug(" cnt_dropped_flag: {}", int(cnt_dropped_flag_)); - spdlog::debug(" n_frames: {}", n_frames_); + LIB_DEBUG(" counting_type: {}", int(counting_type_)); + LIB_DEBUG(" full_timestamp_flag: {}", int(full_timestamp_flag_)); + LIB_DEBUG(" discontinuity_flag: {}", int(discontinuity_flag_)); + LIB_DEBUG(" cnt_dropped_flag: {}", int(cnt_dropped_flag_)); + LIB_DEBUG(" n_frames: {}", n_frames_); if (full_timestamp_flag_ || seconds_flag_) { - spdlog::debug(" seconds_value: {}", int(seconds_value_)); + LIB_DEBUG(" seconds_value: {}", int(seconds_value_)); } if (full_timestamp_flag_ || minutes_flag_) { - spdlog::debug(" minutes_value: {}", int(minutes_value_)); + LIB_DEBUG(" minutes_value: {}", int(minutes_value_)); } if (full_timestamp_flag_ || hours_flag_) { - spdlog::debug(" hours_value: {}", int(hours_value_)); + LIB_DEBUG(" hours_value: {}", int(hours_value_)); } if (time_offset_length_ > 0) { - spdlog::debug(" time_offset_length: {}", int(time_offset_length_)); - spdlog::debug(" time_offset_value: {}", time_offset_value_); + LIB_DEBUG(" time_offset_length: {}", int(time_offset_length_)); + LIB_DEBUG(" time_offset_value: {}", time_offset_value_); } } else if (type == MetadataType::HASH) { - spdlog::debug(" hash_type: {}", int(hash_type_)); - spdlog::debug(" per_plane: {}", int(per_plane_)); - spdlog::debug(" has_grain: {}", int(has_grain_)); - spdlog::debug(" is_monochrome: {}", int(is_monochrome_)); + LIB_DEBUG(" hash_type: {}", int(hash_type_)); + LIB_DEBUG(" per_plane: {}", int(per_plane_)); + LIB_DEBUG(" has_grain: {}", int(has_grain_)); + LIB_DEBUG(" is_monochrome: {}", int(is_monochrome_)); const char* hash_label = per_plane_ ? "plane_hash" : "frame_hash"; for (size_t i = 0; i < hashes_.size(); i++) { const auto& hash = hashes_[i]; - spdlog::debug( + LIB_DEBUG( " {}[{}]: " "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:" "02x}{:02x}", @@ -572,36 +573,36 @@ void MetadataUnit::dump() const { } } else if (type == MetadataType::ICC_PROFILE) { if (!icc_profile_data_payload_bytes_.empty()) { - spdlog::debug(" icc_profile_data: {} bytes", icc_profile_data_payload_bytes_.size()); + LIB_DEBUG(" icc_profile_data: {} bytes", icc_profile_data_payload_bytes_.size()); } } else if (type == MetadataType::SCAN_TYPE) { - spdlog::debug(" mps_pic_struct_type: {}", int(mps_pic_struct_type_)); - spdlog::debug(" mps_source_scan_type_idc: {}", int(mps_source_scan_type_idc_)); - spdlog::debug(" mps_duplicate_flag: {}", int(mps_duplicate_flag_)); + LIB_DEBUG(" mps_pic_struct_type: {}", int(mps_pic_struct_type_)); + LIB_DEBUG(" mps_source_scan_type_idc: {}", int(mps_source_scan_type_idc_)); + LIB_DEBUG(" mps_duplicate_flag: {}", int(mps_duplicate_flag_)); } else if (type == MetadataType::TEMPORAL_POINT_INFO) { - spdlog::debug(" frame_presentation_time_length_minus_1: {}", + LIB_DEBUG(" frame_presentation_time_length_minus_1: {}", int(frame_presentation_time_length_minus_1_)); - spdlog::debug(" frame_presentation_time: {}", frame_presentation_time_); + LIB_DEBUG(" frame_presentation_time: {}", frame_presentation_time_); } else if (type == MetadataType::BANDING_HINTS) { - spdlog::debug(" coding_banding_present_flag: {}", int(coding_banding_present_flag_)); - spdlog::debug(" source_banding_present_flag: {}", int(source_banding_present_flag_)); + LIB_DEBUG(" coding_banding_present_flag: {}", int(coding_banding_present_flag_)); + LIB_DEBUG(" source_banding_present_flag: {}", int(source_banding_present_flag_)); if (coding_banding_present_flag_) { - spdlog::debug(" banding_hints_flag: {}", int(banding_hints_flag_)); + LIB_DEBUG(" banding_hints_flag: {}", int(banding_hints_flag_)); if (banding_hints_flag_) { - spdlog::debug(" three_color_components: {} ({} components)", + LIB_DEBUG(" three_color_components: {} ({} components)", int(three_color_components_), three_color_components_ ? 3 : 1); for (size_t i = 0; i < banding_components_.size(); i++) { const auto& comp = banding_components_[i]; if (comp.banding_in_component_present_flag) { - spdlog::debug(" component[{}]: width={}, step={}", i, comp.max_band_width_minus_4, + LIB_DEBUG(" component[{}]: width={}, step={}", i, comp.max_band_width_minus_4, comp.max_band_step_minus_1); } } if (band_units_information_present_flag_) { - spdlog::debug(" band_units: {} rows x {} cols", num_band_units_rows_minus_1_ + 1, + LIB_DEBUG(" band_units: {} rows x {} cols", num_band_units_rows_minus_1_ + 1, num_band_units_cols_minus_1_ + 1); if (varying_size_band_units_flag_) { - spdlog::debug(" varying_size with {} vertical and {} horizontal sizes", + LIB_DEBUG(" varying_size with {} vertical and {} horizontal sizes", vert_size_in_band_blocks_minus_1_.size(), horz_size_in_band_blocks_minus_1_.size()); } @@ -611,7 +612,7 @@ void MetadataUnit::dump() const { } } - spdlog::debug(" }}"); + LIB_DEBUG(" }}"); } nlohmann::ordered_json MetadataUnit::to_json() const { diff --git a/src/obus/msdo_obu.cpp b/src/obus/msdo_obu.cpp index f9bb41e..0555a6b 100644 --- a/src/obus/msdo_obu.cpp +++ b/src/obus/msdo_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -17,11 +18,11 @@ namespace av2_obu { bool MSDOOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Parsing MSDO payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing MSDO payload ({} bytes)", position_.payload_size); raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read MSDO payload"); + LIB_ERROR("Failed to read MSDO payload"); return false; } @@ -52,7 +53,7 @@ bool MSDOOBU::parse_payload(std::ifstream& ifs) { if (!parse_obu_trailing_bits(br)) return false; - spdlog::debug("MSDO: {} streams", num_streams_); + LIB_DEBUG("MSDO: {} streams", num_streams_); return true; } diff --git a/src/obus/multi_frame_header_obu.cpp b/src/obus/multi_frame_header_obu.cpp index 0412070..51076f3 100644 --- a/src/obus/multi_frame_header_obu.cpp +++ b/src/obus/multi_frame_header_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -17,11 +18,11 @@ namespace av2_obu { bool MultiFrameHeaderOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Parsing MULTI_FRAME_HEADER OBU payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing MULTI_FRAME_HEADER OBU payload ({} bytes)", position_.payload_size); raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read MULTI_FRAME_HEADER payload"); + LIB_ERROR("Failed to read MULTI_FRAME_HEADER payload"); return false; } @@ -55,10 +56,10 @@ bool MultiFrameHeaderOBU::parse_payload(std::ifstream& ifs) { // TODO: Parse seg_info() for deep mode // For lightweight mode, just note presence — seg_info() is complex // and not needed for packaging. - spdlog::debug("MFH: seg_info present but not parsed (lightweight mode)"); + LIB_DEBUG("MFH: seg_info present but not parsed (lightweight mode)"); } - spdlog::debug("MFH: id={}, seq_header_id={}, frame_size={}", + LIB_DEBUG("MFH: id={}, seq_header_id={}, frame_size={}", mfh_id_, mfh_seq_header_id_, mfh_frame_size_present_flag_ ? std::to_string(mfh_frame_width_) + "x" + std::to_string(mfh_frame_height_) diff --git a/src/obus/olk_obu.cpp b/src/obus/olk_obu.cpp index 3df0d7f..33d43e3 100644 --- a/src/obus/olk_obu.cpp +++ b/src/obus/olk_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -18,12 +19,12 @@ namespace av2_obu { bool OLKOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Parsing OLK payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing OLK payload ({} bytes)", position_.payload_size); // Always read raw payload for potential passthrough raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read OLK payload"); + LIB_ERROR("Failed to read OLK payload"); return false; } @@ -31,10 +32,10 @@ bool OLKOBU::parse_payload(std::ifstream& ifs) { if (active_seq_header_) { BitstreamReader br(raw_payload_); if (!tile_group_header_.parse_lightweight(br, OBUType::OLK, *active_seq_header_)) { - spdlog::error("Failed to parse OLK tile group header"); + LIB_ERROR("Failed to parse OLK tile group header"); return false; } - spdlog::debug("OLK: order_hint={}, refresh_flags=0x{:02x}, {}x{}", + LIB_DEBUG("OLK: order_hint={}, refresh_flags=0x{:02x}, {}x{}", tile_group_header_.frame_header.order_hint, tile_group_header_.frame_header.refresh_frame_flags, tile_group_header_.frame_header.FrameWidth, @@ -46,7 +47,7 @@ bool OLKOBU::parse_payload(std::ifstream& ifs) { tile_group_header_.parse_deep(br, OBUType::OLK, *active_seq_header_); } } else { - spdlog::warn("OLK: no active sequence header — frame header not parsed"); + LIB_WARN("OLK: no active sequence header — frame header not parsed"); } return true; diff --git a/src/obus/operating_point_set_obu.cpp b/src/obus/operating_point_set_obu.cpp index a84a7ba..71721d9 100644 --- a/src/obus/operating_point_set_obu.cpp +++ b/src/obus/operating_point_set_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -77,11 +78,11 @@ static OPS::MlayerEntry parse_mlayer_info(BitstreamReader& br, uint32_t xlayer_i } bool OperatingPointSetOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Parsing OPERATING_POINT_SET payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing OPERATING_POINT_SET payload ({} bytes)", position_.payload_size); raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read OPERATING_POINT_SET payload"); + LIB_ERROR("Failed to read OPERATING_POINT_SET payload"); return false; } @@ -183,7 +184,7 @@ bool OperatingPointSetOBU::parse_payload(std::ifstream& ifs) { // Advance to exact end of this OP payload using data_size size_t end_pos = start_pos + static_cast(op.ops_data_size) * 8; if (br.bits_read() != end_pos) { - spdlog::debug("OPS: OP {} consumed {} bits, data_size={} bytes ({} bits)", i, + LIB_DEBUG("OPS: OP {} consumed {} bits, data_size={} bytes ({} bits)", i, br.bits_read() - start_pos, op.ops_data_size, end_pos - start_pos); br.set_bit_pos(end_pos); } @@ -194,7 +195,7 @@ bool OperatingPointSetOBU::parse_payload(std::ifstream& ifs) { if (!parse_obu_trailing_bits(br)) return false; - spdlog::debug("OPS: id={}, cnt={}, intent={}, reset={}", ops_id_, ops_cnt_, ops_intent_, + LIB_DEBUG("OPS: id={}, cnt={}, intent={}, reset={}", ops_id_, ops_cnt_, ops_intent_, ops_reset_flag_); return true; } diff --git a/src/obus/padding_obu.cpp b/src/obus/padding_obu.cpp index 6c0e684..4b8f2ba 100644 --- a/src/obus/padding_obu.cpp +++ b/src/obus/padding_obu.cpp @@ -10,13 +10,14 @@ */ #include +#include #include namespace av2_obu { bool PaddingOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Skipping padding payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Skipping padding payload ({} bytes)", position_.payload_size); return skip_payload(ifs); } diff --git a/src/obus/qm_obu.cpp b/src/obus/qm_obu.cpp index 2d2fbd1..c37d708 100644 --- a/src/obus/qm_obu.cpp +++ b/src/obus/qm_obu.cpp @@ -10,23 +10,24 @@ */ #include +#include #include namespace av2_obu { bool QMOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Parsing QM payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing QM payload ({} bytes)", position_.payload_size); // Read raw payload raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read QM payload"); + LIB_ERROR("Failed to read QM payload"); return false; } // TODO: Implement QM parsing - spdlog::warn("QM parsing not yet implemented"); + LIB_WARN("QM parsing not yet implemented"); return true; } diff --git a/src/obus/ras_frame_obu.cpp b/src/obus/ras_frame_obu.cpp index 05272bb..a9a530d 100644 --- a/src/obus/ras_frame_obu.cpp +++ b/src/obus/ras_frame_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -18,22 +19,22 @@ namespace av2_obu { bool RASFrameOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Parsing RAS_FRAME payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing RAS_FRAME payload ({} bytes)", position_.payload_size); raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read RAS_FRAME payload"); + LIB_ERROR("Failed to read RAS_FRAME payload"); return false; } if (active_seq_header_) { BitstreamReader br(raw_payload_); if (!tile_group_header_.parse_lightweight(br, OBUType::RAS_FRAME, *active_seq_header_)) { - spdlog::error("Failed to parse RAS_FRAME tile group header"); + LIB_ERROR("Failed to parse RAS_FRAME tile group header"); return false; } if (tile_group_header_.frame_header.parsed) { - spdlog::debug("RAS_FRAME: order_hint={}, refresh_flags=0x{:02x}", + LIB_DEBUG("RAS_FRAME: order_hint={}, refresh_flags=0x{:02x}", tile_group_header_.frame_header.order_hint, tile_group_header_.frame_header.refresh_frame_flags); } @@ -43,7 +44,7 @@ bool RASFrameOBU::parse_payload(std::ifstream& ifs) { tile_group_header_.parse_deep(br, OBUType::RAS_FRAME, *active_seq_header_); } } else { - spdlog::warn("RAS_FRAME: no active sequence header — frame header not parsed"); + LIB_WARN("RAS_FRAME: no active sequence header — frame header not parsed"); } return true; diff --git a/src/obus/regular_sef_obu.cpp b/src/obus/regular_sef_obu.cpp index f8f2242..5ce5c70 100644 --- a/src/obus/regular_sef_obu.cpp +++ b/src/obus/regular_sef_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -18,25 +19,25 @@ namespace av2_obu { bool RegularSEFOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Parsing REGULAR_SEF payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing REGULAR_SEF payload ({} bytes)", position_.payload_size); raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read REGULAR_SEF payload"); + LIB_ERROR("Failed to read REGULAR_SEF payload"); return false; } if (active_seq_header_) { BitstreamReader br(raw_payload_); if (!frame_header_.parse_lightweight(br, OBUType::REGULAR_SEF, *active_seq_header_)) { - spdlog::error("Failed to parse REGULAR_SEF frame header"); + LIB_ERROR("Failed to parse REGULAR_SEF frame header"); return false; } - spdlog::debug("REGULAR_SEF: show_ref={}, order_hint={}, derive_sef_oh={}", + LIB_DEBUG("REGULAR_SEF: show_ref={}, order_hint={}, derive_sef_oh={}", frame_header_.frame_to_show_map_idx, frame_header_.order_hint, frame_header_.derive_sef_order_hint); } else { - spdlog::warn("REGULAR_SEF: no active sequence header — frame header not parsed"); + LIB_WARN("REGULAR_SEF: no active sequence header — frame header not parsed"); } return true; diff --git a/src/obus/regular_tile_group_obu.cpp b/src/obus/regular_tile_group_obu.cpp index 4573889..25ffc86 100644 --- a/src/obus/regular_tile_group_obu.cpp +++ b/src/obus/regular_tile_group_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -18,11 +19,11 @@ namespace av2_obu { bool RegularTileGroupOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Parsing REGULAR_TILE_GROUP payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing REGULAR_TILE_GROUP payload ({} bytes)", position_.payload_size); raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read REGULAR_TILE_GROUP payload"); + LIB_ERROR("Failed to read REGULAR_TILE_GROUP payload"); return false; } @@ -30,11 +31,11 @@ bool RegularTileGroupOBU::parse_payload(std::ifstream& ifs) { BitstreamReader br(raw_payload_); if (!tile_group_header_.parse_lightweight(br, OBUType::REGULAR_TILE_GROUP, *active_seq_header_)) { - spdlog::error("Failed to parse REGULAR_TILE_GROUP tile group header"); + LIB_ERROR("Failed to parse REGULAR_TILE_GROUP tile group header"); return false; } if (tile_group_header_.frame_header.parsed) { - spdlog::debug("REGULAR_TILE_GROUP: order_hint={}, type={}, refresh_flags=0x{:02x}", + LIB_DEBUG("REGULAR_TILE_GROUP: order_hint={}, type={}, refresh_flags=0x{:02x}", tile_group_header_.frame_header.order_hint, tile_group_header_.frame_header.FrameType, tile_group_header_.frame_header.refresh_frame_flags); @@ -46,7 +47,7 @@ bool RegularTileGroupOBU::parse_payload(std::ifstream& ifs) { tile_group_header_.parse_deep(br, OBUType::REGULAR_TILE_GROUP, *active_seq_header_); } } else { - spdlog::warn("REGULAR_TILE_GROUP: no active sequence header — frame header not parsed"); + LIB_WARN("REGULAR_TILE_GROUP: no active sequence header — frame header not parsed"); } return true; diff --git a/src/obus/regular_tip_obu.cpp b/src/obus/regular_tip_obu.cpp index e0fb092..6570425 100644 --- a/src/obus/regular_tip_obu.cpp +++ b/src/obus/regular_tip_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -18,25 +19,25 @@ namespace av2_obu { bool RegularTIPOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Parsing REGULAR_TIP payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing REGULAR_TIP payload ({} bytes)", position_.payload_size); raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read REGULAR_TIP payload"); + LIB_ERROR("Failed to read REGULAR_TIP payload"); return false; } if (active_seq_header_) { BitstreamReader br(raw_payload_); if (!frame_header_.parse_lightweight(br, OBUType::REGULAR_TIP, *active_seq_header_)) { - spdlog::error("Failed to parse REGULAR_TIP frame header"); + LIB_ERROR("Failed to parse REGULAR_TIP frame header"); return false; } - spdlog::debug("REGULAR_TIP: order_hint={}, refresh_flags=0x{:02x}, immediate={}", + LIB_DEBUG("REGULAR_TIP: order_hint={}, refresh_flags=0x{:02x}, immediate={}", frame_header_.order_hint, frame_header_.refresh_frame_flags, frame_header_.immediate_output_frame); } else { - spdlog::warn("REGULAR_TIP: no active sequence header — frame header not parsed"); + LIB_WARN("REGULAR_TIP: no active sequence header — frame header not parsed"); } return true; diff --git a/src/obus/sequence_header_obu.cpp b/src/obus/sequence_header_obu.cpp index 850e27e..e6d570a 100644 --- a/src/obus/sequence_header_obu.cpp +++ b/src/obus/sequence_header_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -18,17 +19,17 @@ namespace av2_obu { bool SequenceHeaderOBU::parse_payload(std::ifstream& ifs) { if (position_.payload_size == 0) { - spdlog::warn("Sequence header has no payload"); + LIB_WARN("Sequence header has no payload"); return false; } - spdlog::debug("Parsing AV2 sequence header payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing AV2 sequence header payload ({} bytes)", position_.payload_size); // Snapshot the raw payload bytes so callers (e.g. OBUParser::get_statistics) can // detect SH-byte changes across the stream by direct comparison. raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read sequence header payload"); + LIB_ERROR("Failed to read sequence header payload"); return false; } @@ -37,7 +38,7 @@ bool SequenceHeaderOBU::parse_payload(std::ifstream& ifs) { // Parse using the full AV2 sequence header parser if (!seq_header_.parse(br)) { - spdlog::error("Failed to parse AV2 sequence header"); + LIB_ERROR("Failed to parse AV2 sequence header"); return false; } @@ -45,11 +46,11 @@ bool SequenceHeaderOBU::parse_payload(std::ifstream& ifs) { if (!parse_obu_trailing_bits(br)) return false; - spdlog::debug("Successfully parsed AV2 sequence header"); + LIB_DEBUG("Successfully parsed AV2 sequence header"); return true; } catch (const std::exception& e) { - spdlog::error("Exception while parsing sequence header: {}", e.what()); + LIB_ERROR("Exception while parsing sequence header: {}", e.what()); return false; } } diff --git a/src/obus/switch_obu.cpp b/src/obus/switch_obu.cpp index 1bfaaf3..ca91761 100644 --- a/src/obus/switch_obu.cpp +++ b/src/obus/switch_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -18,22 +19,22 @@ namespace av2_obu { bool SwitchOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Parsing SWITCH payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Parsing SWITCH payload ({} bytes)", position_.payload_size); raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read SWITCH payload"); + LIB_ERROR("Failed to read SWITCH payload"); return false; } if (active_seq_header_) { BitstreamReader br(raw_payload_); if (!tile_group_header_.parse_lightweight(br, OBUType::SWITCH, *active_seq_header_)) { - spdlog::error("Failed to parse SWITCH tile group header"); + LIB_ERROR("Failed to parse SWITCH tile group header"); return false; } if (tile_group_header_.frame_header.parsed) { - spdlog::debug("SWITCH: order_hint={}, restricted_prediction={}", + LIB_DEBUG("SWITCH: order_hint={}, restricted_prediction={}", tile_group_header_.frame_header.order_hint, tile_group_header_.frame_header.restricted_prediction_switch); } @@ -43,7 +44,7 @@ bool SwitchOBU::parse_payload(std::ifstream& ifs) { tile_group_header_.parse_deep(br, OBUType::SWITCH, *active_seq_header_); } } else { - spdlog::warn("SWITCH: no active sequence header — frame header not parsed"); + LIB_WARN("SWITCH: no active sequence header — frame header not parsed"); } return true; diff --git a/src/obus/temporal_delimiter_obu.cpp b/src/obus/temporal_delimiter_obu.cpp index a7701a1..95d8ddb 100644 --- a/src/obus/temporal_delimiter_obu.cpp +++ b/src/obus/temporal_delimiter_obu.cpp @@ -10,6 +10,7 @@ */ #include +#include #include @@ -17,7 +18,7 @@ namespace av2_obu { bool TemporalDelimiterOBU::parse_payload(std::ifstream& ifs) { // Temporal delimiter has no payload - spdlog::debug("Temporal delimiter (no payload)"); + LIB_DEBUG("Temporal delimiter (no payload)"); return skip_payload(ifs); } diff --git a/src/obus/tile_group_obu.cpp b/src/obus/tile_group_obu.cpp index 9156106..8139158 100644 --- a/src/obus/tile_group_obu.cpp +++ b/src/obus/tile_group_obu.cpp @@ -10,13 +10,14 @@ */ #include +#include #include namespace av2_obu { bool TileGroupOBU::parse_payload(std::ifstream& ifs) { - spdlog::debug("Skipping tile group payload ({} bytes)", position_.payload_size); + LIB_DEBUG("Skipping tile group payload ({} bytes)", position_.payload_size); // For now, skip tile data (too large and complex) return skip_payload(ifs); } diff --git a/src/obus/unknown_obu.cpp b/src/obus/unknown_obu.cpp index ffa7536..1f58fdd 100644 --- a/src/obus/unknown_obu.cpp +++ b/src/obus/unknown_obu.cpp @@ -10,19 +10,20 @@ */ #include +#include #include namespace av2_obu { bool UnknownOBU::parse_payload(std::ifstream& ifs) { - spdlog::warn("Unknown OBU type {}, storing raw bytes ({} bytes)", header_.get_obu_type_raw(), + LIB_WARN("Unknown OBU type {}, storing raw bytes ({} bytes)", header_.get_obu_type_raw(), position_.payload_size); if (position_.payload_size > 0) { raw_payload_.resize(position_.payload_size); if (!ifs.read(reinterpret_cast(raw_payload_.data()), position_.payload_size)) { - spdlog::error("Failed to read unknown OBU payload"); + LIB_ERROR("Failed to read unknown OBU payload"); return false; } } From b3eb844c43361d88759646e7c0e362cd7250a6a2 Mon Sep 17 00:00:00 2001 From: Dimitri Podborski Date: Mon, 22 Jun 2026 20:27:44 -0700 Subject: [PATCH 10/10] av2_mux: replace heuristic DOH lift with spec-faithful version --- apps/av2_mux/CMakeLists.txt | 1 + apps/av2_mux/av2_muxer.cpp | 34 +++++--- apps/av2_mux/av2_muxer.h | 5 +- apps/av2_mux/display_order_lifter.cpp | 112 +++++++++++++++++++++----- apps/av2_mux/display_order_lifter.h | 49 ++++------- apps/av2_mux/ref_frame_buffer.cpp | 20 +++++ apps/av2_mux/ref_frame_buffer.h | 47 +++++++++++ src/core/frame_header_info.cpp | 36 ++++----- 8 files changed, 219 insertions(+), 85 deletions(-) create mode 100644 apps/av2_mux/ref_frame_buffer.cpp create mode 100644 apps/av2_mux/ref_frame_buffer.h diff --git a/apps/av2_mux/CMakeLists.txt b/apps/av2_mux/CMakeLists.txt index bb0555a..a047714 100644 --- a/apps/av2_mux/CMakeLists.txt +++ b/apps/av2_mux/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable(av2_mux frame_header_helpers.cpp colr_info.cpp mux_log.cpp + ref_frame_buffer.cpp ) target_link_libraries(av2_mux diff --git a/apps/av2_mux/av2_muxer.cpp b/apps/av2_mux/av2_muxer.cpp index 1044599..b29ac47 100644 --- a/apps/av2_mux/av2_muxer.cpp +++ b/apps/av2_mux/av2_muxer.cpp @@ -58,7 +58,8 @@ bool Av2Muxer::package(const OBUParser& parser, const std::string& output_path) // ISOBMFF §8.6.1.3 forbids ctts when CTS == DTS for every sample, so emit it // only after computing offsets and observing at least one non-zero. - std::vector ctts_offsets = compute_composition_offsets(tus, strategy_.start_tu, end_tu); + std::vector ctts_offsets = compute_composition_offsets(parser, tus, + strategy_.start_tu, end_tu); bool need_ctts = std::any_of(ctts_offsets.begin(), ctts_offsets.end(), [](int32_t v) { return v != 0; }); if (need_ctts) { @@ -215,22 +216,35 @@ bool Av2Muxer::write_tu(const TemporalUnit& tu, int32_t composition_offset) { } std::vector Av2Muxer::compute_composition_offsets( - const std::vector& tus, uint32_t start, uint32_t end) { + const OBUParser& parser, const std::vector& tus, uint32_t start, uint32_t end) { std::vector offsets; if (!doh_lifter_) return offsets; offsets.reserve(end - start); const int64_t dur = static_cast(strategy_.default_sample_duration); + const SequenceHeaderOBU* first_sh = find_first_sequence_header(parser); + const AV2SequenceHeader& sh = first_sh->sequence_header(); + for (uint32_t i = start; i < end; ++i) { int32_t off = 0; - if (const BaseOBU* out = find_output_frame(tus[i])) { - if (const FrameHeaderInfo* fh = frame_header_of(out)) { - int64_t lifted_doh = doh_lifter_->lift(fh->order_hint); - int64_t cts = lifted_doh * dur; - int64_t dts = static_cast(i - start) * dur; - off = static_cast(cts - dts); - } + int64_t output_doh = -1; + + // Process every coded frame OBU in this TU through the lifter so the ref + // buffer reflects the bitstream's decode-order state. The output frame's + // lifted DOH (the last is_output_frame=true frame in decode order, if any) + // determines this sample's ctts. + for (const auto* obu : tus[i].obus()) { + const FrameHeaderInfo* fh = frame_header_of(obu); + if (!fh) continue; + const int64_t doh = doh_lifter_->process(*obu, *fh, sh, ref_buffer_); + if (fh->is_output_frame) output_doh = doh; + } + + if (output_doh >= 0) { + const int64_t cts = output_doh * dur; + const int64_t dts = static_cast(i - start) * dur; + off = static_cast(cts - dts); } - // Hidden-only TUs: no output picture, leave CTS == DTS. + // Hidden-only TUs: leave CTS == DTS (off stays 0). offsets.push_back(off); } return offsets; diff --git a/apps/av2_mux/av2_muxer.h b/apps/av2_mux/av2_muxer.h index 64ab3a1..9d982e2 100644 --- a/apps/av2_mux/av2_muxer.h +++ b/apps/av2_mux/av2_muxer.h @@ -21,6 +21,7 @@ #include "display_order_lifter.h" #include "mp4_writer.h" #include "mux_strategy.h" +#include "ref_frame_buffer.h" namespace av2_obu { @@ -41,7 +42,8 @@ class Av2Muxer { bool check_doh_lifter_supported(const OBUParser& parser); bool write_tu(const TemporalUnit& tu, int32_t composition_offset); bool assemble_sample_bytes(const TemporalUnit& tu, std::vector& out); - std::vector compute_composition_offsets(const std::vector& tus, + std::vector compute_composition_offsets(const OBUParser& parser, + const std::vector& tus, uint32_t start, uint32_t end); static const SequenceHeaderOBU* find_first_sequence_header(const OBUParser& parser); @@ -51,6 +53,7 @@ class Av2Muxer { std::ifstream input_ifs_; Mp4Writer writer_; std::unique_ptr doh_lifter_; + RefFrameBuffer ref_buffer_; std::optional last_colr_; }; diff --git a/apps/av2_mux/display_order_lifter.cpp b/apps/av2_mux/display_order_lifter.cpp index ea94c22..5f08ea9 100644 --- a/apps/av2_mux/display_order_lifter.cpp +++ b/apps/av2_mux/display_order_lifter.cpp @@ -11,37 +11,107 @@ #include "display_order_lifter.h" +#include +#include +#include +#include + namespace av2_obu { -DisplayOrderLifter::DisplayOrderLifter(uint32_t order_hint_bits) { - mod_ = (order_hint_bits == 0) ? 1 : (int64_t{1} << order_hint_bits); - half_ = mod_ >> 1; -} +namespace { -void DisplayOrderLifter::reset_cvs() { - // Wrap state is implicit in max_seen_; nothing to reset across CVS joins. +// Returns max RefOrderHint across slots that pass dep-map and "showable" filters. +// We don't track Ref{Implicit,Immediate}OutputFrame per slot today; the practical +// effect is that hidden refs may briefly contribute to maxDisp until a later +// output frame supersedes them. Doesn't matter for our current fixtures. +uint64_t get_max_disp_order_hint(const RefFrameBuffer& refs, + const AV2SequenceHeader& sh, + uint8_t obu_mlayer_id, uint8_t obu_tlayer_id) { + uint64_t max_disp = 0; + for (size_t i = 0; i < refs.size(); ++i) { + const auto& s = refs.slot(i); + if (!s.valid) continue; + if (s.order_hint == kRestrictedOrderHint) continue; + if (obu_mlayer_id < MAX_NUM_MLAYERS && s.mlayer_id < MAX_NUM_MLAYERS && + !sh.MLayerDependencyMap[obu_mlayer_id][s.mlayer_id]) { + continue; + } + if (obu_mlayer_id < MAX_NUM_MLAYERS && obu_tlayer_id < MAX_NUM_TLAYERS && + s.tlayer_id < MAX_NUM_TLAYERS && + !sh.TLayerDependencyMap[obu_mlayer_id][obu_tlayer_id][s.tlayer_id]) { + continue; + } + if (s.order_hint > max_disp) max_disp = s.order_hint; + } + return max_disp; } -int64_t DisplayOrderLifter::lift(uint32_t order_hint_lsbs) { - int64_t oh = static_cast(order_hint_lsbs); - int64_t candidate = oh + wrap_; - if (any_) { - while (candidate < max_seen_ - half_) { - candidate += mod_; - wrap_ += mod_; - } - while (candidate > max_seen_ + half_) { - candidate -= mod_; - wrap_ -= mod_; +} // namespace + +DisplayOrderLifter::DisplayOrderLifter(uint32_t order_hint_bits) + : order_hint_bits_(order_hint_bits), + mod_(order_hint_bits == 0 ? 1ull : (1ull << order_hint_bits)) {} + +int64_t DisplayOrderLifter::process(const BaseOBU& obu, const FrameHeaderInfo& fh, + const AV2SequenceHeader& sh, RefFrameBuffer& refs) { + const OBUType type = obu.type(); + const uint8_t mlayer_id = obu.header().get_mlayer_id(); + const uint8_t tlayer_id = obu.header().get_tlayer_id(); + + // get_disp_order_hint: CLK and restricted SWITCH return Lsbs directly; otherwise lift. + const uint64_t lsbs = fh.order_hint; + uint64_t lifted; + const bool is_clk = (type == OBUType::CLK); + const bool is_restricted_switch = !is_sef(type) && fh.FrameType == SWITCH_FRAME && + fh.restricted_prediction_switch != 0; + if (is_clk || is_restricted_switch) { + lifted = lsbs; + } else { + const uint64_t max_disp = get_max_disp_order_hint(refs, sh, mlayer_id, tlayer_id); + const int64_t offset = + static_cast(max_disp) - static_cast(mod_ >> 1) - + static_cast(lsbs); + lifted = lsbs; + if (offset >= 0) { + const uint64_t shift = + ((static_cast(offset) >> order_hint_bits_) + 1) << order_hint_bits_; + lifted += shift; } } + if (!anchored_) { - anchor_ = candidate; + anchor_ = lifted; anchored_ = true; } - if (!any_ || candidate > max_seen_) max_seen_ = candidate; - any_ = true; - return candidate - anchor_; + + // CLK at start of CVS clears the ref buffer. + if (type == OBUType::CLK) { + refs.invalidate_all(); + } + + // Restricted SWITCH poisons all valid refs with RESTRICTED_OH so they no longer feed maxDisp. + if (type == OBUType::SWITCH && fh.restricted_prediction_switch != 0) { + for (size_t i = 0; i < refs.size(); ++i) { + auto& s = refs.slot(i); + if (s.valid) s.order_hint = kRestrictedOrderHint; + } + } + + // Apply refresh_frame_flags: each set bit refreshes the corresponding slot + // with this frame's metadata. + const uint32_t flags = fh.refresh_frame_flags; + for (uint32_t i = 0; i < refs.size() && i < 32; ++i) { + if (flags & (1u << i)) { + auto& s = refs.slot(i); + s.valid = true; + s.order_hint = lifted; + s.mlayer_id = mlayer_id; + s.tlayer_id = tlayer_id; + s.counter = refs.allocate_counter(); + } + } + + return static_cast(lifted) - static_cast(anchor_); } } // namespace av2_obu diff --git a/apps/av2_mux/display_order_lifter.h b/apps/av2_mux/display_order_lifter.h index 2d2f246..f4f44bc 100644 --- a/apps/av2_mux/display_order_lifter.h +++ b/apps/av2_mux/display_order_lifter.h @@ -13,48 +13,31 @@ #include +#include "ref_frame_buffer.h" + namespace av2_obu { -// Modular-unwrap lifter for OrderHintLsbs → relative DispOrderHint, used to -// compute per-sample ctts offsets. -// -// This is NOT a full implementation of get_disp_order_hint() -// (05_Syntax_structures.bs:2649). It tracks no reference-frame state, ignores -// dependency maps, does not reset at CLK boundaries, and reads only the -// per-frame OrderHintLsbs. It produces correct output exactly when the input -// stream meets all of the following: -// * Single-layer (one xlayer, one mlayer). -// * Single CVS (one CLK at the start, no further CLKs that reset DOH). -// * No derived-DOH SEFs (derive_sef_order_hint must be 0 if any SEFs exist). -// * No Bridge frames (their DOH is RefOrderHint[bridge_frame_ref_idx], -// which we cannot compute without ref-state tracking). -// * Reorder distance between consecutive output frames < (1 << OrderHintBits) / 2. -// -// Av2Muxer enforces the first four with hard errors. The reorder-distance -// bound is the only soft assumption; it holds for hierarchical-B and -// low-delay GoPs at typical OrderHintBits ≥ 4. -// -// A spec-faithful replacement (with reference state and dep maps) is planned -// once Phase 2 (parser cross-check vs avmdec) validates the per-frame fields -// the full algorithm depends on. +class BaseOBU; +struct AV2SequenceHeader; +struct FrameHeaderInfo; + +// Lifts each frame's OrderHintLsbs into a relative DispOrderHint, tracking the +// reference-frame buffer (driven by refresh_frame_flags) so maxDisp stays +// correct across CVS boundaries. process() returns the lifted DOH relative to +// the first lifted output (anchor = 0) and also updates the ref buffer. class DisplayOrderLifter { public: explicit DisplayOrderLifter(uint32_t order_hint_bits); - // Clears wrap state at a CVS boundary; does not re-anchor. - void reset_cvs(); - - // Returns the lifted DOH relative to the first lifted value of the stream. - int64_t lift(uint32_t order_hint_lsbs); + // Lift this frame's OrderHintLsbs and apply its refresh_frame_flags to refs. + int64_t process(const BaseOBU& obu, const FrameHeaderInfo& fh, + const AV2SequenceHeader& sh, RefFrameBuffer& refs); private: - int64_t mod_; - int64_t half_; - int64_t wrap_ = 0; - int64_t max_seen_ = 0; - bool any_ = false; - int64_t anchor_ = 0; + uint32_t order_hint_bits_; + uint64_t mod_; bool anchored_ = false; + uint64_t anchor_ = 0; }; } // namespace av2_obu diff --git a/apps/av2_mux/ref_frame_buffer.cpp b/apps/av2_mux/ref_frame_buffer.cpp new file mode 100644 index 0000000..86ef344 --- /dev/null +++ b/apps/av2_mux/ref_frame_buffer.cpp @@ -0,0 +1,20 @@ +/* + * 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 "ref_frame_buffer.h" + +namespace av2_obu { + +void RefFrameBuffer::invalidate_all() { + for (auto& s : slots_) s.valid = false; +} + +} // namespace av2_obu diff --git a/apps/av2_mux/ref_frame_buffer.h b/apps/av2_mux/ref_frame_buffer.h new file mode 100644 index 0000000..a78275b --- /dev/null +++ b/apps/av2_mux/ref_frame_buffer.h @@ -0,0 +1,47 @@ +/* + * 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 +#include + +namespace av2_obu { + +constexpr uint32_t kNumRefSlots = 16; +constexpr uint64_t kRestrictedOrderHint = 0xFFFF'FFFF'FFFF'FFFFull; + +// Per-slot reference state mirroring the spec arrays RefValid, RefOrderHint, +// RefMLayerId, RefTLayerId, RefCounter. Only what the DOH lifter needs. +struct RefSlot { + bool valid = false; + uint64_t order_hint = 0; // lifted DOH (kRestrictedOrderHint = invalid for ranking) + uint8_t mlayer_id = 0; + uint8_t tlayer_id = 0; + uint32_t counter = 0; // increments per refresh; powers first_slot_with_ref +}; + +// Stream-level reference state. Tracks slot updates driven by refresh_frame_flags. +class RefFrameBuffer { + public: + RefSlot& slot(size_t i) { return slots_[i]; } + const RefSlot& slot(size_t i) const { return slots_[i]; } + size_t size() const { return slots_.size(); } + + uint32_t allocate_counter() { return ++next_counter_; } + void invalidate_all(); + + private: + std::array slots_{}; + uint32_t next_counter_ = 0; +}; + +} // namespace av2_obu diff --git a/src/core/frame_header_info.cpp b/src/core/frame_header_info.cpp index 47b9380..099dd3d 100644 --- a/src/core/frame_header_info.cpp +++ b/src/core/frame_header_info.cpp @@ -18,8 +18,7 @@ namespace av2_obu { -// film_grain_config() per AV2 spec §film_grain_config_syntax (05_Syntax_structures.bs:4159). -// Reads at most 1 + 19 bits depending on conditions; updates fhi.{apply_grain, fgm_id, grain_seed}. +// film_grain_config(): up to 1 + 19 bits depending on conditions; updates fhi.{apply_grain, fgm_id, grain_seed}. static void parse_film_grain_config(BitstreamReader& br, const AV2SequenceHeader& sh, FrameHeaderInfo& fhi) { if (!sh.film_grain_params_present || @@ -39,9 +38,9 @@ static void parse_film_grain_config(BitstreamReader& br, const AV2SequenceHeader bool FrameHeaderInfo::parse_lightweight(BitstreamReader& br, OBUType obu_type, const AV2SequenceHeader& sh) { - // Frame classification flags. The spec helper `keyFrame` (CLK || OLK) is only used - // for state we don't track in lightweight (LCR activation, RefValid reset, OlkEncountered), - // so we don't bind it. IsBridge is needed locally and exposed in the struct. + // The keyFrame (CLK || OLK) helper from the spec is only needed for state we don't track + // in lightweight (LCR activation, RefValid reset, OlkEncountered), so we don't bind it. + // IsBridge is needed locally and exposed in the struct. IsBridge = (obu_type == OBUType::BRIDGE_FRAME); // --- cur_mfh_id --- @@ -56,7 +55,7 @@ bool FrameHeaderInfo::parse_lightweight(BitstreamReader& br, OBUType obu_type, seq_header_id_in_frame_header = br.read_uvlc(); } - // --- Bridge frame ref idx (spec line 60-63) --- + // --- Bridge frame ref idx --- if (IsBridge) { uint32_t n = CeilLog2(sh.inter_config.NumRefFrames); bridge_frame_ref_idx = (n > 0) ? br.read_bits(n) : 0; @@ -190,7 +189,7 @@ bool FrameHeaderInfo::parse_lightweight(BitstreamReader& br, OBUType obu_type, } } - // --- bridge_frame_overwrite_flag (spec line 207, BEFORE refresh_frame_flags) --- + // --- bridge_frame_overwrite_flag (read BEFORE refresh_frame_flags) --- if (IsBridge) { bridge_frame_overwrite_flag = br.read_bit(); } @@ -207,7 +206,7 @@ bool FrameHeaderInfo::parse_lightweight(BitstreamReader& br, OBUType obu_type, refresh_frame_flags = br.read_bits(sh.inter_config.NumRefFrames); } } else if (IsBridge && !bridge_frame_overwrite_flag) { - // Spec line 237-238: bridge with !overwrite refreshes only the slot it replaces. + // Bridge with !overwrite refreshes only the slot it replaces. refresh_frame_flags = 1u << bridge_frame_ref_idx; } else if (obu_type == OBUType::RAS_FRAME && sh.max_mlayer_id == 0) { // RAS with single layer: refresh non-long-term refs @@ -273,7 +272,7 @@ bool FrameHeaderInfo::parse_lightweight(BitstreamReader& br, OBUType obu_type, ref_frame_idx.resize(NumTotalRefs); for (uint32_t i = 0; i < NumTotalRefs; i++) { if (IsBridge) { - // Spec line 297-298: ref_frame_idx[i] = bridge_frame_ref_idx (already parsed above). + // Bridge: ref_frame_idx[i] = bridge_frame_ref_idx (already parsed above). ref_frame_idx[i] = bridge_frame_ref_idx; } else if (explicitRefFrameMap) { uint32_t n = CeilLog2(sh.inter_config.NumRefFrames); @@ -339,7 +338,7 @@ bool FrameHeaderInfo::parse_lightweight(BitstreamReader& br, OBUType obu_type, } } - // --- use_ref_frame_mvs (spec line 331-336) --- + // --- use_ref_frame_mvs --- if (FrameType == SWITCH_FRAME || !sh.inter_config.enable_ref_frame_mvs || IsBridge || bru_inactive) { use_ref_frame_mvs = 0; @@ -347,7 +346,7 @@ bool FrameHeaderInfo::parse_lightweight(BitstreamReader& br, OBUType obu_type, use_ref_frame_mvs = br.read_bit(); } - // --- tmvp_sample_step_minus_1 (spec line 337-342) --- + // --- tmvp_sample_step_minus_1 --- // Per-frame SbSize: 256x256 promotes to BLOCK_256X256 only for non-intra frames. BlockSize SbSize; if (sh.partition_config.use_256x256_superblock) { @@ -361,12 +360,10 @@ bool FrameHeaderInfo::parse_lightweight(BitstreamReader& br, OBUType obu_type, tmvp_sample_step_minus_1 = br.read_bit(); } - // LIGHTWEIGHT STOP for inter frames - // (skip: enable_tip block, screen_content_params, intrabc_params, MV precision, - // interpolation filter, motion modes, etc.) - // TipFrameMode here is left at the default (TIP_FRAME_DISABLED). The spec assigns - // TipFrameMode either to TIP_FRAME_AS_OUTPUT (when EnableTipOutput && is_tip_frame()) - // or from `f(1) tip_frame_mode` — both are inside the enable_tip block we skip. + // LIGHTWEIGHT STOP for inter frames: skip enable_tip block, screen_content_params, + // intrabc_params, MV precision, interpolation filter, motion modes, etc. + // TipFrameMode left at default (TIP_FRAME_DISABLED); the real value comes from the + // enable_tip block we don't read here. TipFrameMode = TIP_FRAME_DISABLED; } @@ -402,9 +399,8 @@ json FrameHeaderInfo::to_json() const { j["seq_header_id_in_frame_header"] = seq_header_id_in_frame_header; } - // Frame type: emit only when reliably known. For SEF the spec sets FrameType from - // RefFrameType[frame_to_show_map_idx] (decoder reference state we don't track), so - // skip emission to avoid reporting the constructor default. + // Emit FrameType only when reliably known. SEF's FrameType comes from RefFrameType[frame_to_show_map_idx] + // (decoder ref state we don't track), so skip it to avoid reporting the constructor default. const char* frame_type_names[] = {"KEY_FRAME", "INTER_FRAME", "INTRA_ONLY_FRAME", "SWITCH_FRAME"}; if (!ShowExistingFrame) { j["FrameType"] = (FrameType <= 3) ? frame_type_names[FrameType] : "UNKNOWN";