diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..31a687b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + pull_request: + branches: [master] + +jobs: + build: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update -q + sudo apt-get install -yq ninja-build + + - name: Configure + run: cmake -B build -G Ninja + + - name: Build (includes constexpr tests) + run: cmake --build build + + - name: Run tests on AVIF samples + run: | + for f in test/assets/*.avif; do + echo "=== $f ===" + ./build/test/mbmff-test "$f" + done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..518bb56 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,42 @@ +name: Release + +on: + workflow_dispatch: + +jobs: + release: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update -q + sudo apt-get install -yq ninja-build python3 + + - name: Configure + run: cmake -B build -G Ninja + + - name: Read version + id: version + run: echo "version=$(cat VERSION)" >> $GITHUB_OUTPUT + + - name: Build amalgamated header + run: cmake --build build --target amalgamate + + - name: Upload single header + uses: actions/upload-artifact@v4 + with: + name: mbmff-single-header + path: build/mbmff.hpp + + - name: Create Release + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + VERSION="${{ steps.version.outputs.version }}" + gh release create "v${VERSION}" \ + --title "Release v${VERSION}" \ + --notes "mBMFF single-header release." \ + --draft \ + build/mbmff.hpp diff --git a/.gitignore b/.gitignore index 9491a2f..9dd0f75 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,8 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + +ISOBMFF-master/ +include/mbmff/ignored/ +build/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 9300a17..93a5303 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,35 @@ cmake_minimum_required(VERSION 3.22) -project(mBMFF VERSION 0.1.0 LANGUAGES CXX) +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/VERSION" MBMFF_VERSION_STRING) +string(STRIP "${MBMFF_VERSION_STRING}" MBMFF_VERSION_STRING) +project(mBMFF VERSION ${MBMFF_VERSION_STRING} LANGUAGES CXX) include(GNUInstallDirs) +# --- Single-header amalgamation --- +find_package(Python3 REQUIRED COMPONENTS Interpreter) + +file(GLOB MBMFF_SOURCE_HEADERS + ${CMAKE_CURRENT_SOURCE_DIR}/include/mbmff/*.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/mbmff/boxes/*.hpp +) + +set(MBMFF_SINGLE_HEADER "${CMAKE_BINARY_DIR}/mbmff.hpp") + +add_custom_command( + OUTPUT ${MBMFF_SINGLE_HEADER} + COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_CURRENT_SOURCE_DIR}/tools/amalgamate.py" + --input "${CMAKE_CURRENT_SOURCE_DIR}/include/mbmff" + --output "${MBMFF_SINGLE_HEADER}" + DEPENDS + ${MBMFF_SOURCE_HEADERS} + "${CMAKE_CURRENT_SOURCE_DIR}/tools/amalgamate.py" + COMMENT "Generating single-header mbmff.hpp" +) + +add_custom_target(amalgamate DEPENDS ${MBMFF_SINGLE_HEADER}) + +# --- Library --- add_library(mbmff INTERFACE) add_library(mbmff::mbmff ALIAS mbmff) @@ -15,7 +42,12 @@ install(TARGETS mbmff EXPORT mbmffTargets INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} ) -install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + +# Install only the amalgamated single header for distribution +install( + FILES ${MBMFF_SINGLE_HEADER} + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/mbmff +) install(EXPORT mbmffTargets FILE mbmffTargets.cmake @@ -23,14 +55,15 @@ install(EXPORT mbmffTargets DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/mbmff ) -# Determine if this is the top-level project -if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) - set(MBMFF_TOP_LEVEL ON) -else() - set(MBMFF_TOP_LEVEL OFF) -endif() - -option(MBMFF_BUILD_TEST "Build tests" ${MBMFF_TOP_LEVEL}) +option(MBMFF_BUILD_TEST "Build tests" ${PROJECT_IS_TOP_LEVEL}) if (MBMFF_BUILD_TEST) add_subdirectory(test) +endif() + +option(MBMFF_ENABLE_CONSTEXPR_TEST + "Enable compile-time constexpr tests in headers" + ${PROJECT_IS_TOP_LEVEL} +) +if (MBMFF_ENABLE_CONSTEXPR_TEST) + target_compile_definitions(mbmff INTERFACE MBMFF_ENABLE_CONSTEXPR_TEST) endif() \ No newline at end of file diff --git a/README.md b/README.md index 2762922..1212b59 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,77 @@ # mBMFF -mBMFF is a library that primarily serves as an ISO-BMFF parser. -For now it only supportss AVIF. -It is designed with extensibility in mind and can be extended to support additional media formats and features in future versions. +[![CI](https://img.shields.io/github/actions/workflow/status/Agrael1/mBMFF/ci.yml?branch=master&label=CI&logo=github)](https://github.com/Agrael1/mBMFF/actions/workflows/ci.yml) +[![License](https://img.shields.io/github/license/Agrael1/mBMFF)](LICENSE.txt) +[![Version](https://img.shields.io/badge/version-0.1.0-blue)](VERSION) +[![C++](https://img.shields.io/badge/C%2B%2B-20-00599C?logo=cplusplus)](CMakeLists.txt) +[![Header-only](https://img.shields.io/badge/header--only-yellowgreen)](include/mbmff) -## How it works +A lightweight, dependency-free ISOBMFF (ISO Base Media File Format) parser written in modern C++20. Parses AVIF, HEIF, MP4, and any other ISO-BMFF container. -The library operates without any dependencies, making it lightweight and easy to integrate into various projects. -It never allocates memory on its own, instead it relies on the caller to provide memory to parse. +## Features -The API is built around iterators. Boxes are parsed lazily, meaning that the library only parses the boxes that are requested by the caller. -Parsing is done in a resursive manner: +- **Zero dependencies** — no external libraries required +- **Zero allocations** — operates directly on caller-provided memory +- **Lazy parsing** — boxes are parsed on demand, skip what you don't need +- **Single-header amalgamation** — generate a single `mbmff.hpp` via the `amalgamate` CMake target for easy drop-in +- **Constexpr validation** — box structures are validated at compile time where possible (behind `MBMFF_ENABLE_CONSTEXPR_TEST`) +- **Iterators** — traverse boxes with `box_iterator` (flat or recursive) -- The caller uses box_iterator to iterate over box headers. This allows the caller to quickly skip over boxes that are not of interest. -- Then the caller can use box_cast to cast the box header to a specific box type. -- After the box is cast to the type of interest, the caller can use the box's API to access its contents, which are also parsed lazily. - -## Requirements - -- C\+\+23 or later (use of std::span, std::expected and other C++23 features) -- yes, all you need is a C++23 compliant compiler and the standard library. - -## Example - -The example is in the `test` directory, but here is a simple example of how to use the library to parse an AVIF file: +## Quick start ```cpp #include - -int main() { - std::ifstream avif_file("assets/avif_sample_8_420.avif", std::ios::binary); - if (!avif_file) { - std::cerr << "Failed to open the AVIF file.\n"; - return 1; // Failed to open the file - } - - // Read the entire file into a vector of chars - std::vector file_data((std::istreambuf_iterator(avif_file)), std::istreambuf_iterator()); - std::span file_data_span(reinterpret_cast(file_data.data()), file_data.size()); - - for (const auto& box_expected : mbmff::box_iterator(file_data_span, mbmff::iterator_flags::recursive)) { - if (!box_expected) { +#include +#include +#include + +int main(int argc, char* argv[]) +{ + std::ifstream file(argv[1], std::ios::binary); + std::vector data((std::istreambuf_iterator(file)), {}); + + for (const auto& result : mbmff::box_iterator( + std::span(reinterpret_cast(data.data()), data.size()), + mbmff::iterator_flags::recursive)) + { + if (!result) break; + auto& box = *result; + + switch (box.type()) { + case mbmff::box_type::ftyp: + std::cout << "ftyp: " << mbmff::box_cast(box).value().major_brand.view() << '\n'; + break; + case mbmff::box_type::hdlr: + std::cout << "hdlr: " << mbmff::box_cast(box).value().handler_type.view() << '\n'; + break; + default: break; - } - const auto& box = box_expected.value(); - - // Get concrete types - switch (box.box_header.type) { - case mbmff::box_type::ftyp: { - auto ftyp = mbmff::box_cast(box); - // Access the contents of the FTYP box using the ftyp variable - std::cout << "Found FTYP box!" << std::endl; - } break; } } } ``` +## Requirements + +- C++20 compiler (tested with MSVC 2022, GCC 14, Clang 18) +- C++23 for the test runner (`std::format`) + ## Consumption -the library is header only. You can: -- Copy headers directly -- Use FetchContent/CPM and consume via CMake -- Use Conan package manager +The library is header-only. Choose your path: + +| Method | Instructions | +|---|---| +| **Copy headers** | Grab `include/mbmff/` and include `` | +| **Single header** | `cmake --build build --target amalgamate` → `build/mbmff.hpp` | +| **CMake** | `add_subdirectory` or `FetchContent` → `target_link_libraries(foo PRIVATE mbmff::mbmff)` | +| **Conan** | `conan create .` → `requires = "mbmff/1.0.0"` | +| **Release** | Download single-header release | ## Project and contribution -I can only work on this project on my free time, but if there is an actual need for a super lean parsing with blazing speeds, for example for Vulkan Video, feel free to extend the library. +Built in my free time. If you need a lean, fast ISOBMFF parser — for Vulkan Video or anything else — PRs are welcome. + +## License + +MIT — see [LICENSE.txt](LICENSE.txt). diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/conanfile.py b/conanfile.py index 1be128c..e845d39 100644 --- a/conanfile.py +++ b/conanfile.py @@ -11,15 +11,14 @@ class MBMFFConan(ConanFile): topics = ("bmff", "iso-bmff", "media", "header-only") settings = "os", "compiler", "build_type", "arch" - exports_sources = "CMakeLists.txt", "include/*" + exports_sources = "CMakeLists.txt", "include/*", "tools/*" def layout(self): cmake_layout(self) def generate(self): tc = CMakeToolchain(self) - # Disable building tests for conan package - tc.variables["BUILD_TESTING"] = False + tc.variables["MBMFF_BUILD_TEST"] = False tc.generate() deps = CMakeDeps(self) deps.generate() @@ -27,14 +26,13 @@ def generate(self): def build(self): cmake = CMake(self) cmake.configure() - cmake.build() + cmake.build(target="amalgamate") def package(self): cmake = CMake(self) cmake.install() def package_info(self): - # Indicate this is a header-only library self.cpp_info.bindirs = [] self.cpp_info.libdirs = [] diff --git a/include/mbmff/av1.hpp b/include/mbmff/av1.hpp deleted file mode 100644 index e664980..0000000 --- a/include/mbmff/av1.hpp +++ /dev/null @@ -1,786 +0,0 @@ -#pragma once -#include -#include -#include -#include "common.hpp" - -namespace mbmff { -enum class obu_type : uint8_t { - sequence_header = 1, - temporal_delimiter = 2, - frame_header = 3, - tile_group = 4, - metadata = 5, - frame = 6, - redundant_frame_header = 7, - tile_list = 8, - // 9-14 are reserved - padding = 15, // special type used for padding OBUs that may be present in the stream. These should be ignored by - // decoders. -}; -constexpr bool operator==(obu_type type, uint8_t value) noexcept -{ - return static_cast(type) == value; -} -constexpr bool operator==(uint8_t value, obu_type type) noexcept -{ - return type == value; -} - -constexpr static auto to_string(obu_type type) noexcept -> std::string_view -{ - switch (type) { - case obu_type::sequence_header: - return "Sequence Header"; - case obu_type::temporal_delimiter: - return "Temporal Delimiter"; - case obu_type::frame_header: - return "Frame Header"; - case obu_type::tile_group: - return "Tile Group"; - case obu_type::metadata: - return "Metadata"; - case obu_type::frame: - return "Frame"; - case obu_type::redundant_frame_header: - return "Redundant Frame Header"; - case obu_type::tile_list: - return "Tile List"; - case obu_type::padding: - return "Padding"; - default: - return "Unknown OBU Type"; - } -} - -enum class av1_color_primaries : uint8_t { - CP_RESERVED_0 = 0, - CP_BT_709 = 1, - CP_UNSPECIFIED = 2, - CP_RESERVED_3 = 3, - CP_BT_470_M = 4, - CP_BT_470_B_G = 5, - CP_BT_601 = 6, - CP_SMPTE_240 = 7, - CP_GENERIC_FILM = 8, - CP_BT_2020 = 9, - CP_XYZ = 10, - CP_SMPTE_431 = 11, - CP_SMPTE_432 = 12, - CP_EBU_3213 = 22, -}; - -constexpr auto to_string(av1_color_primaries primaries) noexcept -> std::string_view -{ - switch (primaries) { - case mbmff::av1_color_primaries::CP_RESERVED_0: - return "Reserved (0)"; - case mbmff::av1_color_primaries::CP_BT_709: - return "BT.709"; - case mbmff::av1_color_primaries::CP_UNSPECIFIED: - return "Unspecified"; - case mbmff::av1_color_primaries::CP_RESERVED_3: - return "Reserved (3)"; - case mbmff::av1_color_primaries::CP_BT_470_M: - return "BT.470 System M (historical)"; - case mbmff::av1_color_primaries::CP_BT_470_B_G: - return "BT.470 System B/G (historical)"; - case mbmff::av1_color_primaries::CP_BT_601: - return "BT.601"; - case mbmff::av1_color_primaries::CP_SMPTE_240: - return "SMPTE 240M"; - case mbmff::av1_color_primaries::CP_GENERIC_FILM: - return "Generic film (color filters using illuminant C)"; - case mbmff::av1_color_primaries::CP_BT_2020: - return "BT.2020, BT.2100"; - case mbmff::av1_color_primaries::CP_XYZ: - return "SMPTE 428 (CIE 1921 XYZ)"; - case mbmff::av1_color_primaries::CP_SMPTE_431: - return "SMPTE RP 431-2"; - case mbmff::av1_color_primaries::CP_SMPTE_432: - return "SMPTE EG 432-1"; - case mbmff::av1_color_primaries::CP_EBU_3213: - return "EBU Tech 3213-E"; - default: - return "Unknown color primaries"; - } -} - -enum class av1_transfer_characteristics : uint8_t { - TC_RESERVED_0 = 0, - TC_BT_709 = 1, - TC_UNSPECIFIED = 2, - TC_RESERVED_3 = 3, - TC_BT_470_M = 4, - TC_BT_470_B_G = 5, - TC_BT_601 = 6, - TC_SMPTE_240 = 7, - TC_LINEAR = 8, - TC_LOG_100 = 9, - TC_LOG_100_SQRT10 = 10, - TC_IEC_61966 = 11, - TC_BT_1361 = 12, - TC_SRGB = 13, - TC_BT_2020_10_BIT = 14, - TC_BT_2020_12_BIT = 15, - TC_SMPTE_2084 = 16, - TC_SMPTE_428 = 17, - TC_HLG = 18, -}; - -constexpr auto to_string(av1_transfer_characteristics tc) noexcept -> std::string_view -{ - switch (tc) { - case mbmff::av1_transfer_characteristics::TC_RESERVED_0: - return "Reserved (0)"; - case mbmff::av1_transfer_characteristics::TC_BT_709: - return "BT.709"; - case mbmff::av1_transfer_characteristics::TC_UNSPECIFIED: - return "Unspecified"; - case mbmff::av1_transfer_characteristics::TC_RESERVED_3: - return "Reserved (3)"; - case mbmff::av1_transfer_characteristics::TC_BT_470_M: - return "BT.470 System M (historical)"; - case mbmff::av1_transfer_characteristics::TC_BT_470_B_G: - return "BT.470 System B/G (historical)"; - case mbmff::av1_transfer_characteristics::TC_BT_601: - return "BT.601"; - case mbmff::av1_transfer_characteristics::TC_SMPTE_240: - return "SMPTE 240M"; - case mbmff::av1_transfer_characteristics::TC_LINEAR: - return "Linear"; - case mbmff::av1_transfer_characteristics::TC_LOG_100: - return "Logarithmic (100:1 range)"; - case mbmff::av1_transfer_characteristics::TC_LOG_100_SQRT10: - return "Logarithmic (100 * sqrt(10):1 range)"; - case mbmff::av1_transfer_characteristics::TC_IEC_61966: - return "IEC 61966-2-4"; - case mbmff::av1_transfer_characteristics::TC_BT_1361: - return "BT.1361"; - case mbmff::av1_transfer_characteristics::TC_SRGB: - return "sRGB or sYCC"; - case mbmff::av1_transfer_characteristics::TC_BT_2020_10_BIT: - return "BT.2020 10-bit system"; - case mbmff::av1_transfer_characteristics::TC_BT_2020_12_BIT: - return "BT.2020 12-bit system"; - case mbmff::av1_transfer_characteristics::TC_SMPTE_2084: - return "SMPTE ST 2084, ITU BT.2100 PQ"; - case mbmff::av1_transfer_characteristics::TC_SMPTE_428: - return "SMPTE ST 428"; - case mbmff::av1_transfer_characteristics::TC_HLG: - return "BT.2100 HLG, ARIB STD-B67"; - default: - return "Unknown transfer characteristics"; - } -} - -enum class av1_matrix_coefficients : uint8_t { - MC_IDENTITY = 0, - MC_BT_709 = 1, - MC_UNSPECIFIED = 2, - MC_RESERVED_3 = 3, - MC_FCC = 4, - MC_BT_470_B_G = 5, - MC_BT_601 = 6, - MC_SMPTE_240 = 7, - MC_SMPTE_YCGCO = 8, - MC_BT_2020_NCL = 9, - MC_BT_2020_CL = 10, - MC_SMPTE_2085 = 11, - MC_CHROMAT_NCL = 12, - MC_CHROMAT_CL = 13, - MC_ICTCP = 14, -}; - -constexpr auto to_string(av1_matrix_coefficients mc) noexcept -> std::string_view -{ - switch (mc) { - case mbmff::av1_matrix_coefficients::MC_IDENTITY: - return "Identity"; - case mbmff::av1_matrix_coefficients::MC_BT_709: - return "BT.709"; - case mbmff::av1_matrix_coefficients::MC_UNSPECIFIED: - return "Unspecified"; - case mbmff::av1_matrix_coefficients::MC_RESERVED_3: - return "Reserved (3)"; - case mbmff::av1_matrix_coefficients::MC_FCC: - return "US FCC 73.628"; - case mbmff::av1_matrix_coefficients::MC_BT_470_B_G: - return "BT.470 System B/G (historical)"; - case mbmff::av1_matrix_coefficients::MC_BT_601: - return "BT.601"; - case mbmff::av1_matrix_coefficients::MC_SMPTE_240: - return "SMPTE 240M"; - case mbmff::av1_matrix_coefficients::MC_SMPTE_YCGCO: - return "YCgCo"; - case mbmff::av1_matrix_coefficients::MC_BT_2020_NCL: - return "BT.2020 non-constant luminance system"; - case mbmff::av1_matrix_coefficients::MC_BT_2020_CL: - return "BT.2020 constant luminance system"; - case mbmff::av1_matrix_coefficients::MC_SMPTE_2085: - return "SMPTE ST 2085 YDzDx"; - case mbmff::av1_matrix_coefficients::MC_CHROMAT_NCL: - return "Chromaticity-derived non-constant luminance system"; - case mbmff::av1_matrix_coefficients::MC_CHROMAT_CL: - return "Chromaticity-derived constant luminance system"; - case mbmff::av1_matrix_coefficients::MC_ICTCP: - return "ICTCP (BT.2100)"; - default: - return "Unknown matrix coefficients"; - } -} - -enum class av1_chroma_sample_position : uint8_t { - CSP_UNKNOWN = 0, - CSP_VERTICAL = 1, - CSP_COLOCATED = 2, - CSP_RESERVED = 3, -}; - -constexpr auto to_string(av1_chroma_sample_position csp) noexcept -> std::string_view -{ - switch (csp) { - case mbmff::av1_chroma_sample_position::CSP_UNKNOWN: - return "Unknown"; - case mbmff::av1_chroma_sample_position::CSP_VERTICAL: - return "Vertical"; - case mbmff::av1_chroma_sample_position::CSP_COLOCATED: - return "Colocated"; - case mbmff::av1_chroma_sample_position::CSP_RESERVED: - return "Reserved"; - default: - return "Invalid chroma sample position"; - } -} - -// Value that indicates the allow_screen_content_tools syntax element is coded -static constexpr std::uint8_t SELECT_SCREEN_CONTENT_TOOLS = 2; - -// Value that indicates the force_integer_mv syntax element is coded -static constexpr std::uint8_t SELECT_INTEGER_MV = 2; - -//------------------------------------------------------------------------------------------------------------ -struct timing_info { - std::uint32_t num_units_in_display_tick = 0; - std::uint32_t time_scale = 0; - std::uint32_t equal_picture_interval = 0; - std::uint32_t num_ticks_per_picture_minus_1 = 0; -}; - -struct decoder_model_info { - std::uint8_t buffer_delay_length_minus_1 = 0; - std::uint32_t num_units_in_decoding_tick = 0; - std::uint8_t buffer_removal_time_length_minus_1 = 0; - std::uint8_t frame_presentation_time_length_minus_1 = 0; -}; - -struct operating_parameters_info { - std::uint32_t decoder_buffer_delay = 0; - std::uint32_t encoder_buffer_delay = 0; - std::uint32_t low_delay_mode_flag = 0; -}; - -struct operating_point { - std::uint16_t operating_point_idc = 0; - std::uint8_t seq_level_idx : 5 = 0; - std::uint8_t seq_tier : 1 = 0; - std::uint8_t decoder_model_present_for_this_op : 1 = 0; - std::uint8_t initial_display_delay_present_for_this_op : 1 = 0; - std::uint8_t initial_display_delay_minus_1 : 4 = 0; - - operating_parameters_info operating_parameters{}; -}; - -struct color_description { - mbmff::av1_color_primaries color_primaries = mbmff::av1_color_primaries::CP_UNSPECIFIED; - mbmff::av1_transfer_characteristics transfer_characteristics = mbmff::av1_transfer_characteristics::TC_UNSPECIFIED; - mbmff::av1_matrix_coefficients matrix_coefficients = mbmff::av1_matrix_coefficients::MC_UNSPECIFIED; -}; - -struct color_config { - std::uint8_t high_bitdepth : 1 = 0; - std::uint8_t twelve_bit : 1 = 0; - std::uint8_t monochrome : 1 = 0; - std::uint8_t color_description_present_flag : 1 = 0; - - std::uint8_t color_range : 1 = 0; - std::uint8_t subsampling_x : 1 = 0; - std::uint8_t subsampling_y : 1 = 0; - std::uint8_t separate_uv_delta_q : 1 = 0; - - mbmff::av1_chroma_sample_position chroma_sample_position = mbmff::av1_chroma_sample_position::CSP_UNKNOWN; - mbmff::color_description color_description_data{}; -}; - -struct obu_sequence_header { - std::uint8_t seq_profile : 3 = 0; - std::uint8_t still_picture : 1 = 0; - std::uint8_t reduced_still_picture_header : 1 = 0; - std::uint8_t timing_info_present_flag : 1 = 0; - std::uint8_t decoder_model_info_present_flag : 1 = 0; - std::uint8_t initial_display_delay_present_flag : 1 = 0; - - std::uint8_t frame_id_numbers_present_flag : 1 = 0; - std::uint8_t delta_frame_id_length_minus_2 : 4 = 0; - std::uint8_t additional_frame_id_length_minus_1 : 3 = 0; - - std::uint8_t use_128x128_superblock : 1 = 0; - std::uint8_t enable_filter_intra : 1 = 0; - std::uint8_t enable_intra_edge_filter : 1 = 0; - std::uint8_t enable_interintra_compound : 1 = 0; - std::uint8_t enable_masked_compound : 1 = 0; - std::uint8_t enable_warped_motion : 1 = 0; - std::uint8_t enable_dual_filter : 1 = 0; - std::uint8_t enable_order_hint : 1 = 0; - - std::uint8_t order_hint_bits_minus_1 : 3 = 0; - std::uint8_t enable_jnt_comp : 1 = 0; - std::uint8_t enable_ref_frame_mvs : 1 = 0; - std::uint8_t seq_choose_screen_content_tools : 1 = 0; - std::uint8_t seq_force_screen_content_tools : 2 = SELECT_SCREEN_CONTENT_TOOLS; - std::uint8_t seq_choose_integer_mv : 1 = 0; - std::uint8_t seq_force_integer_mv : 2 = SELECT_INTEGER_MV; - - std::uint8_t order_hint_bits_minus_1_present : 1 = 0; - std::uint8_t enable_superres : 1 = 0; - std::uint8_t enable_cdef : 1 = 0; - std::uint8_t enable_restoration : 1 = 0; - std::uint8_t film_grain_params_present : 1 = 0; - - std::uint8_t operating_points_cnt_minus_1 : 5 = 0; - std::array operating_points{}; // max 32 operating points (as per spec) - - timing_info timing_info_data{}; - decoder_model_info decoder_model_info_data{}; - color_config color_config_data{}; - - uint8_t frame_width_bits_minus_1 : 4 = 0; - uint8_t frame_height_bits_minus_1 : 4 = 0; - uint16_t max_frame_width_minus_1 = 0; - uint16_t max_frame_height_minus_1 = 0; -}; - -//------------------------------------------------------------------------------------------------------------ -struct obu_view_base { - constexpr static std::uint64_t max_obu_size = (1ULL << 32) - 1; // 4GB - 1 byte -public: - std::uint8_t forbidden_bit : 1 = 0; // should be 0 - std::uint8_t type : 4 = 0; - std::uint8_t extension_flag : 1 = 0; - std::uint8_t has_size_field : 1 = 0; - std::uint8_t reserved : 1 = 0; // should be 0 - - // obu extension header fields (if extension_flag is set) - std::uint8_t temporal_id : 3 = 0; - std::uint8_t spatial_id : 2 = 0; - std::uint8_t extension_reserved_1 : 3 = 0; // should be 0 - - // followed by payload - std::span payload{}; - -public: - constexpr operator bool() const noexcept - { - return payload.data() != nullptr; - } -}; - -using any_obu_view = struct obu_view_base; - -template -struct basic_obu_view : public obu_view_base {}; - -template <> -struct basic_obu_view : public obu_view_base { - constexpr auto header() const noexcept -> obu_sequence_header; -}; -//------------------------------------------------------------------------------------------------------------ -using obu_sequence_header_view = basic_obu_view; - -//------------------------------------------------------------------------------------------------------------ -template -constexpr auto obu_cast(const any_obu_view& obu) noexcept -> basic_obu_view -{ - if (static_cast(obu.type) != Obu) { - return {}; - } - return static_cast>(obu); -} - -struct av1C_header { - // Byte 0 - std::uint8_t marker : 1 = 0; // should be 1 - std::uint8_t version : 7 = 0; // should be 1 - - // Byte 1 - std::uint8_t seq_profile : 3 = 0; - std::uint8_t seq_level_idx_0 : 5 = 0; - - // Byte 2 - std::uint8_t seq_tier_0 : 1 = 0; - std::uint8_t high_bitdepth : 1 = 0; - std::uint8_t twelve_bit : 1 = 0; - std::uint8_t monochrome : 1 = 0; - std::uint8_t chroma_subsampling_x : 1 = 0; - std::uint8_t chroma_subsampling_y : 1 = 0; - std::uint8_t chroma_sample_position : 2 = 0; - - // Byte 3 - std::uint8_t reserved : 3 = 0; // should be 0 - std::uint8_t initial_presentation_delay_present : 1 = 0; - std::uint8_t initial_presentation_delay_minus_one : 4 = 0; - - // followed by config OBUs - std::span config_obus{}; -}; - -//------------------------------------------------------------------------------------------------------------ -constexpr auto leb128(std::span data) noexcept -> mbmff::parsed -{ - std::uint64_t value = 0; - std::uint32_t leb128_bytes = 0; - for (int i = 0; i < 8; i++) { - std::uint8_t leb128_byte = static_cast(data[i]); - - value |= static_cast(leb128_byte & 0x7f) << (i * 7); - leb128_bytes += 1; - if (!(leb128_byte & 0x80)) { - break; - } - } - return {value, leb128_bytes}; -} - -constexpr auto parse_obu(std::span data) noexcept -> std::expected -{ - any_obu_view header{}; - std::uint64_t offset = 0; - - // Byte 0: - std::uint8_t byte0 = static_cast(data[offset++]); - header.forbidden_bit = (byte0 >> 7) & 0x01; - header.type = (byte0 >> 3) & 0x0F; - header.extension_flag = (byte0 >> 2) & 0x01; - header.has_size_field = (byte0 >> 1) & 0x01; - header.reserved = byte0 & 0x01; - - if (header.forbidden_bit) { - return std::unexpected(unexpected{error_code::invalid_format}); - } - - // read extension header if present - if (header.extension_flag) { - if (data.size() < offset + 1) { - return std::unexpected(unexpected{error_code::need_more_data, offset + 1}); - } - - std::uint8_t ext_byte = static_cast(data[offset++]); - header.temporal_id = (ext_byte >> 5) & 0x07; - header.spatial_id = (ext_byte >> 3) & 0x03; - header.extension_reserved_1 = ext_byte & 0x07; - if (header.extension_reserved_1 || header.reserved) { - return std::unexpected(unexpected{error_code::invalid_format}); - } - } - - std::uint64_t obu_size = 0; - // read OBU size if present - if (header.has_size_field) { - if (data.size() < offset + 1) { - return std::unexpected(unexpected{error_code::need_more_data, offset + 1}); - } - auto [xobu_size, leb128_bytes] = leb128(data.subspan(offset)); - if (obu_size > any_obu_view::max_obu_size) { - return std::unexpected(unexpected{error_code::invalid_format}); - } - obu_size = xobu_size; - offset += leb128_bytes; - } else { - // If no size field is present, the OBU extends to the end of the data - obu_size = data.size() - offset; - } - - // place the payload span after the header and any size field - header.payload = data.subspan(offset, obu_size); - return std::expected{header}; -} - -//------------------------------------------------------------------------------------------------------------ -// Sequence Header -constexpr auto basic_obu_view::header() const noexcept -> obu_sequence_header -{ - obu_sequence_header result{}; - bit_reader reader{payload}; - - // Byte 0 - std::uint8_t read_byte = static_cast(payload[0]); - result.seq_profile = reader.read_bits(3); - result.still_picture = reader.read_bits(1); - result.reduced_still_picture_header = reader.read_bits(1); - - if (result.reduced_still_picture_header) { - result.operating_points[0].operating_point_idc = reader.read_bits(5); - } else { - result.timing_info_present_flag = reader.read_bits(1); - if (result.timing_info_present_flag) { - // Timing info - result.timing_info_data.num_units_in_display_tick = reader.read_bits(32); - result.timing_info_data.time_scale = reader.read_bits(32); - result.timing_info_data.equal_picture_interval = reader.read_bits(1); - if (result.timing_info_data.equal_picture_interval) { - result.timing_info_data.num_ticks_per_picture_minus_1 = reader.uvlc(); - } - - // Decoder model info - result.decoder_model_info_present_flag = reader.read_bits(1); - if (result.decoder_model_info_present_flag) { - result.decoder_model_info_data.buffer_delay_length_minus_1 = reader.read_bits(5); - result.decoder_model_info_data.num_units_in_decoding_tick = reader.read_bits(32); - result.decoder_model_info_data.buffer_removal_time_length_minus_1 = reader.read_bits(5); - result.decoder_model_info_data.frame_presentation_time_length_minus_1 = reader.read_bits( - 5 - ); - } - - result.initial_display_delay_present_flag = reader.read_bits(1); - - // Operating points - result.operating_points_cnt_minus_1 = reader.read_bits(5); - for (std::size_t i = 0; i <= result.operating_points_cnt_minus_1; i++) { - auto& op = result.operating_points[i]; - op.operating_point_idc = reader.read_bits(12); - op.seq_level_idx = reader.read_bits(5); - if (op.seq_level_idx > 7) { - op.seq_tier = reader.read_bits(1); - } - - // Decoding parameters info - if (result.decoder_model_info_present_flag) { - op.decoder_model_present_for_this_op = reader.read_bits(1); - if (op.decoder_model_present_for_this_op) { - std::size_t bits_for_buffer_delay = result.decoder_model_info_data.buffer_delay_length_minus_1 - + 1; - - op.operating_parameters.decoder_buffer_delay = reader.read_bits( - bits_for_buffer_delay - ); - op.operating_parameters.encoder_buffer_delay = reader.read_bits( - bits_for_buffer_delay - ); - op.operating_parameters.low_delay_mode_flag = reader.read_bits(1); - } - } - - // Initial display delay - if (result.initial_display_delay_present_flag) { - op.initial_display_delay_present_for_this_op = reader.read_bits(1); - if (op.initial_display_delay_present_for_this_op) { - op.initial_display_delay_minus_1 = reader.read_bits(4); - } - } - } - } - } - - // here by the spec we should select - // operatingPoint = choose_operating_point( ) - // OperatingPointIdc = operating_point_idc[operatingPoint] - - result.frame_width_bits_minus_1 = reader.read_bits(4); - result.frame_height_bits_minus_1 = reader.read_bits(4); - result.max_frame_width_minus_1 = reader.read_bits(result.frame_width_bits_minus_1 + 1); - result.max_frame_height_minus_1 = reader.read_bits(result.frame_height_bits_minus_1 + 1); - - if (!result.reduced_still_picture_header) { - result.frame_id_numbers_present_flag = reader.read_bits(1); - } - - if (result.frame_id_numbers_present_flag) { - result.delta_frame_id_length_minus_2 = reader.read_bits(4); - result.additional_frame_id_length_minus_1 = reader.read_bits(3); - } - - result.use_128x128_superblock = reader.read_bits(1); - result.enable_filter_intra = reader.read_bits(1); - result.enable_intra_edge_filter = reader.read_bits(1); - - if (!result.reduced_still_picture_header) { - result.enable_interintra_compound = reader.read_bits(1); - result.enable_masked_compound = reader.read_bits(1); - result.enable_warped_motion = reader.read_bits(1); - result.enable_dual_filter = reader.read_bits(1); - result.enable_order_hint = reader.read_bits(1); - - if (result.enable_order_hint) { - result.enable_jnt_comp = reader.read_bits(1); - result.enable_ref_frame_mvs = reader.read_bits(1); - } - result.seq_choose_screen_content_tools = reader.read_bits(1); - if (result.seq_choose_screen_content_tools) { - result.seq_force_screen_content_tools = reader.read_bits(1); - } - - if (result.seq_force_screen_content_tools > 0) { - result.seq_choose_integer_mv = reader.read_bits(1); - if (result.seq_choose_integer_mv) { - result.seq_force_integer_mv = reader.read_bits(1); - } - } - - if (result.enable_order_hint) { - result.order_hint_bits_minus_1 = reader.read_bits(3); - } - } - result.enable_superres = reader.read_bits(1); - result.enable_cdef = reader.read_bits(1); - result.enable_restoration = reader.read_bits(1); - - // Color config - auto& cc = result.color_config_data; - cc.high_bitdepth = reader.read_bits(1); - if (result.seq_profile == 2 && cc.high_bitdepth) { - cc.twelve_bit = reader.read_bits(1); - } - - if (result.seq_profile != 1) { - cc.monochrome = reader.read_bits(1); - } - - cc.color_description_present_flag = reader.read_bits(1); - if (cc.color_description_present_flag) { - cc.color_description_data.color_primaries = static_cast( - reader.read_bits(8) - ); - cc.color_description_data.transfer_characteristics = static_cast( - reader.read_bits(8) - ); - cc.color_description_data.matrix_coefficients = static_cast( - reader.read_bits(8) - ); - } - - if (!cc.monochrome) { - cc.color_range = reader.read_bits(1); - cc.subsampling_x = 1; - cc.subsampling_y = 1; - result.film_grain_params_present = reader.read_bits(1); - return result; - } - if (cc.color_description_data.color_primaries == mbmff::av1_color_primaries::CP_BT_709 - && cc.color_description_data.transfer_characteristics == mbmff::av1_transfer_characteristics::TC_SRGB - && cc.color_description_data.matrix_coefficients == mbmff::av1_matrix_coefficients::MC_IDENTITY) { - cc.color_range = 1; - cc.subsampling_x = 0; - cc.subsampling_y = 0; - } else { - cc.color_range = reader.read_bits(1); - if (result.seq_profile == 0) { - cc.subsampling_x = 1; - cc.subsampling_y = 1; - } else if (result.seq_profile == 1) { - cc.subsampling_x = 0; - cc.subsampling_y = 0; - } else { - if (cc.twelve_bit) { - cc.subsampling_x = reader.read_bits(1); - if (cc.subsampling_x) { - cc.subsampling_y = reader.read_bits(1); - } - } else { - cc.subsampling_x = 1; - cc.subsampling_y = 0; - } - } - if (cc.subsampling_x && cc.subsampling_y) { - cc.chroma_sample_position = static_cast( - reader.read_bits(2) - ); - } - } - cc.separate_uv_delta_q = reader.read_bits(1); - // End color config - - result.film_grain_params_present = reader.read_bits(1); - return result; -} - -//------------------------------------------------------------------------------------------------------------ -// iterator - -struct obu_iterator { - using iterator_category = std::forward_iterator_tag; - using value_type = std::expected; - using difference_type = std::ptrdiff_t; - - std::span data{}; - -public: - constexpr obu_iterator() noexcept = default; - constexpr explicit obu_iterator(std::span data) noexcept - : data(data) - {} - constexpr explicit obu_iterator(const av1C_header& header) noexcept - : obu_iterator(header.config_obus) - {} - -public: - constexpr auto begin() const noexcept -> obu_iterator - { - return *this; - } - constexpr auto end() const noexcept -> obu_iterator - { - return {}; - } - constexpr auto try_get() const noexcept -> std::expected - { - return parse_obu(data); - } - constexpr auto operator*() const noexcept -> std::expected - { - return try_get(); - } - constexpr auto operator++() noexcept -> obu_iterator& - { - if (data.empty()) { - data = {}; - return *this; - } - auto obu = try_get(); - if (!obu) { - // If parsing fails mid-stream, terminate the iteration - data = {}; - return *this; - } - - // Calculate total bytes consumed by this OBU (Header + LEB128 size + Payload) - // By taking the end of the payload and subtracting the start of our data block, - // we get the exact number of bytes this specific OBU occupied. - std::size_t bytes_consumed = (obu->payload.data() + obu->payload.size()) - data.data(); - - if (bytes_consumed >= data.size()) { - data = {}; // We reached the end - } else { - // Shrink the span from the front, moving to the next OBU - data = data.subspan(bytes_consumed); - } - - return *this; - } - constexpr auto operator++(int) noexcept -> obu_iterator - { - auto tmp = *this; - ++(*this); - return tmp; - } - constexpr bool operator==(const obu_iterator& other) const noexcept - { - // special case: both iterators are at the end (empty) - if (data.empty() && other.data.empty()) { - return true; - } - return data.data() == other.data.data() && data.size() == other.data.size(); - } -}; - -} // namespace mbmff diff --git a/include/mbmff/boxes/av1C.hpp b/include/mbmff/boxes/av1C.hpp new file mode 100644 index 0000000..16d02c1 --- /dev/null +++ b/include/mbmff/boxes/av1C.hpp @@ -0,0 +1,85 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { +struct av1C_data { + std::uint8_t marker : 1 = 0; + std::uint8_t version : 7 = 0; + std::uint8_t seq_profile : 3 = 0; + std::uint8_t seq_level_idx_0 : 5 = 0; + std::uint8_t seq_tier_0 : 1 = 0; + std::uint8_t high_bitdepth : 1 = 0; + std::uint8_t twelve_bit : 1 = 0; + std::uint8_t monochrome : 1 = 0; + std::uint8_t chroma_subsampling_x : 1 = 0; + std::uint8_t chroma_subsampling_y : 1 = 0; + std::uint8_t chroma_sample_position : 2 = 0; + std::uint8_t reserved : 3 = 0; + std::uint8_t initial_presentation_delay_present : 1 = 0; + std::uint8_t initial_presentation_delay_minus_one : 4 = 0; + + std::span config_obus{}; +}; + +template <> +struct basic_box_view : public box_view_base { + constexpr static box_properties properties = box_properties::none; + constexpr static auto validate(mbmff::any_box_view box) noexcept -> mbmff::result; + constexpr auto value() const noexcept -> mbmff::av1C_data; +}; + +inline constexpr auto basic_box_view::validate(mbmff::any_box_view box) noexcept + -> mbmff::result +{ + if (box.payload.size() < 4) { + return mbmff::make_result(mbmff::error_code::need_more_data, 4); + } + return {box}; +} + +constexpr auto basic_box_view::value() const noexcept -> mbmff::av1C_data +{ + av1C_data result{}; + std::uint8_t read_byte = static_cast(payload[0]); + result.marker = (read_byte >> 7) & 0x01; + result.version = read_byte & 0x7F; + + read_byte = static_cast(payload[1]); + result.seq_profile = (read_byte >> 5) & 0x07; + result.seq_level_idx_0 = read_byte & 0x1F; + + read_byte = static_cast(payload[2]); + result.seq_tier_0 = (read_byte >> 7) & 0x01; + result.high_bitdepth = (read_byte >> 6) & 0x01; + result.twelve_bit = (read_byte >> 5) & 0x01; + result.monochrome = (read_byte >> 4) & 0x01; + result.chroma_subsampling_x = (read_byte >> 3) & 0x01; + result.chroma_subsampling_y = (read_byte >> 2) & 0x01; + result.chroma_sample_position = read_byte & 0x03; + + read_byte = static_cast(payload[3]); + result.reserved = (read_byte >> 5) & 0x07; + result.initial_presentation_delay_present = (read_byte >> 4) & 0x01; + if (result.initial_presentation_delay_present) { + result.initial_presentation_delay_minus_one = read_byte & 0x0F; + } + + result.config_obus = payload.subspan(4); + return result; +} + +#ifdef MBMFF_ENABLE_CONSTEXPR_TEST +// validate must reject empty payload +static_assert(!mbmff::basic_box_view::validate( + {mbmff::box_header{}, std::span{}})); + +// validate must accept 4-byte payload +static_assert([] { + constexpr std::byte min_data[4]{}; + auto r = mbmff::basic_box_view::validate( + {mbmff::box_header{}, std::span(min_data)}); + return static_cast(r); +}()); +#endif + +} // namespace mbmff diff --git a/include/mbmff/boxes/box_view.hpp b/include/mbmff/boxes/box_view.hpp new file mode 100644 index 0000000..e67e583 --- /dev/null +++ b/include/mbmff/boxes/box_view.hpp @@ -0,0 +1,203 @@ +#pragma once +#include +#include "common.hpp" + +#define MBMFF_ITERATE_BOX_TYPES(MACRO) \ + MACRO(ftyp) \ + MACRO(meta) \ + MACRO(mdat) \ + MACRO(moov) \ + MACRO(trak) \ + MACRO(mdia) \ + MACRO(minf) \ + MACRO(stbl) \ + MACRO(dinf) \ + MACRO(edts) \ + MACRO(udta) \ + MACRO(mvex) \ + MACRO(moof) \ + MACRO(traf) \ + MACRO(mfra) \ + MACRO(iprp) \ + MACRO(ipco) \ + MACRO(iinf) \ + MACRO(iref) \ + MACRO(iloc) \ + MACRO(hdlr) \ + MACRO(pitm) \ + MACRO(ispe) \ + MACRO(av1C) \ + MACRO(pixi) \ + MACRO(ipma) \ + MACRO(pasp) \ + MACRO(infe) + +#define MBMFF_FLAG_OPERATORS(EnumType) \ + constexpr auto operator|(EnumType a, EnumType b) noexcept -> EnumType \ + { \ + return static_cast(mbmff::to_underlying(a) | mbmff::to_underlying(b)); \ + } \ + constexpr auto operator&(EnumType a, EnumType b) noexcept -> EnumType \ + { \ + return static_cast(mbmff::to_underlying(a) & mbmff::to_underlying(b)); \ + } \ + constexpr auto operator+(EnumType a) noexcept -> std::underlying_type_t \ + { \ + return mbmff::to_underlying(a); \ + } \ + constexpr auto has(EnumType a, EnumType check) noexcept -> bool \ + { \ + return (a & check) != EnumType::none; \ + } + +namespace mbmff { +#define MBMFF_ITERATE_ENUM(name) name = mbmff::fourcc(#name), +enum class box_type : std::uint32_t { unknown, MBMFF_ITERATE_BOX_TYPES(MBMFF_ITERATE_ENUM) }; + +//------------------------------------------------------------------------------------------------------------ +enum class box_properties : std::uint32_t { + none = 0, + full_box = 1 << 0, + container = 1 << 1, +}; +MBMFF_FLAG_OPERATORS(box_properties) + +//------------------------------------------------------------------------------------------------------------ +struct box_header { + std::uint64_t size_ = 0; + mbmff::box_type type_ = mbmff::box_type::unknown; + std::uint8_t version_ = 0; + std::array flags_{}; + +public: + constexpr auto type_string() const noexcept -> mbmff::fourcc_string + { + return mbmff::fourcc_string::from_uint32(mbmff::to_underlying(type_)); + } + constexpr auto size() const noexcept -> std::uint64_t + { + return size_; + } + constexpr auto type() const noexcept -> mbmff::box_type + { + return type_; + } + constexpr auto flags() const noexcept -> std::uint32_t + { + return (static_cast(flags_[0]) << 16) | (static_cast(flags_[1]) << 8) + | static_cast(flags_[2]); + } + constexpr auto version() const noexcept -> std::uint8_t + { + return version_; + } + constexpr auto valid() const noexcept -> bool + { + return type_ != mbmff::box_type::unknown; + } + constexpr operator bool() const noexcept + { + return valid(); + } + +public: + constexpr void fill_full_header(std::span data) noexcept + { + version_ = static_cast(data[0]); + flags_[0] = static_cast(data[1]); + flags_[1] = static_cast(data[2]); + flags_[2] = static_cast(data[3]); + } +}; + +using any_box_view = struct box_view_base; + +struct box_view_base : mbmff::box_header { + std::span payload{}; + +public: + constexpr static auto validate(mbmff::any_box_view box) noexcept -> mbmff::result + { + return {box}; + } +}; + +template +struct basic_box_view : public mbmff::box_view_base { + constexpr static mbmff::box_properties properties = mbmff::box_properties::none; +}; + +//------------------------------------------------------------------------------------------------------------ +constexpr inline auto parse_box_header(std::span data) noexcept + -> mbmff::result> +{ + if (data.size() < 8) { + return mbmff::make_result>(mbmff::error_code::need_more_data, 8); + } + + std::uint64_t size = mbmff::read_be(data); + if (size == 0) { + return mbmff::make_result>(mbmff::error_code::invalid_format); + } + + char type_str[4]{ + static_cast(data[4]), + static_cast(data[5]), + static_cast(data[6]), + static_cast(data[7]), + }; + mbmff::box_type type = mbmff::box_type(mbmff::fourcc(type_str)); + if (size == 1) { + if (data.size() < 16) { + return mbmff::make_result>(mbmff::error_code::need_more_data, 16); + } + size = mbmff::read_be(data.subspan(8)); + } + + return {mbmff::parsed{{size, type}, size == 1 ? 16u : 8u}}; +} + +#ifdef MBMFF_ENABLE_CONSTEXPR_TEST +// parse_box_header must reject data < 8 bytes +static_assert(!mbmff::parse_box_header(std::span{}), "parse_box_header should reject empty data"); +static_assert( + []() { + std::array data{}; + return !mbmff::parse_box_header(std::span(data)); + }(), + "parse_box_header should reject data with less than 8 bytes" +); +static_assert( + []() { + std::array data{ + std::byte(1), + }; + mbmff::parse_box_header(std::span(data)); + return true; + }(), + "parse_box_header should accept data with 16 bytes (for large size)" +); + +// Try parsing a valid box header +static_assert( + []() { + std::array data{ + std::byte(12), // size = 12 + std::byte(0), + std::byte(0), + std::byte(0), + std::byte('m'), + std::byte('d'), + std::byte('a'), + std::byte('t'), // type = "mdat" + }; + auto result = mbmff::parse_box_header(std::span(data)); + return result && result->value.size() == 12 && result->value.type() == mbmff::box_type::mdat + && result->consumed == 8; + }(), + "parse_box_header should parse a valid box header correctly" +); + +#endif + +} // namespace mbmff diff --git a/include/mbmff/boxes/common.hpp b/include/mbmff/boxes/common.hpp new file mode 100644 index 0000000..32feb59 --- /dev/null +++ b/include/mbmff/boxes/common.hpp @@ -0,0 +1,237 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace mbmff { +enum class error_code { + success = 0, + invalid_format, + need_more_data, +}; + +//------------------------------------------------------------------------------------------------------------ +static constexpr auto to_string(mbmff::error_code code) noexcept -> std::string_view +{ + switch (code) { + case mbmff::error_code::success: + return "Success"; + case mbmff::error_code::invalid_format: + return "Invalid format"; + case mbmff::error_code::need_more_data: + return "Need more data"; + default: + return "Unknown error code"; + } +} +//------------------------------------------------------------------------------------------------------------ +template +struct parsed { + Type value; + std::size_t consumed = 0; +}; + +template +struct result { + Type value{}; + mbmff::error_code code = mbmff::error_code::success; + std::size_t needed = 0; + +public: + constexpr explicit operator bool() const noexcept + { + return code == mbmff::error_code::success; + } + constexpr auto operator->() noexcept -> Type* + { + return &value; + } + constexpr auto operator*() noexcept -> Type& + { + return value; + } + constexpr auto operator*() const noexcept -> const Type& + { + return value; + } +}; + +// Error result — Type must be specified explicitly (e.g. make_result(code, n)) +template +constexpr auto make_result(mbmff::error_code code, std::size_t needed = 0) noexcept -> mbmff::result +{ + return {{}, code, needed}; +} + +template + requires(!std::same_as) +constexpr auto make_result(Type value) noexcept -> mbmff::result +{ + return {value}; +} + +//------------------------------------------------------------------------------------------------------------ + +constexpr auto fourcc(const char* str) noexcept -> std::uint32_t +{ + return (static_cast(str[3]) << 24) | (static_cast(str[2]) << 16) + | (static_cast(str[1]) << 8) | static_cast(str[0]); +} + +struct fourcc_string { + std::array data_{}; + +public: + constexpr auto view() const noexcept -> std::string_view + { + return std::string_view(data_.data(), data_.size()); + } + constexpr auto data() const noexcept -> const std::span + { + return data_; + } + constexpr auto data() noexcept -> std::span + { + return data_; + } + constexpr auto to_uint32() const noexcept -> std::uint32_t + { + return fourcc(data_.data()); + } + + constexpr auto operator==(const mbmff::fourcc_string& other) const noexcept -> bool + { + return data_ == other.data_; + } + constexpr auto operator[](std::size_t index) const noexcept -> char + { + return data_[index]; + } + constexpr operator std::string_view() const noexcept + { + return view(); + } + +public: + constexpr static auto from_data(std::span data) noexcept -> mbmff::fourcc_string + { + return mbmff::fourcc_string{std::array{ + static_cast(data[0]), + static_cast(data[1]), + static_cast(data[2]), + static_cast(data[3]), + }}; + } + constexpr static auto from_uint32(std::uint32_t value) noexcept -> mbmff::fourcc_string + { + return fourcc_string{std::array{ + static_cast(value & 0xFF), + static_cast((value >> 8) & 0xFF), + static_cast((value >> 16) & 0xFF), + static_cast((value >> 24) & 0xFF), + }}; + } +}; + +//------------------------------------------------------------------------------------------------------------ +template +[[nodiscard]] constexpr auto to_underlying(Type value) noexcept -> std::underlying_type_t +{ + return static_cast>(value); +} + +template +constexpr auto byteswap(T value) noexcept -> T +{ + if constexpr (sizeof(T) == 1) { + return value; + } else if constexpr (sizeof(T) == 2) { + return static_cast(((value & 0x00FF) << 8) | ((value & 0xFF00) >> 8)); + } else if constexpr (sizeof(T) == 4) { + return static_cast( + ((value & 0x000000FF) << 24) | ((value & 0x0000FF00) << 8) | ((value & 0x00FF0000) >> 8) + | ((value & 0xFF000000) >> 24) + ); + } else if constexpr (sizeof(T) == 8) { + return static_cast( + ((value & 0x00000000000000FFULL) << 56) | ((value & 0x000000000000FF00ULL) << 40) + | ((value & 0x0000000000FF0000ULL) << 24) | ((value & 0x00000000FF000000ULL) << 8) + | ((value & 0x000000FF00000000ULL) >> 8) | ((value & 0x0000FF0000000000ULL) >> 24) + | ((value & 0x00FF000000000000ULL) >> 40) | ((value & 0xFF00000000000000ULL) >> 56) + ); + } +} + +template +constexpr auto read_be(std::span data) noexcept -> T +{ + T value{}; + if (std::is_constant_evaluated()) { + for (std::size_t i = 0; i < sizeof(T); ++i) { + value <<= 8; + value |= static_cast(data[i]); + } + } else { + std::memcpy(&value, data.data(), sizeof(value)); + } + return mbmff::byteswap(value); +} + +//------------------------------------------------------------------------------------------------------------ +struct parsed_cstr { + std::string_view value{}; + std::size_t next = 0; +}; + +constexpr auto read_cstr(std::span data, std::size_t offset) noexcept -> parsed_cstr +{ + if (offset >= data.size()) { + return {}; + } + + const auto* begin = reinterpret_cast(data.data() + offset); + std::size_t length = 0; + for (std::size_t i = offset; i < data.size(); ++i) { + if (data[i] == std::byte{0}) { + break; + } + ++length; + } + + std::size_t next = offset + length; + if (next < data.size() && data[next] == std::byte{0}) { + ++next; + } + + return {std::string_view(begin, length), next}; +} + +} // namespace mbmff + +#ifdef MBMFF_ENABLE_CONSTEXPR_TEST +// result tests +static_assert(mbmff::make_result(42), "make_result(42) should be sucessful (result)"); +static_assert(!mbmff::make_result(mbmff::error_code::invalid_format), "make_result(code) should be an error"); + +// fourcc tests +static_assert(mbmff::fourcc("abcd") == 0x64636261, "fourcc(\"abcd\") should be 0x64636261"); +static_assert( + []() { + constexpr auto str = mbmff::fourcc_string::from_uint32(0x64636261); + constexpr auto d = std::array{'a', 'b', 'c', 'd'}; + return str[0] == d[0] && str[1] == d[1] && str[2] == d[2] && str[3] == d[3]; + }(), + "fourcc_string::from_uint32(0x64636261) should return 'abcd'" +); + +// byteswap tests +static_assert(mbmff::byteswap(std::uint16_t{0x1234}) == 0x3412, "byteswap(0x1234) should be 0x3412"); +static_assert(mbmff::byteswap(std::uint32_t{0x12345678}) == 0x78563412, "byteswap(0x12345678) should be 0x78563412"); +static_assert( + mbmff::byteswap(std::uint64_t{0x0123456789ABCDEF}) == 0xEFCDAB8967452301, + "byteswap(0x0123456789ABCDEF) should be 0xEFCDAB8967452301" +); +#endif diff --git a/include/mbmff/boxes/containers.hpp b/include/mbmff/boxes/containers.hpp new file mode 100644 index 0000000..073bd28 --- /dev/null +++ b/include/mbmff/boxes/containers.hpp @@ -0,0 +1,42 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +//------------------------------------------------------------------------------------------------------------ +// Pure container boxes — no payload fields, just child boxes. +// All share the same pattern: container property, always-passes validate, empty value(). +//------------------------------------------------------------------------------------------------------------ + +#define MBMFF_DEFINE_PURE_CONTAINER(box) \ + struct box##_data {}; \ + \ + template <> \ + struct basic_box_view : public mbmff::box_view_base { \ + constexpr static mbmff::box_properties properties = mbmff::box_properties::container; \ + } + +MBMFF_DEFINE_PURE_CONTAINER(moov); +MBMFF_DEFINE_PURE_CONTAINER(trak); +MBMFF_DEFINE_PURE_CONTAINER(mdia); +MBMFF_DEFINE_PURE_CONTAINER(minf); +MBMFF_DEFINE_PURE_CONTAINER(stbl); +MBMFF_DEFINE_PURE_CONTAINER(dinf); +MBMFF_DEFINE_PURE_CONTAINER(edts); +MBMFF_DEFINE_PURE_CONTAINER(mvex); +MBMFF_DEFINE_PURE_CONTAINER(udta); +MBMFF_DEFINE_PURE_CONTAINER(moof); +MBMFF_DEFINE_PURE_CONTAINER(traf); +MBMFF_DEFINE_PURE_CONTAINER(mfra); +MBMFF_DEFINE_PURE_CONTAINER(iprp); +MBMFF_DEFINE_PURE_CONTAINER(ipco); + +#undef MBMFF_DEFINE_PURE_CONTAINER + +#ifdef MBMFF_ENABLE_CONSTEXPR_TEST +static_assert( + mbmff::basic_box_view::validate({mbmff::box_header{}, std::span{}}) +); +#endif + +} // namespace mbmff diff --git a/include/mbmff/boxes/ftyp.hpp b/include/mbmff/boxes/ftyp.hpp new file mode 100644 index 0000000..b5a90d2 --- /dev/null +++ b/include/mbmff/boxes/ftyp.hpp @@ -0,0 +1,59 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { +struct ftyp_data { + mbmff::fourcc_string major_brand{}; + std::uint32_t minor_version = 0; + std::span compatible_brands{}; +}; + +template <> +struct basic_box_view : public mbmff::box_view_base { + constexpr static box_properties properties = box_properties::none; + constexpr static auto validate(mbmff::any_box_view box) noexcept -> mbmff::result; + constexpr auto value() const noexcept -> mbmff::ftyp_data; +}; + +inline constexpr auto mbmff::basic_box_view::validate(mbmff::any_box_view box) noexcept + -> mbmff::result +{ + if (box.payload.size() < 8) { + return mbmff::make_result(mbmff::error_code::need_more_data, 8); + } + + if (box.payload.size() % 4 != 0) { + return mbmff::make_result(mbmff::error_code::invalid_format); + } + + return {box}; +} + +constexpr auto mbmff::basic_box_view::value() const noexcept -> mbmff::ftyp_data +{ + const auto compatible_data = payload.subspan(8); + std::span compatible_brands( + reinterpret_cast(compatible_data.data()), + compatible_data.size() / 4 + ); + return mbmff::ftyp_data{ + fourcc_string::from_data(payload), + mbmff::read_be(payload.subspan(4)), + compatible_brands + }; +} +#ifdef MBMFF_ENABLE_CONSTEXPR_TEST +// validate must reject payload < 8 bytes +static_assert(!mbmff::basic_box_view::validate( + {mbmff::box_header{}, std::span{}})); + +// validate must reject payload not divisible by 4 +static_assert([] { + constexpr std::byte bad_div[10]{}; + auto r = mbmff::basic_box_view::validate( + {mbmff::box_header{}, std::span(bad_div)}); + return !r; +}()); +#endif + +} // namespace mbmff diff --git a/include/mbmff/boxes/hdlr.hpp b/include/mbmff/boxes/hdlr.hpp new file mode 100644 index 0000000..712a6f6 --- /dev/null +++ b/include/mbmff/boxes/hdlr.hpp @@ -0,0 +1,45 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct hdlr_data { + mbmff::fourcc_string handler_type{}; + std::string_view name{}; +}; + +template <> +struct basic_box_view : public mbmff::box_view_base { + constexpr static mbmff::box_properties properties = mbmff::box_properties::full_box; + constexpr static auto validate(mbmff::any_box_view box) noexcept -> mbmff::result; + constexpr auto value() const noexcept -> mbmff::hdlr_data; +}; + +inline constexpr auto basic_box_view::validate(mbmff::any_box_view box) noexcept + -> mbmff::result +{ + if (box.payload.size() < 4) { + return mbmff::make_result(mbmff::error_code::need_more_data, 4); + } + box.fill_full_header(box.payload); + box.payload = box.payload.subspan(4); + + if (box.payload.size() < 20) { + return mbmff::make_result(mbmff::error_code::need_more_data, 20); + } + return {box}; +} + +inline constexpr auto basic_box_view::value() const noexcept -> mbmff::hdlr_data +{ + mbmff::hdlr_data result{}; + result.handler_type = mbmff::fourcc_string::from_data(payload.subspan(4)); + auto name_span = payload.subspan(16); + if (!name_span.empty()) { + auto name = mbmff::read_cstr(name_span, 0); + result.name = name.value; + } + return result; +} + +} // namespace mbmff diff --git a/include/mbmff/boxes/iinf.hpp b/include/mbmff/boxes/iinf.hpp new file mode 100644 index 0000000..b2d1663 --- /dev/null +++ b/include/mbmff/boxes/iinf.hpp @@ -0,0 +1,51 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct iinf_data { + std::uint32_t entry_count = 0; +}; + +template <> +struct basic_box_view : public mbmff::box_view_base { + constexpr static mbmff::box_properties properties + = mbmff::box_properties::full_box | mbmff::box_properties::container; + constexpr static auto validate(mbmff::any_box_view box) noexcept -> mbmff::result; + constexpr auto value() const noexcept -> mbmff::iinf_data; +}; + +inline constexpr auto basic_box_view::validate(mbmff::any_box_view box) noexcept + -> mbmff::result +{ + if (box.payload.size() < 4) { + return mbmff::make_result(mbmff::error_code::need_more_data, 4); + } + box.fill_full_header(box.payload); + box.payload = box.payload.subspan(4); + + auto entry_size = (box.version() == 0) ? std::size_t{2} : std::size_t{4}; + if (box.payload.size() < entry_size) { + return mbmff::make_result(mbmff::error_code::need_more_data, entry_size); + } + box.payload = box.payload.subspan(entry_size); + return {box}; +} + +inline constexpr auto basic_box_view::value() const noexcept -> mbmff::iinf_data +{ + mbmff::iinf_data result{}; + auto entry_size = (version() == 0) ? std::size_t{2} : std::size_t{4}; + if (payload.size() == 0 && payload.data() == nullptr) { + return result; + } + auto* data = payload.data() - static_cast(entry_size); + if (version() == 0) { + result.entry_count = mbmff::read_be(std::span(data, 2)); + } else { + result.entry_count = mbmff::read_be(std::span(data, 4)); + } + return result; +} + +} // namespace mbmff diff --git a/include/mbmff/boxes/iloc.hpp b/include/mbmff/boxes/iloc.hpp new file mode 100644 index 0000000..69362d8 --- /dev/null +++ b/include/mbmff/boxes/iloc.hpp @@ -0,0 +1,268 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct iloc_extent { + std::uint64_t index = 0; + std::uint64_t offset = 0; + std::uint64_t length = 0; +}; + +struct iloc_item { + std::uint64_t base_offset = 0; + std::uint32_t item_id = 0; + + std::uint8_t iloc_version = 0; + std::uint8_t offset_size = 0; + std::uint8_t length_size = 0; + std::uint8_t index_size = 0; + + std::uint8_t construction_method = 0; + + std::uint16_t data_reference_index = 0; + std::uint32_t extent_count = 0; + + std::span extent_data{}; + +public: + constexpr auto get_extent(std::size_t index) const noexcept -> mbmff::iloc_extent; + constexpr auto size() const noexcept -> std::size_t + { + return extent_count; + } + constexpr auto operator[](std::size_t index) const noexcept -> mbmff::iloc_extent + { + return get_extent(index); + } + constexpr auto extent_size() const noexcept -> std::size_t + { + return std::size_t(offset_size) + length_size + index_size; + } +}; + +constexpr auto mbmff::iloc_item::get_extent(std::size_t index) const noexcept -> mbmff::iloc_extent +{ + auto entry_span = extent_data.subspan(index * extent_size(), extent_size()); + mbmff::iloc_extent extent{}; + std::uint32_t offset = 0; + + if ((iloc_version == 1 || iloc_version == 2) && index_size > 0) { + if (index_size == 2) { + extent.index = mbmff::read_be(entry_span); + } else if (index_size == 4) { + extent.index = mbmff::read_be(entry_span); + } else if (index_size == 8) { + extent.index = mbmff::read_be(entry_span); + } + offset += index_size; + } + + if (offset_size == 2) { + extent.offset = mbmff::read_be(entry_span.subspan(offset)); + } else if (offset_size == 4) { + extent.offset = mbmff::read_be(entry_span.subspan(offset)); + } else if (offset_size == 8) { + extent.offset = mbmff::read_be(entry_span.subspan(offset)); + } + offset += offset_size; + + if (length_size == 2) { + extent.length = mbmff::read_be(entry_span.subspan(offset)); + } else if (length_size == 4) { + extent.length = mbmff::read_be(entry_span.subspan(offset)); + } else if (length_size == 8) { + extent.length = mbmff::read_be(entry_span.subspan(offset)); + } + return extent; +} + +struct iloc_data { + std::uint8_t offset_size = 0; + std::uint8_t length_size = 0; + std::uint8_t base_offset_size = 0; + std::uint8_t index_size = 0; + std::uint32_t item_count = 0; + std::span item_data{}; +}; + +template <> +struct basic_box_view : public mbmff::box_view_base { + constexpr static mbmff::box_properties properties = mbmff::box_properties::full_box; + constexpr static auto validate(mbmff::any_box_view box) noexcept -> mbmff::result; + constexpr auto value() const noexcept -> mbmff::iloc_data; +}; + +inline constexpr auto mbmff::basic_box_view::validate(mbmff::any_box_view box) noexcept + -> mbmff::result +{ + if (box.payload.size() < 4) { + return mbmff::make_result(mbmff::error_code::need_more_data, 4); + } + box.fill_full_header(box.payload); + box.payload = box.payload.subspan(4); + + if (box.payload.size() < 2) { + return mbmff::make_result(mbmff::error_code::need_more_data, 2); + } + auto entry_size = (box.version() < 2) ? std::size_t{2} : std::size_t{4}; + if (box.payload.size() < 2 + entry_size) { + return mbmff::make_result(mbmff::error_code::need_more_data, 2 + entry_size); + } + return {box}; +} + +inline constexpr auto mbmff::basic_box_view::value() const noexcept -> mbmff::iloc_data +{ + mbmff::iloc_data result{}; + if (payload.size() < 2) return result; + + auto first_byte = static_cast(payload[0]); + result.offset_size = (first_byte >> 4) & 0x0F; + result.length_size = first_byte & 0x0F; + auto second_byte = static_cast(payload[1]); + result.base_offset_size = (second_byte >> 4) & 0x0F; + result.index_size = second_byte & 0x0F; + + auto data = payload.subspan(2); + if (version() < 2) { + result.item_count = mbmff::read_be(data); + result.item_data = data.subspan(2); + } else { + result.item_count = mbmff::read_be(data); + result.item_data = data.subspan(4); + } + return result; +} + +class iloc_item_iterator { + using iterator_category = std::forward_iterator_tag; + using value_type = mbmff::iloc_item; + using difference_type = std::ptrdiff_t; + +private: + std::span remaining_; + std::uint8_t iloc_version_ = 0; + std::uint8_t offset_size_ = 0; + std::uint8_t length_size_ = 0; + std::uint8_t index_size_ = 0; + std::uint8_t base_offset_size_ = 0; + +public: + constexpr iloc_item_iterator() noexcept = default; + constexpr iloc_item_iterator(const mbmff::iloc_data& header, std::uint8_t version) noexcept + : remaining_(header.item_data) + , iloc_version_(version) + , offset_size_(header.offset_size) + , length_size_(header.length_size) + , index_size_(header.index_size) + , base_offset_size_(header.base_offset_size) + {} + constexpr explicit iloc_item_iterator(const mbmff::basic_box_view& box) noexcept + : iloc_item_iterator(box.value(), box.version()) + {} + +public: + constexpr auto extent_size() const noexcept -> std::size_t + { + return std::size_t(offset_size_) + length_size_ + (index_size_ > 0 ? index_size_ : 0); + } + constexpr auto begin() const noexcept -> mbmff::iloc_item_iterator + { + return *this; + } + constexpr auto end() const noexcept -> mbmff::iloc_item_iterator + { + return {}; + } + constexpr auto get() const noexcept -> mbmff::iloc_item + { + mbmff::iloc_item item{}; + if (remaining_.empty()) { + return item; + } + + std::size_t offset = 0; + + if (iloc_version_ < 2) { + item.item_id = mbmff::read_be(remaining_); + offset += 2; + } else { + item.item_id = mbmff::read_be(remaining_); + offset += 4; + } + + if (iloc_version_ == 1 || iloc_version_ == 2) { + auto value = mbmff::read_be(remaining_.subspan(offset)); + item.construction_method = static_cast(value & 0x000F); + offset += 2; + } + + item.data_reference_index = mbmff::read_be(remaining_.subspan(offset)); + offset += 2; + + switch (base_offset_size_) { + default: + case 0: + item.base_offset = 0; + break; + case 2: + item.base_offset = mbmff::read_be(remaining_.subspan(offset)); + break; + case 4: + item.base_offset = mbmff::read_be(remaining_.subspan(offset)); + break; + case 8: + item.base_offset = mbmff::read_be(remaining_.subspan(offset)); + break; + } + offset += base_offset_size_; + + item.extent_count = mbmff::read_be(remaining_.subspan(offset)); + item.extent_data = remaining_.subspan(offset + 2); + + item.iloc_version = iloc_version_; + item.offset_size = offset_size_; + item.length_size = length_size_; + item.index_size = index_size_; + return item; + } + + constexpr auto operator*() const noexcept -> mbmff::iloc_item + { + return get(); + } + + constexpr auto operator++() noexcept -> mbmff::iloc_item_iterator& + { + if (remaining_.empty()) { + remaining_ = {}; + return *this; + } + auto current_item = get(); + const std::byte* data = current_item.extent_data.data(); + std::size_t total_extent_size = current_item.extent_count * extent_size(); + + remaining_ = {data + total_extent_size, + remaining_.size() - (data - remaining_.data()) - total_extent_size}; + + return *this; + } + + constexpr auto operator++(int) noexcept -> mbmff::iloc_item_iterator + { + auto tmp = *this; + ++(*this); + return tmp; + } + constexpr bool operator==(const mbmff::iloc_item_iterator& other) const noexcept + { + if (remaining_.empty() && other.remaining_.empty()) { + return true; + } + return remaining_.data() == other.remaining_.data() + && remaining_.size() == other.remaining_.size(); + } +}; + +} // namespace mbmff diff --git a/include/mbmff/boxes/infe.hpp b/include/mbmff/boxes/infe.hpp new file mode 100644 index 0000000..5448acd --- /dev/null +++ b/include/mbmff/boxes/infe.hpp @@ -0,0 +1,85 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct infe_data { + std::uint32_t item_id = 0; + std::uint16_t item_protection_index = 0; + mbmff::fourcc_string item_type{}; + std::string_view item_name{}; + std::string_view content_type{}; + std::string_view content_encoding{}; + std::string_view item_uri_type{}; +}; + +template <> +struct basic_box_view : public mbmff::box_view_base { + constexpr static mbmff::box_properties properties = mbmff::box_properties::full_box; + constexpr static auto validate(mbmff::any_box_view box) noexcept -> mbmff::result; + constexpr auto value() const noexcept -> mbmff::infe_data; +}; + +inline constexpr auto basic_box_view::validate(mbmff::any_box_view box) noexcept + -> mbmff::result +{ + if (box.payload.size() < 4) { + return mbmff::make_result(mbmff::error_code::need_more_data, 4); + } + box.fill_full_header(box.payload); + box.payload = box.payload.subspan(4); + + auto need = (box.version() == 0 || box.version() == 1 || box.version() == 2) + ? std::size_t{4} : std::size_t{6}; + if (box.payload.size() < need) { + return mbmff::make_result(mbmff::error_code::need_more_data, need); + } + return {box}; +} + +inline constexpr auto basic_box_view::value() const noexcept -> mbmff::infe_data +{ + mbmff::infe_data result{}; + auto xversion = version(); + std::size_t offset = 0; + + if (xversion == 0 || xversion == 1 || xversion == 2) { + result.item_id = mbmff::read_be(payload); + offset = 2; + } else { + result.item_id = mbmff::read_be(payload); + offset = 4; + } + + result.item_protection_index = mbmff::read_be(payload.subspan(offset)); + offset += 2; + + if (xversion >= 2) { + result.item_type = mbmff::fourcc_string::from_data(payload.subspan(offset)); + offset += 4; + } + + auto name = mbmff::read_cstr(payload, offset); + result.item_name = name.value; + offset = name.next; + + if (xversion >= 2) { + if (result.item_type.view() == "mime") { + auto type = mbmff::read_cstr(payload, offset); + result.content_type = type.value; + offset = type.next; + + auto encoding = mbmff::read_cstr(payload, offset); + result.content_encoding = encoding.value; + offset = encoding.next; + } else if (result.item_type.view() == "uri ") { + auto uri = mbmff::read_cstr(payload, offset); + result.item_uri_type = uri.value; + offset = uri.next; + } + } + + return result; +} + +} // namespace mbmff diff --git a/include/mbmff/boxes/ipma.hpp b/include/mbmff/boxes/ipma.hpp new file mode 100644 index 0000000..a0d97e4 --- /dev/null +++ b/include/mbmff/boxes/ipma.hpp @@ -0,0 +1,170 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct ipma_association { + std::uint16_t essential : 1 = false; + std::uint16_t property_index : 15 = 0; +}; + +struct ipma_entry { + std::uint32_t item_id = 0; + std::uint8_t entry_count = 0; + std::uint8_t entry_size = 0; + std::span association_values{}; + +public: + constexpr auto operator[](std::size_t index) const noexcept -> mbmff::ipma_association; + constexpr auto size() const noexcept -> std::size_t + { + return entry_count; + } +}; + +constexpr auto ipma_entry::operator[](std::size_t index) const noexcept -> mbmff::ipma_association +{ + if (index >= entry_count) { + return {}; + } + mbmff::ipma_association association; + if (entry_size == 1) { + auto value = static_cast(association_values[index]); + association.essential = (value >> 7) & 0x01; + association.property_index = value & 0x7F; + } else { + auto value = mbmff::read_be(association_values.subspan(index * 2, 2)); + association.essential = (value >> 15) & 0x01; + association.property_index = value & 0x7FFF; + } + return association; +} + +struct ipma_data { + std::uint32_t entry_count = 0; + std::uint8_t index_size = 2; + std::uint8_t asoc_size = 1; + std::span entry_data{}; +}; + +template <> +struct basic_box_view : public mbmff::box_view_base { + constexpr static mbmff::box_properties properties = mbmff::box_properties::full_box; + constexpr static auto validate(mbmff::any_box_view box) noexcept -> mbmff::result; + constexpr auto value() const noexcept -> mbmff::ipma_data; +}; + +inline constexpr auto basic_box_view::validate(mbmff::any_box_view box) noexcept + -> mbmff::result +{ + if (box.payload.size() < 4) { + return mbmff::make_result(mbmff::error_code::need_more_data, 4); + } + box.fill_full_header(box.payload); + box.payload = box.payload.subspan(4); + + // entry_count (4 bytes) + at least one entry + if (box.payload.size() < 4) { + return mbmff::make_result(mbmff::error_code::need_more_data, 4); + } + return {box}; +} + +inline constexpr auto basic_box_view::value() const noexcept -> mbmff::ipma_data +{ + mbmff::ipma_data result{}; + result.entry_count = mbmff::read_be(payload); + result.index_size = (version() == 0) ? std::uint8_t{2} : std::uint8_t{4}; + result.asoc_size = (flags() & 0x01) ? std::uint8_t{2} : std::uint8_t{1}; + result.entry_data = payload.subspan(4); + return result; +} + +class ipma_entry_iterator { + using iterator_category = std::forward_iterator_tag; + using value_type = mbmff::ipma_entry; + using difference_type = std::ptrdiff_t; + +private: + std::uint32_t entry_count_ = 0; + std::uint8_t index_size_ = 0; + std::uint8_t asoc_size_ = 0; + std::span remaining_; + +public: + constexpr ipma_entry_iterator() noexcept = default; + constexpr explicit ipma_entry_iterator(const mbmff::ipma_data& data) noexcept + : entry_count_(data.entry_count) + , index_size_(data.index_size) + , asoc_size_(data.asoc_size) + , remaining_(data.entry_data) + {} + constexpr explicit ipma_entry_iterator(const mbmff::basic_box_view& box) noexcept + : ipma_entry_iterator(box.value()) + {} + +public: + constexpr auto begin() const noexcept -> mbmff::ipma_entry_iterator + { + return *this; + } + constexpr auto end() const noexcept -> mbmff::ipma_entry_iterator + { + return {}; + } + constexpr auto get() const noexcept -> mbmff::ipma_entry + { + mbmff::ipma_entry entry{}; + if (entry_count_ == 0 || remaining_.empty()) { + return entry; + } + if (index_size_ == 2) { + entry.item_id = mbmff::read_be(remaining_); + entry.association_values = remaining_.subspan(2); + } else { + entry.item_id = mbmff::read_be(remaining_); + entry.association_values = remaining_.subspan(4); + } + + entry.entry_count = static_cast(entry.association_values[0]); + entry.association_values = entry.association_values.subspan(1); + entry.entry_size = asoc_size_; + return entry; + } + constexpr auto operator*() const noexcept -> mbmff::ipma_entry + { + return get(); + } + constexpr auto operator++() noexcept -> mbmff::ipma_entry_iterator& + { + if (entry_count_ == 0 || remaining_.empty()) { + entry_count_ = 0; + remaining_ = {}; + return *this; + } + entry_count_--; + auto current_entry = get(); + + std::size_t offset = static_cast(current_entry.entry_count) * current_entry.entry_size + + index_size_ + 1; + + remaining_ = (offset >= remaining_.size()) ? std::span{} : remaining_.subspan(offset); + return *this; + } + constexpr auto operator++(int) noexcept -> mbmff::ipma_entry_iterator + { + auto tmp = *this; + ++(*this); + return tmp; + } + constexpr bool operator==(const mbmff::ipma_entry_iterator& other) const noexcept + { + if (entry_count_ == 0 && other.entry_count_ == 0) { + return true; + } + return entry_count_ == other.entry_count_ && remaining_.data() == other.remaining_.data() + && remaining_.size() == other.remaining_.size(); + } +}; + +} // namespace mbmff diff --git a/include/mbmff/boxes/iref.hpp b/include/mbmff/boxes/iref.hpp new file mode 100644 index 0000000..3134af9 --- /dev/null +++ b/include/mbmff/boxes/iref.hpp @@ -0,0 +1,24 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +template <> +struct basic_box_view : public mbmff::box_view_base { + constexpr static mbmff::box_properties properties + = mbmff::box_properties::full_box | mbmff::box_properties::container; + constexpr static auto validate(mbmff::any_box_view box) noexcept -> mbmff::result; +}; + +inline constexpr auto basic_box_view::validate(mbmff::any_box_view box) noexcept + -> mbmff::result +{ + if (box.payload.size() < 4) { + return mbmff::make_result(mbmff::error_code::need_more_data, 4); + } + box.fill_full_header(box.payload); + box.payload = box.payload.subspan(4); + return {box}; +} + +} // namespace mbmff diff --git a/include/mbmff/boxes/ispe.hpp b/include/mbmff/boxes/ispe.hpp new file mode 100644 index 0000000..1137581 --- /dev/null +++ b/include/mbmff/boxes/ispe.hpp @@ -0,0 +1,41 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct ispe_data { + std::uint32_t image_width = 0; + std::uint32_t image_height = 0; +}; + +template <> +struct basic_box_view : public mbmff::box_view_base { + constexpr static mbmff::box_properties properties = mbmff::box_properties::full_box; + constexpr static auto validate(mbmff::any_box_view box) noexcept -> mbmff::result; + constexpr auto value() const noexcept -> mbmff::ispe_data; +}; + +inline constexpr auto basic_box_view::validate(mbmff::any_box_view box) noexcept + -> mbmff::result +{ + if (box.payload.size() < 4) { + return mbmff::make_result(mbmff::error_code::need_more_data, 4); + } + box.fill_full_header(box.payload); + box.payload = box.payload.subspan(4); + + if (box.payload.size() < 8) { + return mbmff::make_result(mbmff::error_code::need_more_data, 8); + } + return {box}; +} + +inline constexpr auto basic_box_view::value() const noexcept -> mbmff::ispe_data +{ + mbmff::ispe_data result{}; + result.image_width = mbmff::read_be(payload); + result.image_height = mbmff::read_be(payload.subspan(4)); + return result; +} + +} // namespace mbmff diff --git a/include/mbmff/boxes/mdat.hpp b/include/mbmff/boxes/mdat.hpp new file mode 100644 index 0000000..1be213f --- /dev/null +++ b/include/mbmff/boxes/mdat.hpp @@ -0,0 +1,17 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { +template <> +struct basic_box_view : public mbmff::box_view_base { + constexpr static mbmff::box_properties properties = mbmff::box_properties::none; +}; + +#ifdef MBMFF_ENABLE_CONSTEXPR_TEST +// mdat always accepts any payload +static_assert( + mbmff::basic_box_view::validate({mbmff::box_header{}, std::span{}}) +); +#endif + +} // namespace mbmff diff --git a/include/mbmff/boxes/meta.hpp b/include/mbmff/boxes/meta.hpp new file mode 100644 index 0000000..fba5273 --- /dev/null +++ b/include/mbmff/boxes/meta.hpp @@ -0,0 +1,46 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { +template <> +struct basic_box_view : public mbmff::box_view_base { + // full_box is not ticked here because `meta` can + // be a regular box in some formats (e.g. QTFF) + constexpr static mbmff::box_properties properties = mbmff::box_properties::container; + constexpr static auto validate(mbmff::any_box_view box) noexcept -> mbmff::result; +}; + +inline constexpr auto basic_box_view::validate(mbmff::any_box_view box) noexcept + -> mbmff::result +{ + constexpr auto hdlr_type = "hdlr"; + bool has_full_box = false; + + if (box.payload.size() < 4) { + return mbmff::make_result(mbmff::error_code::need_more_data, 4); + } + + for (int i = 0; i < 4; ++i) { + if (box.payload[i] != std::byte(hdlr_type[i])) { + has_full_box = true; + break; + } + } + + if (has_full_box) { + box.fill_full_header(box.payload); + box.payload = box.payload.subspan(4); + } + return {box}; +} + +#ifdef MBMFF_ENABLE_CONSTEXPR_TEST +// payload starting with "hdlr" → not full_box → passes validation +static_assert([] { + constexpr std::byte no_fullbox[4]{std::byte{'h'}, std::byte{'d'}, std::byte{'l'}, std::byte{'r'}}; + auto r = mbmff::basic_box_view::validate({mbmff::box_header{}, std::span(no_fullbox)}); + return static_cast(r); +}()); +#endif + +} // namespace mbmff diff --git a/include/mbmff/boxes/pasp.hpp b/include/mbmff/boxes/pasp.hpp new file mode 100644 index 0000000..8f759e8 --- /dev/null +++ b/include/mbmff/boxes/pasp.hpp @@ -0,0 +1,35 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct pasp_data { + std::uint32_t h_spacing = 0; + std::uint32_t v_spacing = 0; +}; + +template <> +struct basic_box_view : public mbmff::box_view_base { + constexpr static mbmff::box_properties properties = mbmff::box_properties::none; + constexpr static auto validate(mbmff::any_box_view box) noexcept -> mbmff::result; + constexpr auto value() const noexcept -> mbmff::pasp_data; +}; + +inline constexpr auto basic_box_view::validate(mbmff::any_box_view box) noexcept + -> mbmff::result +{ + if (box.payload.size() < 8) { + return mbmff::make_result(mbmff::error_code::need_more_data, 8); + } + return {box}; +} + +inline constexpr auto basic_box_view::value() const noexcept -> mbmff::pasp_data +{ + mbmff::pasp_data result{}; + result.h_spacing = mbmff::read_be(payload); + result.v_spacing = mbmff::read_be(payload.subspan(4)); + return result; +} + +} // namespace mbmff diff --git a/include/mbmff/boxes/pitm.hpp b/include/mbmff/boxes/pitm.hpp new file mode 100644 index 0000000..6adbfb0 --- /dev/null +++ b/include/mbmff/boxes/pitm.hpp @@ -0,0 +1,44 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct pitm_data { + std::uint32_t item_id = 0; +}; + +template <> +struct basic_box_view : public mbmff::box_view_base { + constexpr static mbmff::box_properties properties = mbmff::box_properties::full_box; + constexpr static auto validate(mbmff::any_box_view box) noexcept -> mbmff::result; + constexpr auto value() const noexcept -> mbmff::pitm_data; +}; + +inline constexpr auto basic_box_view::validate(mbmff::any_box_view box) noexcept + -> mbmff::result +{ + if (box.payload.size() < 4) { + return mbmff::make_result(mbmff::error_code::need_more_data, 4); + } + box.fill_full_header(box.payload); + box.payload = box.payload.subspan(4); + + auto need = (box.version() == 0) ? std::size_t{2} : std::size_t{4}; + if (box.payload.size() < need) { + return mbmff::make_result(mbmff::error_code::need_more_data, need); + } + return {box}; +} + +inline constexpr auto basic_box_view::value() const noexcept -> mbmff::pitm_data +{ + mbmff::pitm_data result{}; + if (version() == 0) { + result.item_id = mbmff::read_be(payload); + } else { + result.item_id = mbmff::read_be(payload); + } + return result; +} + +} // namespace mbmff diff --git a/include/mbmff/boxes/pixi.hpp b/include/mbmff/boxes/pixi.hpp new file mode 100644 index 0000000..01aa101 --- /dev/null +++ b/include/mbmff/boxes/pixi.hpp @@ -0,0 +1,45 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct pixi_data { + std::span bits_per_channel{}; +}; + +template <> +struct basic_box_view : public mbmff::box_view_base { + constexpr static mbmff::box_properties properties = mbmff::box_properties::full_box; + constexpr static auto validate(mbmff::any_box_view box) noexcept -> mbmff::result; + constexpr auto value() const noexcept -> mbmff::pixi_data; +}; + +inline constexpr auto basic_box_view::validate(mbmff::any_box_view box) noexcept + -> mbmff::result +{ + if (box.payload.size() < 4) { + return mbmff::make_result(mbmff::error_code::need_more_data, 4); + } + box.fill_full_header(box.payload); + box.payload = box.payload.subspan(4); + + if (box.payload.size() < 1) { + return mbmff::make_result(mbmff::error_code::need_more_data, 1); + } + return {box}; +} + +inline constexpr auto basic_box_view::value() const noexcept -> mbmff::pixi_data +{ + mbmff::pixi_data result{}; + auto num_channels = static_cast(payload[0]); + auto remaining = payload.size() - 1; + auto count = (num_channels < remaining) ? num_channels : static_cast(remaining); + result.bits_per_channel = std::span( + static_cast(static_cast(payload.data() + 1)), + count + ); + return result; +} + +} // namespace mbmff diff --git a/include/mbmff/common.hpp b/include/mbmff/common.hpp deleted file mode 100644 index 333c55f..0000000 --- a/include/mbmff/common.hpp +++ /dev/null @@ -1,81 +0,0 @@ -#pragma once -#include -#include -#include - -namespace mbmff { -enum class error_code { - success = 0, - invalid_format, - need_more_data, -}; - -//------------------------------------------------------------------------------------------------------------ -static constexpr auto get_error_message(error_code code) noexcept -> std::string_view -{ - switch (code) { - case error_code::success: - return "Success"; - case error_code::invalid_format: - return "Invalid format"; - case error_code::need_more_data: - return "Need more data"; - default: - return "Unknown error code"; - } -} -//------------------------------------------------------------------------------------------------------------ -template -struct parsed { - Type value; - std::size_t consumed; -}; -struct unexpected { - mbmff::error_code code; - std::size_t needed; -}; - -//------------------------------------------------------------------------------------------------------------ -struct bit_reader { - std::span data{}; - std::size_t bit_offset = 0; - -public: - template - constexpr auto read_bits(std::size_t num_bits) noexcept -> T - { - T value = 0; - while (num_bits > 0) { - std::size_t byte_index = bit_offset / 8; - std::size_t bit_index = bit_offset % 8; - - std::size_t bits_avail = 8 - bit_index; - std::size_t bits_to_read = std::min(num_bits, bits_avail); - - std::uint8_t byte_val = static_cast(data[byte_index]); - - std::size_t shift = bits_avail - bits_to_read; - std::size_t mask = (1ULL << bits_to_read) - 1; - - value <<= bits_to_read; - value |= static_cast((byte_val >> shift) & mask); - - bit_offset += bits_to_read; - num_bits -= bits_to_read; - } - return value; - } - - constexpr auto uvlc() noexcept -> std::uint32_t - { - std::size_t leading_zero_bits = 0; - while (!read_bits(1)) { - if (++leading_zero_bits >= 32) { - return (1ull << 32) - 1; - } - } - std::size_t value = read_bits(leading_zero_bits); - return value + ((1u << leading_zero_bits) - 1); - } -}; -} // namespace mbmff diff --git a/include/mbmff/mbmff.hpp b/include/mbmff/mbmff.hpp index b60c7f4..5833c0d 100644 --- a/include/mbmff/mbmff.hpp +++ b/include/mbmff/mbmff.hpp @@ -1,930 +1,149 @@ #pragma once -#include -#include -#include -#include -#include -#include -#include -#include "av1.hpp" - -#define MBMFF_ITERATE_BOX_TYPES(MACRO) \ - MACRO(ftyp) \ - MACRO(meta) \ - MACRO(mdat) \ - MACRO(moov) \ - MACRO(trak) \ - MACRO(mdia) \ - MACRO(minf) \ - MACRO(stbl) \ - MACRO(dinf) \ - MACRO(edts) \ - MACRO(udta) \ - MACRO(mvex) \ - MACRO(moof) \ - MACRO(traf) \ - MACRO(mfra) \ - MACRO(iprp) \ - MACRO(ipco) \ - MACRO(iinf) \ - MACRO(iref) \ - MACRO(iloc) \ - MACRO(hdlr) \ - MACRO(pitm) \ - MACRO(ispe) \ - MACRO(av1C) \ - MACRO(pixi) \ - MACRO(ipma) \ - MACRO(pasp) \ - MACRO(infe) +#include "boxes/ftyp.hpp" +#include "boxes/meta.hpp" +#include "boxes/mdat.hpp" +#include "boxes/containers.hpp" +#include "boxes/av1C.hpp" +#include "boxes/iinf.hpp" +#include "boxes/iref.hpp" +#include "boxes/iloc.hpp" +#include "boxes/hdlr.hpp" +#include "boxes/pitm.hpp" +#include "boxes/ispe.hpp" +#include "boxes/pixi.hpp" +#include "boxes/pasp.hpp" +#include "boxes/ipma.hpp" +#include "boxes/infe.hpp" namespace mbmff { -//------------------------------------------------------------------------------------------------------------ -constexpr auto fourcc(const char* str) noexcept -> uint32_t -{ - return (static_cast(str[3]) << 24) | (static_cast(str[2]) << 16) - | (static_cast(str[1]) << 8) | static_cast(str[0]); -} - -#define MBMFF_ITERATE_ENUM(name) name = fourcc(#name), - -enum class box_type : uint32_t { unknown, MBMFF_ITERATE_BOX_TYPES(MBMFF_ITERATE_ENUM) }; - -//------------------------------------------------------------------------------------------------------------ -enum class box_properties : uint32_t { - none = 0, - full_box = 1 << 0, - container = 1 << 1, -}; -constexpr auto operator|(box_properties a, box_properties b) noexcept -> box_properties -{ - return static_cast(std::to_underlying(a) | std::to_underlying(b)); -} -constexpr auto operator&(box_properties a, box_properties b) noexcept -> box_properties -{ - return static_cast(std::to_underlying(a) & std::to_underlying(b)); -} -constexpr auto operator+(box_properties a) noexcept -> std::underlying_type_t -{ - return std::to_underlying(a); -} -constexpr auto has(box_properties a, box_properties check) noexcept -> bool -{ - return (a & check) != box_properties::none; -} - -//------------------------------------------------------------------------------------------------------------ -struct fourcc_string { - std::array data{}; - -public: - constexpr auto view() const noexcept -> std::string_view - { - return std::string_view(data.data(), data.size()); - } - constexpr operator std::string_view() const noexcept - { - return view(); - } - constexpr static auto from_data(std::span data) noexcept -> fourcc_string - { - return fourcc_string{std::array{ - static_cast(data[0]), - static_cast(data[1]), - static_cast(data[2]), - static_cast(data[3]), - }}; - } -}; - -//------------------------------------------------------------------------------------------------------------ -struct box_header { - std::uint64_t size = 0; - mbmff::box_type type{}; - std::uint8_t version = 0; - std::array flags{}; - -public: - constexpr auto type_string() const noexcept -> fourcc_string - { - std::uint32_t value = std::to_underlying(type); - std::array str{ - char(value & 0xFF), - char((value >> 8) & 0xFF), - char((value >> 16) & 0xFF), - char((value >> 24) & 0xFF), - }; - return {str}; - } - constexpr auto flags_value() const noexcept -> std::uint32_t - { - return (static_cast(flags[0]) << 16) | (static_cast(flags[1]) << 8) - | static_cast(flags[2]); - } - - // Assumes there is enough data in the buffer (i.e. the caller should have already checked that the full header is - // present) - constexpr void fill_full_header(std::span data) noexcept - { - version = static_cast(data[0]); - flags[0] = static_cast(data[1]); - flags[1] = static_cast(data[2]); - flags[2] = static_cast(data[3]); - } -}; - -struct infe_header { - std::uint32_t item_id = 0; - std::uint16_t item_protection_index = 0; - mbmff::fourcc_string item_type{}; - std::string_view item_name{}; - std::string_view content_type{}; - std::string_view content_encoding{}; - std::string_view item_uri_type{}; -}; - -struct ftyp_header { - mbmff::fourcc_string major_brand{}; - std::uint32_t minor_version = 0; - std::span compatible_brands{}; -}; - -struct hdlr_header { - mbmff::fourcc_string handler_type{}; - std::string_view name{}; -}; - -struct iloc_extent { - std::uint64_t index = 0; - std::uint64_t offset = 0; - std::uint64_t length = 0; -}; - -struct iloc_item { - std::uint64_t base_offset = 0; - std::uint32_t item_id = 0; - - // Redundant fields for easier parsing - std::uint8_t iloc_version = 0; - std::uint8_t offset_size = 0; - std::uint8_t length_size = 0; - std::uint8_t index_size = 0; - - std::uint8_t construction_method = 0; - - std::uint16_t data_reference_index = 0; - std::uint32_t extent_count = 0; - - std::span extent_data{}; - -public: - constexpr auto get_extent(std::size_t index) const noexcept -> iloc_extent; - constexpr auto size() const noexcept -> std::size_t - { - return extent_count; - } - constexpr auto operator[](std::size_t index) const noexcept -> iloc_extent - { - return get_extent(index); - } - constexpr auto extent_size() const noexcept -> std::size_t - { - return std::size_t(offset_size) + length_size + index_size; - } -}; - -struct iloc_header { - std::uint8_t offset_size = 0; - std::uint8_t length_size = 0; - std::uint8_t base_offset_size = 0; - std::uint8_t index_size = 0; - std::uint32_t item_count = 0; - std::span item_data{}; -}; - -struct ispe_header { - std::uint32_t image_width = 0; - std::uint32_t image_height = 0; -}; - -struct ipma_association { - std::uint16_t essential : 1 = false; - std::uint16_t property_index : 15 = 0; -}; - -struct ipma_entry { - std::uint32_t item_id = 0; - std::uint8_t entry_count = 0; - std::uint8_t entry_size = 0; // size in bytes of each association entry (1 or 2) - std::span association_values{}; - -public: - constexpr auto operator[](std::size_t index) const noexcept -> ipma_association; - constexpr auto size() const noexcept -> std::size_t; -}; - -//------------------------------------------------------------------------------------------------------------ - -using any_box_view = struct box_view_base; - -struct box_view_base { - mbmff::box_header box_header{}; - std::span payload{}; - -public: - constexpr operator bool() const noexcept - { - return box_header.size != 0; - } - constexpr static auto parse(any_box_view box) noexcept -> std::expected - { - return box; - } - constexpr auto version() const noexcept -> std::uint8_t - { - return box_header.version; - } -}; - -template -struct basic_box_view : public box_view_base { - constexpr static box_properties properties = box_properties::none; -}; - -//------------------------------------------------------------------------------------------------------------ -// Box-specific views -//------------------------------------------------------------------------------------------------------------ -template <> -struct basic_box_view : public box_view_base { - constexpr static box_properties properties = box_properties::none; - constexpr static auto parse(any_box_view box) noexcept -> std::expected; - -public: - constexpr auto header() const noexcept -> ftyp_header; - constexpr auto major_brand() const noexcept -> fourcc_string; - constexpr auto minor_version() const noexcept -> std::uint32_t; - constexpr auto compatible_brands() const noexcept -> std::span; -}; - -template <> -struct basic_box_view : public box_view_base { - // full_box is not ticked here because `meta` can - // be a regular box in some formats (e.g. QTFF) - constexpr static box_properties properties = box_properties::container; - constexpr static auto parse(any_box_view box) noexcept -> std::expected; -}; - -template <> -struct basic_box_view : public box_view_base { - constexpr auto data_size() const noexcept -> std::size_t; - constexpr static box_properties properties = box_properties::none; -}; - -template <> -struct basic_box_view : public box_view_base { - constexpr static box_properties properties = box_properties::container | box_properties::full_box; - constexpr static auto parse(any_box_view box) noexcept -> std::expected; - constexpr auto entry_count() const noexcept -> std::uint32_t; -}; - -template <> -struct basic_box_view : public box_view_base { - constexpr static box_properties properties = box_properties::full_box; - constexpr auto header() const noexcept -> infe_header; - constexpr auto item_id() const noexcept -> std::uint32_t; - constexpr auto item_protection_index() const noexcept -> std::uint16_t; -}; - -template <> -struct basic_box_view : public box_view_base { - constexpr static box_properties properties = box_properties::full_box; - constexpr auto header() const noexcept -> hdlr_header; -}; - -template <> -struct basic_box_view : public box_view_base { - constexpr static box_properties properties = box_properties::full_box; - constexpr auto item_id() const noexcept -> uint32_t; -}; - -template <> -struct basic_box_view : public box_view_base { - constexpr static box_properties properties = box_properties::full_box; - constexpr auto header() const noexcept -> iloc_header; -}; - -template <> -struct basic_box_view : public box_view_base { - constexpr static box_properties properties = box_properties::container; -}; - -template <> -struct basic_box_view : public box_view_base { - constexpr static box_properties properties = box_properties::container; -}; - -template <> -struct basic_box_view : public box_view_base { - constexpr static box_properties properties = box_properties::full_box; - constexpr auto header() const noexcept -> ispe_header; -}; - -template <> -struct basic_box_view : public box_view_base { - constexpr static box_properties properties = box_properties::none; - constexpr auto header() const noexcept -> av1C_header; -}; - -template <> -struct basic_box_view : public box_view_base { - constexpr static box_properties properties = box_properties::full_box; - constexpr auto bits_per_channel() const noexcept -> std::span; -}; - -template <> -struct basic_box_view : public box_view_base { - constexpr static box_properties properties = box_properties::full_box; -}; - -template <> -struct basic_box_view : public box_view_base { - constexpr static box_properties properties = box_properties::none; - constexpr auto aspect_ratio() const noexcept -> std::pair; -}; - -//------------------------------------------------------------------------------------------------------------ -#define MBMFF_ITERATE_USING(box) using box##_box = basic_box_view; +#define MBMFF_ITERATE_USING(name) using name##_box = basic_box_view; MBMFF_ITERATE_BOX_TYPES(MBMFF_ITERATE_USING) #undef MBMFF_ITERATE_USING -//------------------------------------------------------------------------------------------------------------ -// Implementation -//------------------------------------------------------------------------------------------------------------ -template -constexpr auto read_be(std::span data) noexcept -> T -{ - T value{}; - if (std::is_constant_evaluated()) { - for (std::size_t i = 0; i < sizeof(T); ++i) { - value <<= 8; - value |= static_cast(data[i]); - } - } else { - std::memcpy(&value, data.data(), sizeof(value)); // strict aliasing - } - return std::byteswap(value); -} - -struct parsed_cstr { - std::string_view value{}; - std::size_t next = 0; -}; - -constexpr auto read_cstr(std::span data, std::size_t offset) noexcept -> parsed_cstr -{ - if (offset >= data.size()) { - return {}; - } - - const auto* begin = reinterpret_cast(data.data() + offset); - std::size_t length = 0; - for (std::size_t i = offset; i < data.size(); ++i) { - if (data[i] == std::byte{0}) { - break; - } - ++length; - } - - std::size_t next = offset + length; - if (next < data.size() && data[next] == std::byte{0}) { - ++next; - } - - return {std::string_view(begin, length), next}; -} - -//------------------------------------------------------------------------------------------------------------ -constexpr inline auto parse_box_header(std::span data) noexcept - -> std::expected, unexpected> -{ - if (data.size() < 8) { - return std::unexpected(unexpected{error_code::need_more_data, 8}); - } - - std::uint64_t size = read_be(data); - if (size == 0) { - return std::unexpected(unexpected{error_code::invalid_format, 8}); - } - - // Read the header type (4 bytes) - box_type type = box_type(fourcc(reinterpret_cast(data.data() + 4))); - if (size == 1) { - if (data.size() < 16) { - return std::unexpected(unexpected{error_code::need_more_data, 16}); - } - size = read_be(data.subspan(8)); - } - - return parsed{box_header{size, type}, size == 1 ? 16u : 8u}; -} - -inline constexpr auto get_box_properties(box_type type) noexcept -> box_properties +inline constexpr auto get_box_properties(mbmff::box_type type) noexcept -> mbmff::box_properties { -#define MBMFF_CASE_ITERATE(box) \ - case box_type::box: \ - return basic_box_view::properties; - +#define MBMFF_CASE_PROP(name) \ + case mbmff::box_type::name: return mbmff::basic_box_view::properties; switch (type) { - MBMFF_ITERATE_BOX_TYPES(MBMFF_CASE_ITERATE) + MBMFF_ITERATE_BOX_TYPES(MBMFF_CASE_PROP) default: - return box_properties::none; + return mbmff::box_properties::none; } -#undef MBMFF_CASE_ITERATE +#undef MBMFF_CASE_PROP } -inline constexpr auto parse(std::span data) noexcept -> std::expected +inline constexpr auto parse(std::span data) noexcept -> mbmff::result { - auto header_result = parse_box_header(data); + auto header_result = mbmff::parse_box_header(data); if (!header_result) { - return std::unexpected(header_result.error()); + return mbmff::make_result(header_result.code, header_result.needed); } - auto header = header_result->value; - if (header.size != 0 && data.size() < header.size) { - return std::unexpected(unexpected{error_code::need_more_data, static_cast(header.size)}); - } - if (header.size != 0 && header.size < header_result->consumed) { - return std::unexpected(unexpected{error_code::invalid_format, header_result->consumed}); - } - - std::size_t payload_size = (header.size == 0) ? (data.size() - header_result->consumed) - : (static_cast(header.size) - header_result->consumed); - auto payload_span = data.subspan(header_result->consumed, payload_size); - - box_properties properties = get_box_properties(header.type); - if ((properties & box_properties::full_box) != box_properties::none) { - if (payload_span.size() < 4) { - return std::unexpected(unexpected{error_code::need_more_data, 4}); - } - - header.fill_full_header(payload_span); - payload_span = payload_span.subspan(4); - } - -#define MBMFF_CASE_PARSE(box) \ - case box_type::box: \ - return basic_box_view::parse(any_box_view{header, payload_span}); - - switch (header.type) { - MBMFF_ITERATE_BOX_TYPES(MBMFF_CASE_PARSE) - default: - return basic_box_view::parse(any_box_view{header, payload_span}); - } -#undef MBMFF_CASE_PARSE -} - -//------------------------------------------------------------------------------------------------------------ -// FTYP -inline constexpr auto basic_box_view::parse(any_box_view box) noexcept - -> std::expected -{ - if (box.payload.size() < 8) { - return std::unexpected(unexpected{error_code::need_more_data, 8}); - } - - if (box.payload.size() % 4 != 0) { - return std::unexpected(unexpected{error_code::invalid_format}); - } - - return box; -} -constexpr auto basic_box_view::header() const noexcept -> ftyp_header -{ - return ftyp_header{major_brand(), minor_version(), compatible_brands()}; -} -constexpr auto basic_box_view::major_brand() const noexcept -> fourcc_string -{ - return fourcc_string::from_data(payload); -} - -constexpr auto basic_box_view::minor_version() const noexcept -> std::uint32_t -{ - return read_be(payload.subspan(4)); -} - -constexpr auto basic_box_view::compatible_brands() const noexcept -> std::span -{ - const auto compatible_data = payload.subspan(8); - - return std::span( - reinterpret_cast(compatible_data.data()), - compatible_data.size() / 4 - ); -} - -//------------------------------------------------------------------------------------------------------------ -// META -inline constexpr auto basic_box_view::parse(any_box_view box) noexcept - -> std::expected -{ - // In a QT File, `meta` is a regular box. Its payload starts directly with a child box (e.g. `[size][hdlr]`). - // In ISOBMFF, `meta` is a Full Box. Its payload starts with 4 bytes (version+flags), then the child box - // `[size][hdlr]`. - constexpr auto hdlr_type = "hdlr"; - bool full_box = false; - - if (std::is_constant_evaluated()) { - for (int i = 0; i < 4; ++i) { - if (box.payload.size() < 4 || box.payload[i] != std::byte(hdlr_type[i])) { - full_box = true; - break; - } - } - } else { - full_box = std::memcmp(box.payload.data() + 4, hdlr_type, 4) != 0; - } - - if (full_box) { - box.box_header.fill_full_header(box.payload); - box.payload = box.payload.subspan(4); - } - return box; -} - -//------------------------------------------------------------------------------------------------------------ -// MDAT -constexpr auto basic_box_view::data_size() const noexcept -> std::size_t -{ - return payload.size(); -} + auto header_size = header_result->consumed; -//------------------------------------------------------------------------------------------------------------ -// IINF -inline constexpr auto basic_box_view::parse(any_box_view box) noexcept - -> std::expected -{ - if (box.version() == 0) { - box.payload = box.payload.subspan(2); // skip 2 bytes of reserved data - } else { - box.payload = box.payload.subspan(4); // skip 4 bytes of reserved data - } - return box; -} - -inline constexpr auto basic_box_view::entry_count() const noexcept -> std::uint32_t -{ - std::size_t reserved_size = (version() == 0) ? 2 : 4; - const std::byte* reserved_end = payload.data() - reserved_size; - std::span reserved_span(reserved_end, reserved_size); - - if (version() == 0) { - return read_be(reserved_span); - } - return read_be(reserved_span); -} - -//------------------------------------------------------------------------------------------------------------ -// INFE -inline constexpr auto basic_box_view::item_id() const noexcept -> std::uint32_t -{ - auto xversion = version(); - if (xversion == 0 || xversion == 1 || xversion == 2) { - return read_be(payload); - } else { - return read_be(payload); - } -} - -inline constexpr auto basic_box_view::item_protection_index() const noexcept -> std::uint16_t -{ - auto xversion = version(); - std::size_t offset = (xversion == 0 || xversion == 1 || xversion == 2) ? 2 : 4; - return read_be(payload.subspan(offset)); -} - -inline constexpr auto basic_box_view::header() const noexcept -> infe_header -{ - infe_header result{}; - auto xversion = version(); - std::size_t offset = 0; - - if (xversion == 0 || xversion == 1 || xversion == 2) { - result.item_id = read_be(payload); - offset = 2; - } else { - result.item_id = read_be(payload); - offset = 4; - } - - result.item_protection_index = read_be(payload.subspan(offset)); - offset += 2; - - if (xversion >= 2) { - result.item_type = fourcc_string::from_data(payload.subspan(offset)); - offset += 4; - } - - auto name = read_cstr(payload, offset); - result.item_name = name.value; - offset = name.next; - - if (xversion >= 2) { - if (result.item_type.view() == "mime") { - auto type = read_cstr(payload, offset); - result.content_type = type.value; - offset = type.next; - - auto encoding = read_cstr(payload, offset); - result.content_encoding = encoding.value; - offset = encoding.next; - } else if (result.item_type.view() == "uri ") { - auto uri = read_cstr(payload, offset); - result.item_uri_type = uri.value; - offset = uri.next; - } - } - - return result; -} - -//------------------------------------------------------------------------------------------------------------ -// HDLR -constexpr auto basic_box_view::header() const noexcept -> hdlr_header -{ - hdlr_header result{}; - // Skip archaic pre-ISOBMFF reserved fields (4 bytes) "mhlr" or "alis" - uint32_t reserved = read_be(payload); - - // Handler type is at offset 4 - result.handler_type = fourcc_string::from_data(payload.subspan(4)); - - // skip another 3x4 bytes of reserved data - auto subspan = payload.subspan(16); - if (!subspan.empty()) { - auto name = read_cstr(subspan, 0); - result.name = name.value; - } - return result; -} - -//------------------------------------------------------------------------------------------------------------ -// PITM -constexpr auto basic_box_view::item_id() const noexcept -> uint32_t -{ - if (version() == 0) { - return read_be(payload); - } - return read_be(payload); -} - -//------------------------------------------------------------------------------------------------------------ -// ILOC -constexpr auto basic_box_view::header() const noexcept -> iloc_header -{ - iloc_header result{}; - uint8_t first_byte = static_cast(payload[0]); - result.offset_size = (first_byte >> 4) & 0x0F; - result.length_size = first_byte & 0x0F; - uint8_t second_byte = static_cast(payload[1]); - result.base_offset_size = (second_byte >> 4) & 0x0F; - result.index_size = second_byte & 0x0F; - - auto xpayload = this->payload.subspan(2); - if (version() < 2) { - result.item_count = read_be(xpayload); - result.item_data = xpayload.subspan(2); - } else { - result.item_count = read_be(xpayload); - result.item_data = xpayload.subspan(4); - } - return result; -} - -constexpr auto mbmff::iloc_item::get_extent(std::size_t index) const noexcept -> iloc_extent -{ - std::span entry_span = extent_data.subspan(index * extent_size(), extent_size()); - iloc_extent extent{}; - - std::uint32_t offset = 0; - if ((iloc_version == 1 || iloc_version == 2) && index_size > 0) { - if (index_size == 2) { - extent.index = read_be(entry_span); - } else if (index_size == 4) { - extent.index = read_be(entry_span); - } else if (index_size == 8) { - extent.index = read_be(entry_span); - } - offset += index_size; - } - - if (offset_size == 2) { - extent.offset = read_be(entry_span.subspan(offset)); - } else if (offset_size == 4) { - extent.offset = read_be(entry_span.subspan(offset)); - } else if (offset_size == 8) { - extent.offset = read_be(entry_span.subspan(offset)); + if (header.size() != 0 && data.size() < header.size()) { + return mbmff::make_result( + mbmff::error_code::need_more_data, + static_cast(header.size()) + ); } - offset += offset_size; - - if (length_size == 2) { - extent.length = read_be(entry_span.subspan(offset)); - } else if (length_size == 4) { - extent.length = read_be(entry_span.subspan(offset)); - } else if (length_size == 8) { - extent.length = read_be(entry_span.subspan(offset)); - } - return extent; -} - -//------------------------------------------------------------------------------------------------------------ -// ISPE -constexpr auto basic_box_view::header() const noexcept -> ispe_header -{ - ispe_header result{}; - result.image_width = read_be(payload); - result.image_height = read_be(payload.subspan(4)); - return result; -} - -//------------------------------------------------------------------------------------------------------------ -// AV1C -constexpr auto basic_box_view::header() const noexcept -> av1C_header -{ - av1C_header result{}; - if (payload.size() < 3) { - return result; + if (header.size() != 0 && header.size() < header_size) { + return mbmff::make_result(mbmff::error_code::invalid_format); } - // Byte 0 - std::uint8_t read_byte = static_cast(payload[0]); - result.marker = (read_byte >> 7) & 0x01; - result.version = read_byte & 0x7F; + auto payload_size = (header.size() == 0) ? (data.size() - header_size) + : (static_cast(header.size()) - header_size); + auto payload_span = data.subspan(header_size, payload_size); - // Byte 1 - read_byte = static_cast(payload[1]); - result.seq_profile = (read_byte >> 5) & 0x07; - result.seq_level_idx_0 = read_byte & 0x1F; + mbmff::any_box_view box{{header.size_, header.type_}, payload_span}; - // Byte 2 - read_byte = static_cast(payload[2]); - result.seq_tier_0 = (read_byte >> 7) & 0x01; - result.high_bitdepth = (read_byte >> 6) & 0x01; - result.twelve_bit = (read_byte >> 5) & 0x01; - result.monochrome = (read_byte >> 4) & 0x01; - result.chroma_subsampling_x = (read_byte >> 3) & 0x01; - result.chroma_subsampling_y = (read_byte >> 2) & 0x01; - result.chroma_sample_position = read_byte & 0x03; +#define MBMFF_CASE_VALIDATE(name) \ + case mbmff::box_type::name: \ + return mbmff::basic_box_view::validate(box); - // Byte 3 - read_byte = static_cast(payload[3]); - result.reserved = (read_byte >> 5) & 0x07; - result.initial_presentation_delay_present = (read_byte >> 4) & 0x01; - if (result.initial_presentation_delay_present) { - result.initial_presentation_delay_minus_one = read_byte & 0x0F; + switch (header.type_) { + MBMFF_ITERATE_BOX_TYPES(MBMFF_CASE_VALIDATE) + default: + return {box}; } - - result.config_obus = payload.subspan(4); - return result; -} - -//------------------------------------------------------------------------------------------------------------ -// PIXI -constexpr auto basic_box_view::bits_per_channel() const noexcept -> std::span -{ - std::uint8_t size = static_cast(payload[0]); - return std::span( - static_cast(static_cast(payload.data() + 1)), - size - ); +#undef MBMFF_CASE_VALIDATE } -//------------------------------------------------------------------------------------------------------------ -// IPMA -constexpr auto ipma_entry::operator[](std::size_t index) const noexcept -> ipma_association +template +constexpr auto box_cast(const mbmff::any_box_view& box) noexcept -> mbmff::basic_box_view { - if (index >= entry_count) { + if (box.type() != Box) { return {}; } - ipma_association association; - if (entry_size == 1) { - std::uint8_t value = static_cast(association_values[index]); - association.essential = (value >> 7) & 0x01; - association.property_index = value & 0x7F; - } else { - std::uint16_t value = read_be(association_values.subspan(index * 2, 2)); - association.essential = (value >> 15) & 0x01; - association.property_index = value & 0x7FFF; - } - return association; -} -constexpr auto ipma_entry::size() const noexcept -> std::size_t -{ - return entry_count; -} - -//------------------------------------------------------------------------------------------------------------ -// PASP -inline constexpr auto basic_box_view::aspect_ratio() const noexcept -> std::pair -{ - return {read_be(payload), read_be(payload.subspan(4))}; + return static_cast>(box); } -//------------------------------------------------------------------------------------------------------------ -template -constexpr auto box_cast(const any_box_view& box) noexcept -> basic_box_view -{ - if (box.box_header.type != Box) { - return {}; - } - return static_cast>(box); -} - -//------------------------------------------------------------------------------------------------------------ -enum class iterator_flags : uint32_t { +enum class iterator_flags : std::uint32_t { none = 0, recursive = 1 << 0, }; -constexpr auto operator|(iterator_flags a, iterator_flags b) noexcept -> iterator_flags -{ - return static_cast(std::to_underlying(a) | std::to_underlying(b)); -} -constexpr auto operator&(iterator_flags a, iterator_flags b) noexcept -> iterator_flags -{ - return static_cast(std::to_underlying(a) & std::to_underlying(b)); -} -constexpr auto operator+(iterator_flags a) noexcept -> std::underlying_type_t -{ - return std::to_underlying(a); -} -constexpr auto has(iterator_flags a, iterator_flags check) noexcept -> bool -{ - return (a & check) != iterator_flags::none; -} +MBMFF_FLAG_OPERATORS(iterator_flags) struct box_iterator { using iterator_category = std::forward_iterator_tag; - using value_type = std::expected; + using value_type = mbmff::result; using difference_type = std::ptrdiff_t; private: std::span remaining_; - iterator_flags flags_ = iterator_flags::none; + mbmff::iterator_flags flags_ = mbmff::iterator_flags::none; public: constexpr box_iterator() noexcept = default; constexpr explicit box_iterator( std::span data, - iterator_flags flags = iterator_flags::none + mbmff::iterator_flags flags = mbmff::iterator_flags::none ) noexcept : remaining_(data) , flags_(flags) {} public: - constexpr auto begin() const noexcept -> box_iterator + constexpr auto begin() const noexcept -> mbmff::box_iterator { return *this; } - constexpr auto end() const noexcept -> box_iterator + constexpr auto end() const noexcept -> mbmff::box_iterator { return {}; } - constexpr auto try_get() const noexcept -> std::expected + constexpr auto try_get() const noexcept -> mbmff::result { - return parse(remaining_); + return mbmff::parse(remaining_); } - constexpr auto operator*() const noexcept -> std::expected + constexpr auto operator*() const noexcept -> mbmff::result { return try_get(); } - constexpr auto operator++() noexcept -> box_iterator& + constexpr auto operator++() noexcept -> mbmff::box_iterator& { if (remaining_.empty()) { remaining_ = {}; return *this; } - auto result = parse(remaining_); - if (!result) { + auto op_result = mbmff::parse(remaining_); + if (!op_result) { remaining_ = {}; return *this; } - // If recursive flag is set and the box is a container, iterate into it instead of moving to the next sibling - if (has(flags_, iterator_flags::recursive)) { - auto properties = get_box_properties(result->box_header.type); - - if (has(properties, box_properties::container)) { + if (mbmff::has(flags_, mbmff::iterator_flags::recursive)) { + auto properties = mbmff::get_box_properties(op_result->type_); + if (mbmff::has(properties, mbmff::box_properties::container)) { auto* current_end = &remaining_.back() + 1; - auto* payload_start = &result->payload[0]; - + auto* payload_start = op_result->payload.data(); remaining_ = std::span(payload_start, current_end); return *this; } } - std::size_t box_size = (result->box_header.size == 0) ? remaining_.size() - : static_cast(result->box_header.size); + auto box_size = (op_result->size_ == 0) ? remaining_.size() : static_cast(op_result->size_); if (box_size == 0 || box_size > remaining_.size()) { remaining_ = {}; } else { @@ -932,234 +151,14 @@ struct box_iterator { } return *this; } - constexpr auto operator++(int) noexcept -> box_iterator - { - auto tmp = *this; - ++(*this); - return tmp; - } - constexpr bool operator==(const box_iterator& other) const noexcept - { - // special case: both iterators are at the end (empty) - if (remaining_.empty() && other.remaining_.empty()) { - return true; - } - return remaining_.data() == other.remaining_.data() && remaining_.size() == other.remaining_.size(); - } -}; - -struct ipma_entry_iterator { - using iterator_category = std::forward_iterator_tag; - using value_type = mbmff::ipma_entry; - using difference_type = std::ptrdiff_t; - -private: - std::uint32_t entry_count_ = 0; - std::uint8_t index_size_ = 0; - std::uint8_t asoc_size_ = 0; - std::span remaining_; - -public: - constexpr ipma_entry_iterator() noexcept = default; - constexpr explicit ipma_entry_iterator(const basic_box_view& box) noexcept - : entry_count_(read_be(box.payload)) - , index_size_(box.version() == 0 ? 2 : 4) - , asoc_size_(box.box_header.flags_value() & 0x01 ? 2 : 1) - , remaining_(box.payload.subspan(4)) - {} - -public: - constexpr auto begin() const noexcept -> ipma_entry_iterator - { - return *this; - } - constexpr auto end() const noexcept -> ipma_entry_iterator - { - return {}; - } - constexpr auto get() const noexcept -> ipma_entry - { - ipma_entry entry{}; - if (entry_count_ == 0 || remaining_.empty()) { - return entry; - } - if (index_size_ == 2) { - entry.item_id = read_be(remaining_); - entry.association_values = remaining_.subspan(2); - } else { - entry.item_id = read_be(remaining_); - entry.association_values = remaining_.subspan(4); - } - - entry.entry_count = static_cast(entry.association_values[0]); - entry.association_values = entry.association_values.subspan(1); - entry.entry_size = asoc_size_; - return entry; - } - constexpr auto operator*() const noexcept -> ipma_entry - { - return get(); - } - constexpr auto operator++() noexcept -> ipma_entry_iterator& - { - if (entry_count_ == 0 || remaining_.empty()) { - entry_count_ = 0; - remaining_ = {}; - return *this; - } - entry_count_--; - auto current_entry = get(); - - // Get the size of the current entry - std::size_t offset = static_cast(current_entry.entry_count) * current_entry.entry_size - + index_size_ + 1; - - remaining_ = (offset >= remaining_.size()) ? std::span{} : remaining_.subspan(offset); - return *this; - } - constexpr auto operator++(int) noexcept -> ipma_entry_iterator - { - auto tmp = *this; - ++(*this); - return tmp; - } - constexpr bool operator==(const ipma_entry_iterator& other) const noexcept - { - // special case: both iterators are at the end (empty) - if (entry_count_ == 0 && other.entry_count_ == 0) { - return true; - } - return entry_count_ == other.entry_count_ && remaining_.data() == other.remaining_.data() - && remaining_.size() == other.remaining_.size(); - } -}; - -struct iloc_item_iterator { - using iterator_category = std::forward_iterator_tag; - using value_type = mbmff::iloc_item; - using difference_type = std::ptrdiff_t; - -private: - std::span remaining_; - std::uint8_t iloc_version_ = 0; - std::uint8_t offset_size_ = 0; - std::uint8_t length_size_ = 0; - std::uint8_t index_size_ = 0; - std::uint8_t base_offset_size_ = 0; - -public: - constexpr iloc_item_iterator() noexcept = default; - constexpr iloc_item_iterator(const iloc_header& header, std::uint8_t version) noexcept - : remaining_(header.item_data) - , iloc_version_(version) - , offset_size_(header.offset_size) - , length_size_(header.length_size) - , index_size_(header.index_size) - , base_offset_size_(header.base_offset_size) - {} - constexpr explicit iloc_item_iterator(const basic_box_view& box) noexcept - : iloc_item_iterator(box.header(), box.version()) - {} - -public: - constexpr auto extent_size() const noexcept -> std::size_t - { - return std::size_t(offset_size_) + length_size_ + (index_size_ > 0 ? index_size_ : 0); - } - constexpr auto begin() const noexcept -> iloc_item_iterator - { - return *this; - } - constexpr auto end() const noexcept -> iloc_item_iterator - { - return {}; - } - constexpr auto get() const noexcept -> iloc_item - { - iloc_item item{}; - if (remaining_.empty()) { - return item; - } - - std::size_t offset = 0; - - // Read item_id - if (iloc_version_ < 2) { - item.item_id = read_be(remaining_); - offset += 2; - } else { - item.item_id = read_be(remaining_); - offset += 4; - } - - // Read construction_method - if (iloc_version_ == 1 || iloc_version_ == 2) { - std::uint16_t value = read_be(remaining_.subspan(offset)); - item.construction_method = static_cast(value & 0x000F); // lower 4 bits - offset += 2; - } - - // Read data_reference_index - item.data_reference_index = read_be(remaining_.subspan(offset)); - offset += 2; - - switch (base_offset_size_) { - default: - case 0: - item.base_offset = 0; - break; - case 2: - item.base_offset = read_be(remaining_.subspan(offset)); - break; - case 4: - item.base_offset = read_be(remaining_.subspan(offset)); - break; - case 8: - item.base_offset = read_be(remaining_.subspan(offset)); - break; - } - offset += base_offset_size_; - - item.extent_count = read_be(remaining_.subspan(offset)); - item.extent_data = remaining_.subspan(offset + 2); - - // fill redundant fields for easier access - item.iloc_version = iloc_version_; - item.offset_size = offset_size_; - item.length_size = length_size_; - item.index_size = index_size_; - return item; - } - - constexpr auto operator*() const noexcept -> iloc_item - { - return get(); - } - - constexpr auto operator++() noexcept -> iloc_item_iterator& - { - if (remaining_.empty()) { - remaining_ = {}; - return *this; - } - auto current_item = get(); - const std::byte* data = current_item.extent_data.data(); - std::size_t total_extent_size = current_item.extent_count * extent_size(); - - remaining_ = {data + total_extent_size, remaining_.size() - (data - remaining_.data()) - total_extent_size}; - - return *this; - } - - constexpr auto operator++(int) noexcept -> iloc_item_iterator + constexpr auto operator++(int) noexcept -> mbmff::box_iterator { auto tmp = *this; ++(*this); return tmp; } - constexpr bool operator==(const iloc_item_iterator& other) const noexcept + constexpr bool operator==(const mbmff::box_iterator& other) const noexcept { - // special case: both iterators are at the end (empty) if (remaining_.empty() && other.remaining_.empty()) { return true; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 90d317f..03c9e64 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -7,10 +7,3 @@ set_target_properties(mbmff-test PROPERTIES CXX_EXTENSIONS OFF ) target_link_libraries(mbmff-test PRIVATE mbmff) - -# Copy file to output directory -add_custom_command(TARGET mbmff-test POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory - "${CMAKE_CURRENT_SOURCE_DIR}/assets" - "$/assets" -) \ No newline at end of file diff --git a/test/entry_main.cpp b/test/entry_main.cpp index 6a95d84..54b1bb4 100644 --- a/test/entry_main.cpp +++ b/test/entry_main.cpp @@ -1,129 +1,208 @@ -#include +#include #include #include #include #include "formatters.hpp" - - -int main() +static constexpr auto is_box_implemented(mbmff::box_type type) noexcept -> bool { - std::ifstream avif_file("assets/avif_sample_8_420.avif", std::ios::binary); - if (!avif_file) { - std::cerr << "Failed to open the AVIF file.\n"; - return 1; // Failed to open the file + switch (type) { + case mbmff::box_type::ftyp: + case mbmff::box_type::meta: + case mbmff::box_type::mdat: + case mbmff::box_type::av1C: + case mbmff::box_type::moov: + case mbmff::box_type::trak: + case mbmff::box_type::mdia: + case mbmff::box_type::minf: + case mbmff::box_type::stbl: + case mbmff::box_type::dinf: + case mbmff::box_type::edts: + case mbmff::box_type::udta: + case mbmff::box_type::mvex: + case mbmff::box_type::moof: + case mbmff::box_type::traf: + case mbmff::box_type::mfra: + case mbmff::box_type::ipco: + case mbmff::box_type::iprp: + case mbmff::box_type::iinf: + case mbmff::box_type::iref: + case mbmff::box_type::iloc: + case mbmff::box_type::hdlr: + case mbmff::box_type::pitm: + case mbmff::box_type::ispe: + case mbmff::box_type::pixi: + case mbmff::box_type::pasp: + case mbmff::box_type::ipma: + case mbmff::box_type::infe: + return true; + default: + return false; } +} - std::vector file_data((std::istreambuf_iterator(avif_file)), std::istreambuf_iterator()); - std::span file_data_span(reinterpret_cast(file_data.data()), file_data.size()); - - uint32_t primary_item_id = 0; - - for (const auto& box_expected : mbmff::box_iterator(file_data_span, mbmff::iterator_flags::recursive)) { - if (!box_expected) { +static auto display_fourcc(const mbmff::fourcc_string& fc) -> std::string +{ + std::string out; + out.reserve(12); + bool all_printable = true; + for (std::size_t i = 0; i < 4; ++i) { + auto c = fc[i]; + if (static_cast(c) < 0x20 || static_cast(c) > 0x7e) { + all_printable = false; break; } - const auto& box = box_expected.value(); + } + if (all_printable) { + out.append(fc.view()); + } else { + out += '['; + for (std::size_t i = 0; i < 4; ++i) { + if (i) out += ' '; + auto uc = static_cast(fc[i]); + auto hex = "0123456789abcdef"; + out += "0x"; + out += hex[uc >> 4]; + out += hex[uc & 0xf]; + } + out += ']'; + } + return out; +} - // Get concrete types - switch (box.box_header.type) { - case mbmff::box_type::ftyp: { - auto ftyp = mbmff::box_cast(box); - std::cout << std::format("{}\n", ftyp); - } break; - case mbmff::box_type::infe: { - auto infe = mbmff::box_cast(box); - std::cout << std::format("{}\n", infe); - } break; - case mbmff::box_type::meta: { - auto meta = mbmff::box_cast(box); - std::cout << std::format("{}\n", meta); - } break; - case mbmff::box_type::hdlr: { - auto hdlr = mbmff::box_cast(box); - std::cout << std::format("{}\n", hdlr); - } break; - case mbmff::box_type::pitm: { - auto pitm = mbmff::box_cast(box); - std::cout << std::format("{}\n", pitm); - primary_item_id = pitm.item_id(); - } break; - case mbmff::box_type::iinf: { - auto iinf = mbmff::box_cast(box); - std::cout << std::format("{}\n", iinf); - } break; - case mbmff::box_type::iloc: { - auto iloc = mbmff::box_cast(box); - std::cout << std::format("{}\n", iloc); +static void print_box(const mbmff::any_box_view& box, std::size_t depth) +{ + for (std::size_t i = 0; i < depth; ++i) std::cout << " "; - for (const auto& item : mbmff::iloc_item_iterator(iloc)) { - std::cout << std::format(" {}\n", item); - } - } break; - case mbmff::box_type::iprp: { - auto iprp = mbmff::box_cast(box); - std::cout << std::format("{}\n", iprp); - } break; - case mbmff::box_type::ipco: { - auto ipco = mbmff::box_cast(box); - std::cout << std::format("{}\n", ipco); - } break; - case mbmff::box_type::mdat: { - auto mdat = mbmff::box_cast(box); - std::cout << std::format("{}\n", mdat); - } break; - case mbmff::box_type::ispe: { - auto ispe = mbmff::box_cast(box); - std::cout << std::format("{}\n", ispe); - } break; - case mbmff::box_type::av1C: { - auto av1C = mbmff::box_cast(box); - std::cout << std::format("{}\n", av1C); + if (!is_box_implemented(box.type_)) { + std::cout << "\x1b[33m[!]\x1b[0m "; + } - // scan OBUs in the av1C payload - for (std::uint32_t i = 0; const auto& obu : mbmff::obu_iterator(av1C.header())) { - if (!obu) { - std::cout << std::format( - " OBU parsing error: code={}, needed={}\n", - obu.error().code, - obu.error().needed - ); - break; - } + std::cout << '<' << display_fourcc(box.type_string()); - if (obu->type == mbmff::obu_type::sequence_header) { - auto seq_header_obu = mbmff::obu_cast(*obu); - std::cout << std::format(" Sequence Header OBU: {}\n", seq_header_obu); - break; - } + switch (box.type_) { + case mbmff::box_type::ftyp: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + case mbmff::box_type::meta: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + case mbmff::box_type::mdat: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + case mbmff::box_type::iinf: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + case mbmff::box_type::iloc: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + case mbmff::box_type::hdlr: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + case mbmff::box_type::pitm: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + case mbmff::box_type::ispe: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + case mbmff::box_type::pixi: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + case mbmff::box_type::pasp: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + case mbmff::box_type::ipma: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + case mbmff::box_type::infe: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + case mbmff::box_type::av1C: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + default: + std::cout << " size=\"" << box.size_ << '"'; + break; + } - std::cout << std::format(" {}:{}\n", i++, obu.value()); - } + bool is_container = mbmff::has(mbmff::get_box_properties(box.type_), mbmff::box_properties::container); + std::cout << (is_container ? ">" : " />") << '\n'; - } break; - case mbmff::box_type::pixi: { - auto pixi = mbmff::box_cast(box); - std::cout << std::format("{}\n", pixi); - } break; - case mbmff::box_type::ipma: { - auto ipma = mbmff::box_cast(box); - // ipma doesn't have a custom formatter, because we need to intrude into its payload to parse the actual - // entries, so we print it manually here + if (box.type_ == mbmff::box_type::iloc) { + auto iloc = mbmff::box_cast(box); + auto data = iloc.value(); + auto iter = mbmff::iloc_item_iterator(data, iloc.version()); + for (; iter != mbmff::iloc_item_iterator{}; ++iter) { + auto item = *iter; + for (std::size_t i = 0; i < depth + 1; ++i) std::cout << " "; + std::cout << std::format("\n", + item.item_id, item.base_offset, + item.construction_method, item.data_reference_index, item.size()); + for (std::size_t e = 0; e < item.size(); ++e) { + auto ext = item[e]; + for (std::size_t i = 0; i < depth + 2; ++i) std::cout << " "; + std::cout << std::format("\n"; + } + for (std::size_t i = 0; i < depth + 1; ++i) std::cout << " "; + std::cout << "\n"; + } + } +} - for (auto entry : mbmff::ipma_entry_iterator(ipma)) { - std::cout << std::format("{}\n", entry); +static void walk_boxes_impl(const mbmff::any_box_view& box, std::size_t depth) +{ + print_box(box, depth); + auto props = mbmff::get_box_properties(box.type_); + if (mbmff::has(props, mbmff::box_properties::container) && !box.payload.empty()) { + for (const auto& child : mbmff::box_iterator(box.payload)) { + if (!child) { + std::cerr << "\x1b[31;1m" << std::string(depth * 2, ' ') + << "Parse error: " << mbmff::to_string(child.code) + << " (needed " << child.needed << " bytes)\x1b[0m\n"; + return; } - } break; - case mbmff::box_type::pasp: { - auto pasp = mbmff::box_cast(box); - std::cout << std::format("{}\n", pasp); - } break; - default: - std::cout << "Box type: " << box.box_header.type_string().view() << ", size: " << box.box_header.size - << '\n'; - break; + walk_boxes_impl(*child, depth + 1); + } + } +} + +static void walk_boxes(std::span data) +{ + for (const auto& result : mbmff::box_iterator(data)) { + if (!result) { + std::cerr << "\x1b[31;1mParse error at top level: " + << mbmff::to_string(result.code) + << " (needed " << result.needed << " bytes)\x1b[0m\n"; + return; } + walk_boxes_impl(*result, 0); } +} + +int main(int argc, char* argv[]) +{ + if (argc < 2) { + std::cerr << "Usage: mbmff-test \n"; + return 1; + } + + std::ifstream file(argv[1], std::ios::binary); + if (!file) { + std::cerr << "Failed to open file: " << argv[1] << '\n'; + return 1; + } + + std::vector file_data((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + std::span file_span( + reinterpret_cast(file_data.data()), + file_data.size() + ); + walk_boxes(file_span); return 0; } diff --git a/test/formatters.hpp b/test/formatters.hpp index e94e709..1c06dcb 100644 --- a/test/formatters.hpp +++ b/test/formatters.hpp @@ -1,401 +1,225 @@ #pragma once +#include #include #include -#include - -template<> -struct std::formatter : std::formatter { - auto format(const mbmff::obu_sequence_header_view& view, std::format_context& ctx) const - { - auto header = view.header(); - std::string output = std::format( - "obu_sequence_header: seq_profile={}, still_picture={}, reduced_still_picture_header={}, " - "timing_info_present_flag={}, decoder_model_info_present_flag={}, " - "initial_display_delay_present_flag={}, operating_points_cnt_minus_1={}", - uint8_t(header.seq_profile), - uint8_t(header.still_picture), - uint8_t(header.reduced_still_picture_header), - uint8_t(header.timing_info_present_flag), - uint8_t(header.decoder_model_info_present_flag), - uint8_t(header.initial_display_delay_present_flag), - uint8_t(header.operating_points_cnt_minus_1) - ); - - if (header.operating_points_cnt_minus_1 > 0 || true) { - output += "\n Operating Points:"; - for (std::uint8_t i = 0; i <= header.operating_points_cnt_minus_1; ++i) { - const auto& op = header.operating_points[i]; - output += std::format( - "\n [{}] idc={}, level_idx={}, tier={}, decoder_model_present={}, initial_display_delay_present={}", - i, op.operating_point_idc, static_cast(op.seq_level_idx), - op.seq_tier ? "high" : "main", - static_cast(op.decoder_model_present_for_this_op), - static_cast(op.initial_display_delay_present_for_this_op) - ); - } - } - - output += std::format("\n Color Config:"); - output += std::format("\n high_bitdepth={}, twelve_bit={}, monochrome={}, color_description_present={}", - static_cast(header.color_config_data.high_bitdepth), - static_cast(header.color_config_data.twelve_bit), - static_cast(header.color_config_data.monochrome), - static_cast(header.color_config_data.color_description_present_flag) - ); - - if (header.color_config_data.color_description_present_flag) { - const auto& cd = header.color_config_data.color_description_data; - output += std::format("\n color_primaries={}, transfer_characteristics={}, matrix_coefficients={}", - mbmff::to_string(cd.color_primaries), - mbmff::to_string(cd.transfer_characteristics), - mbmff::to_string(cd.matrix_coefficients) - ); - } - - output += std::format("\n Frame Max Dimensions: {}x{}", - header.max_frame_width_minus_1 + 1, - header.max_frame_height_minus_1 + 1 - ); - - return std::formatter::format(output, ctx); - } -}; //------------------------------------------------------------------------------------------------------------ -template <> -struct std::formatter : std::formatter { - auto format(const mbmff::any_obu_view& box, format_context& ctx) const -> format_context::iterator - { - std::string output = std::format( - "OBU: type={}", - mbmff::to_string(static_cast(box.type)) - ); - - // Add size field if present - if (box.has_size_field) { - output += std::format(", size_field=1"); - } - - // Add extension fields - if (box.extension_flag) { - output += std::format( - ", temporal_id={}, spatial_id={}", - box.temporal_id, - box.spatial_id - ); - } - - output += "}"; - return std::formatter::format(output, ctx); - } -}; - -//------------------------------------------------------------------------------------------------------------ -template <> -struct std::formatter : std::formatter { - auto format(mbmff::error_code code, std::format_context& ctx) const - { - return std::formatter::format(mbmff::get_error_message(code), ctx); - } -}; - -template <> -struct std::formatter : std::formatter { - auto format(const mbmff::box_header& header, std::format_context& ctx) const - { - return std::formatter::format( - std::format("{} ({} bytes)", header.type_string().view(), header.size), - ctx - ); - } -}; - +// ftyp template <> struct std::formatter : std::formatter { auto format(const mbmff::ftyp_box& box, std::format_context& ctx) const { - std::string output = std::format( - "FTYP [{} bytes]: {} minor={} compatible=[", - box.box_header.size, - box.major_brand().view(), - box.minor_version() - ); - - auto compatible_brands = box.compatible_brands(); - - for (std::size_t i = 0; i < compatible_brands.size(); ++i) { - if (i != 0) { - output.append(", "); - } - output.append(compatible_brands[i].view()); + auto v = box.value(); + std::string out; + out += std::format(" size=\"{}\" major=\"{}\"", box.size_, v.major_brand.view()); + out += std::format(" minor=\"{}\" compatible=\"[", v.minor_version); + for (std::size_t i = 0; i < v.compatible_brands.size(); ++i) { + if (i) out += ','; + out += v.compatible_brands[i].view(); } - output.push_back(']'); - return std::formatter::format(output, ctx); + out += "]\""; + return std::formatter::format(out, ctx); } }; +//------------------------------------------------------------------------------------------------------------ +// meta template <> struct std::formatter : std::formatter { auto format(const mbmff::meta_box& box, std::format_context& ctx) const { return std::formatter::format( - std::format( - "META [{} bytes]: version={} flags=0x{:06X}", - box.box_header.size, - box.box_header.version, - box.box_header.flags_value() - ), + std::format(" size=\"{}\"", box.size_), ctx ); } }; +//------------------------------------------------------------------------------------------------------------ +// mdat template <> struct std::formatter : std::formatter { auto format(const mbmff::mdat_box& box, std::format_context& ctx) const { return std::formatter::format( - std::format("MDAT [{} bytes]: payload={} bytes", box.box_header.size, box.data_size()), + std::format(" size=\"{}\" data_size=\"{}\"", box.size_, box.payload.size()), ctx ); } }; +//------------------------------------------------------------------------------------------------------------ +// iinf template <> struct std::formatter : std::formatter { auto format(const mbmff::iinf_box& box, std::format_context& ctx) const { + auto v = box.value(); return std::formatter::format( - std::format( - "IINF [{} bytes]: version={} flags=0x{:06X}, entries={}", - box.box_header.size, - box.box_header.version, - box.box_header.flags_value(), - box.entry_count() - ), + std::format(" size=\"{}\" version=\"{}\" entry_count=\"{}\"", box.size_, box.version(), v.entry_count), ctx ); } }; -template <> -struct std::formatter : std::formatter { - auto format(const mbmff::infe_box& box, std::format_context& ctx) const - { - auto header = box.header(); - auto version = box.version(); - std::string output = std::format( - "INFE [{} bytes]: version={} id={} protection={}", - box.box_header.size, - version, - header.item_id, - header.item_protection_index - ); - - if (version < 2) { - output.append(std::format(" name={}", header.item_name)); - } - - if (version >= 2) { - output.append(std::format(" type={}", header.item_type.view())); - if (header.item_type.view() == "mime") { - output.append(std::format(" content_type={}", header.content_type)); - if (!header.content_encoding.empty()) { - output.append(std::format(" content_encoding={}", header.content_encoding)); - } - } else if (header.item_type.view() == "uri ") { - output.append(std::format(" uri={}", header.item_uri_type)); - } - } - - return std::formatter::format(output, ctx); - } -}; - +//------------------------------------------------------------------------------------------------------------ +// hdlr template <> struct std::formatter : std::formatter { auto format(const mbmff::hdlr_box& box, std::format_context& ctx) const { - auto header = box.header(); - std::string output = std::format( - "HDLR [{} bytes]: handler_type={}", - box.box_header.size, - header.handler_type.view() - ); - if (!header.name.empty()) { - output.append(std::format(" name={}", header.name)); + auto v = box.value(); + std::string out = std::format(" size=\"{}\" handler_type=\"{}\"", box.size_, v.handler_type.view()); + if (!v.name.empty()) { + out += std::format(" name=\"{}\"", v.name); } - return std::formatter::format(output, ctx); + return std::formatter::format(out, ctx); } }; +//------------------------------------------------------------------------------------------------------------ +// pitm template <> struct std::formatter : std::formatter { auto format(const mbmff::pitm_box& box, std::format_context& ctx) const { + auto v = box.value(); return std::formatter::format( - std::format("PITM [{} bytes]: item_id={}", box.box_header.size, box.item_id()), - ctx - ); - } -}; - -template <> -struct std::formatter : std::formatter { - auto format(const mbmff::iloc_box& box, std::format_context& ctx) const - { - auto header = box.header(); - std::string output = std::format( - "ILOC [{} bytes]: version={} offset_size={} length_size={} base_offset_size={} index_size={} item_count={}", - box.box_header.size, - box.version(), - header.offset_size, - header.length_size, - header.base_offset_size, - header.index_size, - header.item_count - ); - return std::formatter::format(output, ctx); - } -}; - -template <> -struct std::formatter : std::formatter { - auto format(const mbmff::iprp_box& box, std::format_context& ctx) const - { - return std::formatter::format( - std::format("IPRP [{} bytes]: Container", box.box_header.size), - ctx - ); - } -}; - -template <> -struct std::formatter : std::formatter { - auto format(const mbmff::ipco_box& box, std::format_context& ctx) const - { - return std::formatter::format( - std::format("IPCO [{} bytes]: Container", box.box_header.size), + std::format(" size=\"{}\" item_id=\"{}\"", box.size_, v.item_id), ctx ); } }; +//------------------------------------------------------------------------------------------------------------ +// ispe template <> struct std::formatter : std::formatter { auto format(const mbmff::ispe_box& box, std::format_context& ctx) const { - auto header = box.header(); + auto v = box.value(); return std::formatter::format( - std::format( - "ISPE [{} bytes]: image_width={} image_height={}", - box.box_header.size, - header.image_width, - header.image_height - ), + std::format(" size=\"{}\" image_width=\"{}\" image_height=\"{}\"", box.size_, v.image_width, v.image_height), ctx ); } }; -template <> -struct std::formatter : std::formatter { - auto format(const mbmff::av1C_box& box, std::format_context& ctx) const - { - auto header = box.header(); - std::string output = std::format( - "AV1C [{} bytes]: profile={} level={} tier={} bitdepth={} monochrome={} chroma_subsampling=({}, {})", - box.box_header.size, - std::uint8_t(header.seq_profile), - std::uint8_t(header.seq_level_idx_0), - header.seq_tier_0 ? "high" : "main", - header.high_bitdepth ? (header.twelve_bit ? 12 : 10) : 8, - header.monochrome ? "yes" : "no", - header.chroma_subsampling_x ? "1" : "0", - header.chroma_subsampling_y ? "1" : "0" - ); - return std::formatter::format(output, ctx); - } -}; - +//------------------------------------------------------------------------------------------------------------ +// pixi template <> struct std::formatter : std::formatter { auto format(const mbmff::pixi_box& box, std::format_context& ctx) const { - auto bits = box.bits_per_channel(); - std::string output = std::format("PIXI [{} bytes]: bits_per_channel=[", box.box_header.size); - for (std::size_t i = 0; i < bits.size(); ++i) { - if (i != 0) { - output.append(", "); - } - output.append(std::format("{}", bits[i])); + auto v = box.value(); + std::string out = std::format(" size=\"{}\" bits_per_channel=\"[", box.size_); + for (std::size_t i = 0; i < v.bits_per_channel.size(); ++i) { + if (i) out += ','; + out += std::format("{}", v.bits_per_channel[i]); } - return std::formatter::format(output.append("]"), ctx); + out += "]\""; + return std::formatter::format(out, ctx); } }; +//------------------------------------------------------------------------------------------------------------ +// pasp template <> -struct std::formatter : std::formatter { - auto format(const mbmff::ipma_association& box, std::format_context& ctx) const +struct std::formatter : std::formatter { + auto format(const mbmff::pasp_box& box, std::format_context& ctx) const { + auto v = box.value(); return std::formatter::format( - std::format("ipco_property={} essential={}", box.property_index, box.essential ? "yes" : "no"), + std::format(" size=\"{}\" h_spacing=\"{}\" v_spacing=\"{}\"", box.size_, v.h_spacing, v.v_spacing), ctx ); } }; +//------------------------------------------------------------------------------------------------------------ +// ipma template <> -struct std::formatter : std::formatter { - auto format(const mbmff::ipma_entry& entry, std::format_context& ctx) const +struct std::formatter : std::formatter { + auto format(const mbmff::ipma_box& box, std::format_context& ctx) const { - std::string output = std::format("IPMA Entry: item_id={} associations=[", entry.item_id); - for (std::size_t i = 0; i < entry.size(); ++i) { - if (i != 0) { - output.append(", "); - } - output += std::format("{{ {} }}", entry[i]); - } - return std::formatter::format(output.append("]"), ctx); + auto v = box.value(); + return std::formatter::format( + std::format(" size=\"{}\" entry_count=\"{}\"", box.size_, v.entry_count), + ctx + ); } }; +//------------------------------------------------------------------------------------------------------------ +// infe template <> -struct std::formatter : std::formatter { - auto format(const mbmff::iloc_extent& extent, std::format_context& ctx) const +struct std::formatter : std::formatter { + auto format(const mbmff::infe_box& box, std::format_context& ctx) const { - return std::formatter::format( - std::format("extent(index={} offset={} length={})", extent.index, extent.offset, extent.length), - ctx - ); + auto v = box.value(); + std::string out = std::format(" size=\"{}\" id=\"{}\" protection=\"{}\"", + box.size_, v.item_id, v.item_protection_index); + if (!v.item_name.empty()) { + out += std::format(" name=\"{}\"", v.item_name); + } + if (v.item_type.view().size() == 4) { + out += std::format(" type=\"{}\"", v.item_type.view()); + } + if (!v.content_type.empty()) { + out += std::format(" content_type=\"{}\"", v.content_type); + } + if (!v.content_encoding.empty()) { + out += std::format(" content_encoding=\"{}\"", v.content_encoding); + } + if (!v.item_uri_type.empty()) { + out += std::format(" uri=\"{}\"", v.item_uri_type); + } + return std::formatter::format(out, ctx); } }; +//------------------------------------------------------------------------------------------------------------ +// av1C template <> -struct std::formatter : std::formatter { - auto format(const mbmff::iloc_item& item, std::format_context& ctx) const +struct std::formatter : std::formatter { + auto format(const mbmff::av1C_box& box, std::format_context& ctx) const { - std::string output = std::format( - "ILOC Item: item_id={} base_offset={} construction_method={} data_reference_index={} extents=[", - item.item_id, item.base_offset, item.construction_method, item.data_reference_index - ); - for (std::size_t i = 0; i < item.size(); ++i) { - if (i != 0) { - output.append(", "); - } - output += std::format("{}", item[i]); + auto v = box.value(); + std::string out = std::format( + " size=\"{}\" seq_profile=\"{}\" seq_level_idx_0=\"{}\" seq_tier_0=\"{}\"" + " high_bitdepth=\"{}\" twelve_bit=\"{}\" monochrome=\"{}\"" + " chroma_subsampling_x=\"{}\" chroma_subsampling_y=\"{}\"" + " chroma_sample_position=\"{}\"", + box.size_, + static_cast(v.seq_profile), static_cast(v.seq_level_idx_0), + static_cast(v.seq_tier_0), + static_cast(v.high_bitdepth), static_cast(v.twelve_bit), + static_cast(v.monochrome), + static_cast(v.chroma_subsampling_x), static_cast(v.chroma_subsampling_y), + static_cast(v.chroma_sample_position)); + if (v.initial_presentation_delay_present) { + out += std::format(" initial_presentation_delay_minus_one=\"{}\"", + static_cast(v.initial_presentation_delay_minus_one)); + } + if (!v.config_obus.empty()) { + out += std::format(" config_obus_size=\"{}\"", v.config_obus.size()); } - return std::formatter::format(output.append("]"), ctx); + return std::formatter::format(out, ctx); } }; +//------------------------------------------------------------------------------------------------------------ +// iloc template <> -struct std::formatter : std::formatter { - auto format(const mbmff::pasp_box& box, std::format_context& ctx) const +struct std::formatter : std::formatter { + auto format(const mbmff::iloc_box& box, std::format_context& ctx) const { - auto [x, y] = box.aspect_ratio(); + auto v = box.value(); return std::formatter::format( - std::format("PASP [{} bytes]: aspect_ratio=({}x{})", box.box_header.size, x, y), + std::format(" size=\"{}\" version=\"{}\" offset_size=\"{}\" length_size=\"{}\"" + " base_offset_size=\"{}\" index_size=\"{}\" item_count=\"{}\"", + box.size_, box.version(), + v.offset_size, v.length_size, v.base_offset_size, v.index_size, v.item_count), ctx ); } diff --git a/tools/amalgamate.py b/tools/amalgamate.py new file mode 100644 index 0000000..f6d8b57 --- /dev/null +++ b/tools/amalgamate.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +"""Amalgamate mBMFF headers into a single header file.""" + +import argparse +import os +import re +import sys + +REPO_URL = "https://github.com/Agrael1/mBMFF" +COPYRIGHT_YEAR = "2026" +COPYRIGHT_HOLDER = "Ilya \"Agrael\" Doroshenko" + + +def read_version(repo_root: str) -> str: + path = os.path.join(repo_root, "VERSION") + with open(path, "r", encoding="utf-8") as f: + return f.read().strip() + + +def read_license(repo_root: str) -> str: + path = os.path.join(repo_root, "LICENSE.txt") + with open(path, "r", encoding="utf-8") as f: + text = f.read() + text = text.replace("[year]", COPYRIGHT_YEAR) + text = text.replace("[fullname]", COPYRIGHT_HOLDER) + return text + + +def build_comment_block(lines: list[str]) -> str: + return "\n".join("// " + line if line else "//" for line in lines) + + +def resolve_include(path: str, search_dirs: list[str]) -> str | None: + for d in search_dirs: + candidate = os.path.join(d, path) + if os.path.isfile(candidate): + return os.path.normpath(candidate) + return None + + +def amalgamate(entry_point: str, search_dirs: list[str], version: str) -> str: + inlined: set[str] = set() + guard_name = "MBMFF_AMALGAMATED_HPP" + + def process_file(filepath: str, depth: int = 0) -> list[str]: + norm = os.path.normpath(filepath) + if norm in inlined: + return [] + inlined.add(norm) + + lines: list[str] = [] + with open(filepath, "r", encoding="utf-8") as f: + for line in f: + stripped = line.strip() + + if stripped == "#pragma once": + continue + + m = re.match(r'#include\s+"([^"]+)"', stripped) + if m: + included = m.group(1) + resolved = resolve_include(included, [os.path.dirname(filepath)] + search_dirs) + if resolved is None: + print( + f"Warning: could not resolve '{included}' (from {filepath})", + file=sys.stderr, + ) + lines.append(line) + continue + content = process_file(resolved, depth + 1) + if content: + if lines and lines[-1].strip(): + lines.append("\n") + lines.extend(content) + if not lines[-1].endswith("\n"): + lines.append("\n") + continue + + lines.append(line) + + return lines + + resolved = resolve_include(entry_point, search_dirs) + if resolved is None: + print(f"Error: entry point '{entry_point}' not found", file=sys.stderr) + sys.exit(1) + + body = process_file(resolved) + + header = build_comment_block([ + "MIT License", + "", + f"Copyright (c) {COPYRIGHT_YEAR} {COPYRIGHT_HOLDER}", + "", + "Permission is hereby granted, free of charge, to any person obtaining a copy", + "of this software and associated documentation files (the \"Software\"), to deal", + "in the Software without restriction, including without limitation the rights", + "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", + "copies of the Software, and to permit persons to whom the Software is", + "furnished to do so, subject to the following conditions:", + "", + "The above copyright notice and this permission notice shall be included in all", + "copies or substantial portions of the Software.", + "", + 'THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR', + "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", + "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", + "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", + "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM", + "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE", + "SOFTWARE.", + "", + f"Repository: {REPO_URL}", + f"Version: {version}", + "", + "mBMFF -- single-header amalgamation", + "Generated by tools/amalgamate.py", + ]) + + result = [ + f"#ifndef {guard_name}\n", + f"#define {guard_name}\n", + "\n", + header + "\n", + "\n", + ] + result.extend(body) + result.append(f"\n#endif // {guard_name}\n") + return "".join(result) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Amalgamate mBMFF headers") + parser.add_argument( + "--input", + default=None, + help="Input directory containing mbmff.hpp (default: include/mbmff)", + ) + parser.add_argument( + "--entry", + default="mbmff.hpp", + help="Entry point header (default: mbmff.hpp)", + ) + parser.add_argument( + "--output", + default=None, + help="Output file (default: stdout)", + ) + args = parser.parse_args() + + script_dir = os.path.dirname(os.path.abspath(__file__)) + repo_root = os.path.abspath(os.path.join(script_dir, "..")) + + version = read_version(repo_root) + _ = read_license(repo_root) # validate it exists + + input_dir = args.input + if input_dir is None: + input_dir = os.path.join(repo_root, "include", "mbmff") + + entry_path = args.entry + if not os.path.isabs(entry_path): + entry_path = os.path.join(input_dir, entry_path) + + search_dirs = [input_dir, os.path.join(input_dir, "boxes")] + + output = amalgamate(entry_path, search_dirs, version) + + if args.output: + out_dir = os.path.dirname(args.output) + if out_dir: + os.makedirs(out_dir, exist_ok=True) + with open(args.output, "w", encoding="utf-8", newline="\n") as f: + f.write(output) + else: + sys.stdout.write(output) + + +if __name__ == "__main__": + main()