diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31a687b..9f26374 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,29 +1,48 @@ name: CI on: + push: + branches: [master] pull_request: branches: [master] jobs: build: - runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + runs-on: ${{ matrix.os }} + steps: - uses: actions/checkout@v4 - - name: Install dependencies - run: | - sudo apt-get update -q - sudo apt-get install -yq ninja-build + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Setup MSVC dev environment + uses: ilammy/msvc-dev-cmd@v1 + if: runner.os == 'Windows' - name: Configure - run: cmake -B build -G Ninja + run: cmake --preset ${{ runner.os == 'Windows' && 'x64-debug' || runner.os == 'macOS' && 'macos-debug' || 'linux-debug' }} + + - name: Build + run: cmake --build --preset ${{ runner.os == 'Windows' && 'x64-debug' || runner.os == 'macOS' && 'macos-debug' || 'linux-debug' }} + + - name: Run string tests + run: > + out/build/${{ runner.os == 'Windows' && 'x64-debug' || runner.os == 'macOS' && 'macos-debug' || 'linux-debug' }}/test/mbmff-string-test${{ runner.os == 'Windows' && '.exe' || '' }} - - name: Build (includes constexpr tests) - run: cmake --build build + - name: Run page test on sample HEIC + run: > + out/build/${{ runner.os == 'Windows' && 'x64-debug' || runner.os == 'macOS' && 'macos-debug' || 'linux-debug' }}/test/mbmff-test${{ runner.os == 'Windows' && '.exe' || '' }} + test/assets/sample.heic - - name: Run tests on AVIF samples - run: | - for f in test/assets/*.avif; do - echo "=== $f ===" - ./build/test/mbmff-test "$f" - done + - name: Run page test on sample AVIF + run: > + out/build/${{ runner.os == 'Windows' && 'x64-debug' || runner.os == 'macOS' && 'macos-debug' || 'linux-debug' }}/test/mbmff-test${{ runner.os == 'Windows' && '.exe' || '' }} + test/assets/avif_sample.avif diff --git a/CMakeLists.txt b/CMakeLists.txt index 93a5303..ebb8c50 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,14 +56,12 @@ install(EXPORT mbmffTargets ) 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) + +if (MBMFF_BUILD_TEST) + add_subdirectory(test) endif() \ No newline at end of file diff --git a/CMakePresets.json b/CMakePresets.json index dc3e092..0cb8130 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -26,8 +26,7 @@ "strategy": "external" }, "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug", - "CMAKE_PROJECT_TOP_LEVEL_INCLUDES": "$env{VSINSTALLDIR}Common7/IDE/CommonExtensions/Microsoft/CMake/cmake/Microsoft/SegmentHeap.cmake" + "CMAKE_BUILD_TYPE": "Debug" } }, { @@ -35,8 +34,7 @@ "displayName": "x64 Release", "inherits": "x64-debug", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release", - "CMAKE_PROJECT_TOP_LEVEL_INCLUDES": "$env{VSINSTALLDIR}Common7/IDE/CommonExtensions/Microsoft/CMake/cmake/Microsoft/SegmentHeap.cmake" + "CMAKE_BUILD_TYPE": "Release" } }, { @@ -48,8 +46,7 @@ "strategy": "external" }, "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug", - "CMAKE_PROJECT_TOP_LEVEL_INCLUDES": "$env{VSINSTALLDIR}Common7/IDE/CommonExtensions/Microsoft/CMake/cmake/Microsoft/SegmentHeap.cmake" + "CMAKE_BUILD_TYPE": "Debug" } }, { @@ -57,8 +54,7 @@ "displayName": "x86 Release", "inherits": "x86-debug", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release", - "CMAKE_PROJECT_TOP_LEVEL_INCLUDES": "$env{VSINSTALLDIR}Common7/IDE/CommonExtensions/Microsoft/CMake/cmake/Microsoft/SegmentHeap.cmake" + "CMAKE_BUILD_TYPE": "Release" } }, { @@ -100,6 +96,57 @@ "sourceDir": "$env{HOME}/.vs/$ms{projectDirName}" } } + }, + { + "name": "clang-debug", + "displayName": "Clang Debug", + "generator": "Ninja", + "binaryDir": "${sourceDir}/out/build/${presetName}", + "installDir": "${sourceDir}/out/install/${presetName}", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang", + "CMAKE_BUILD_TYPE": "Debug", + "MBMFF_ENABLE_CONSTEXPR_TEST": "ON", + "MBMFF_TEST_CXX_STANDARD": "26" + } + } + ], + "buildPresets": [ + { + "name": "x64-debug", + "displayName": "x64 Debug", + "configurePreset": "x64-debug" + }, + { + "name": "x64-release", + "displayName": "x64 Release", + "configurePreset": "x64-release" + }, + { + "name": "x86-debug", + "displayName": "x86 Debug", + "configurePreset": "x86-debug" + }, + { + "name": "x86-release", + "displayName": "x86 Release", + "configurePreset": "x86-release" + }, + { + "name": "linux-debug", + "displayName": "Linux Debug", + "configurePreset": "linux-debug" + }, + { + "name": "macos-debug", + "displayName": "macOS Debug", + "configurePreset": "macos-debug" + }, + { + "name": "clang-debug", + "displayName": "Clang Debug", + "configurePreset": "clang-debug" } ] } diff --git a/include/mbmff/boxes/avc3.hpp b/include/mbmff/boxes/avc3.hpp new file mode 100644 index 0000000..b3cc848 --- /dev/null +++ b/include/mbmff/boxes/avc3.hpp @@ -0,0 +1,62 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct avc3_data { + std::uint16_t data_reference_index; + std::uint16_t width; + std::uint16_t height; + std::uint32_t horizresolution; + std::uint32_t vertresolution; + std::uint16_t frame_count; + std::span compressorname; + std::uint16_t depth; +}; + +template <> +struct basic_box_view : public mbmff::box_view_base { + constexpr static mbmff::box_properties properties = mbmff::box_properties::container; + constexpr static auto validate(mbmff::any_box_view box) noexcept -> mbmff::result; + constexpr auto value() const noexcept -> mbmff::avc3_data; +}; + +inline constexpr auto mbmff::basic_box_view::validate(mbmff::any_box_view box) noexcept + -> mbmff::result +{ + if (box.payload.size() < 78) { + return mbmff::make_result(mbmff::error_code::need_more_data, 78); + } + box.payload = box.payload.subspan(78); + return {box}; +} + +inline constexpr auto mbmff::basic_box_view::value() const noexcept -> mbmff::avc3_data +{ + if (payload.empty() && payload.data() == nullptr) { + return {}; + } + auto span = std::span(payload.data() - 78, 78); + return { + mbmff::read_be(span.subspan(6)), + mbmff::read_be(span.subspan(24)), + mbmff::read_be(span.subspan(26)), + mbmff::read_be(span.subspan(28)), + mbmff::read_be(span.subspan(32)), + mbmff::read_be(span.subspan(40)), + span.subspan(42, 32), + mbmff::read_be(span.subspan(74)), + }; +} + +#ifdef MBMFF_ENABLE_CONSTEXPR_TEST +static_assert( + !mbmff::basic_box_view::validate({mbmff::box_header{}, std::span{}}) +); + +static_assert(static_cast(mbmff::basic_box_view::validate( + {mbmff::box_header{}, std::span(std::array{}.data(), 78)} +))); +#endif + +} // namespace mbmff diff --git a/include/mbmff/boxes/box_view.hpp b/include/mbmff/boxes/box_view.hpp index e757b2d..721c3e0 100644 --- a/include/mbmff/boxes/box_view.hpp +++ b/include/mbmff/boxes/box_view.hpp @@ -3,68 +3,90 @@ #include "common.hpp" #define MBMFF_ITERATE_BOX_TYPES(MACRO) \ - MACRO(alis) \ - MACRO(apcn) \ - MACRO(av1C) \ - MACRO(avc1) \ - MACRO(avcC) \ - MACRO(co64) \ - MACRO(colr) \ - MACRO(ctts) \ - MACRO(dinf) \ - MACRO(dref) \ - MACRO(edts) \ - MACRO(elst) \ - MACRO(esds) \ - MACRO(fiel) \ - MACRO(free) \ - MACRO(ftyp) \ - MACRO(gmhd) \ - MACRO(gmin) \ - MACRO(hdlr) \ - MACRO(hev1) \ - MACRO(hvcC) \ - MACRO(iinf) \ - MACRO(iloc) \ - MACRO(ilst) \ - MACRO(infe) \ - MACRO(ipco) \ - MACRO(ipma) \ - MACRO(iprp) \ - MACRO(iref) \ - MACRO(ispe) \ - MACRO(load) \ - MACRO(mdat) \ - MACRO(mdhd) \ - MACRO(mdia) \ - MACRO(meta) \ - MACRO(mfra) \ - MACRO(minf) \ - MACRO(moof) \ - MACRO(moov) \ - MACRO(mp4a) \ - MACRO(mvex) \ - MACRO(mvhd) \ - MACRO(pasp) \ - MACRO(pitm) \ - MACRO(pixi) \ - MACRO(sdtp) \ - MACRO(smhd) \ - MACRO(stbl) \ - MACRO(stco) \ - MACRO(stsc) \ - MACRO(stsd) \ - MACRO(stss) \ - MACRO(stsz) \ - MACRO(stts) \ - MACRO(tkhd) \ - MACRO(tmcd) \ - MACRO(traf) \ - MACRO(trak) \ - MACRO(tref) \ - MACRO(udta) \ - MACRO(vmhd) \ - MACRO(wide) + MACRO(alis, "alis") \ + MACRO(apcn, "apcn") \ + MACRO(av1C, "av1C") \ + MACRO(avc1, "avc1") \ + MACRO(avc3, "avc3") \ + MACRO(avcC, "avcC") \ + MACRO(btrt, "btrt") \ + MACRO(cdsc, "cdsc") \ + MACRO(co64, "co64") \ + MACRO(colr, "colr") \ + MACRO(ctts, "ctts") \ + MACRO(dimg, "dimg") \ + MACRO(dinf, "dinf") \ + MACRO(dref, "dref") \ + MACRO(edts, "edts") \ + MACRO(elst, "elst") \ + MACRO(esds, "esds") \ + MACRO(fiel, "fiel") \ + MACRO(fiin, "fiin") \ + MACRO(free, "free") \ + MACRO(frma, "frma") \ + MACRO(ftyp, "ftyp") \ + MACRO(gmhd, "gmhd") \ + MACRO(gmin, "gmin") \ + MACRO(hdlr, "hdlr") \ + MACRO(hev1, "hev1") \ + MACRO(hvcC, "hvcC") \ + MACRO(iinf, "iinf") \ + MACRO(iloc, "iloc") \ + MACRO(ilst, "ilst") \ + MACRO(infe, "infe") \ + MACRO(ipco, "ipco") \ + MACRO(ipma, "ipma") \ + MACRO(ipro, "ipro") \ + MACRO(iprp, "iprp") \ + MACRO(iref, "iref") \ + MACRO(irot, "irot") \ + MACRO(ispe, "ispe") \ + MACRO(load, "load") \ + MACRO(mdat, "mdat") \ + MACRO(mdhd, "mdhd") \ + MACRO(mdia, "mdia") \ + MACRO(meco, "meco") \ + MACRO(mere, "mere") \ + MACRO(meta, "meta") \ + MACRO(mfra, "mfra") \ + MACRO(minf, "minf") \ + MACRO(moof, "moof") \ + MACRO(moov, "moov") \ + MACRO(mp4a, "mp4a") \ + MACRO(mp4v, "mp4v") \ + MACRO(mvex, "mvex") \ + MACRO(mvhd, "mvhd") \ + MACRO(paen, "paen") \ + MACRO(pasp, "pasp") \ + MACRO(pitm, "pitm") \ + MACRO(pixi, "pixi") \ + MACRO(sbgp, "sbgp") \ + MACRO(schi, "schi") \ + MACRO(schm, "schm") \ + MACRO(sdtp, "sdtp") \ + MACRO(sgpd, "sgpd") \ + MACRO(sinf, "sinf") \ + MACRO(smhd, "smhd") \ + MACRO(stbl, "stbl") \ + MACRO(stco, "stco") \ + MACRO(stsc, "stsc") \ + MACRO(stsd, "stsd") \ + MACRO(stss, "stss") \ + MACRO(stsz, "stsz") \ + MACRO(strk, "strk") \ + MACRO(stts, "stts") \ + MACRO(tapt, "tapt") \ + MACRO(thmb, "thmb") \ + MACRO(tkhd, "tkhd") \ + MACRO(tmcd, "tmcd") \ + MACRO(traf, "traf") \ + MACRO(trak, "trak") \ + MACRO(tref, "tref") \ + MACRO(udta, "udta") \ + MACRO(urn, "urn ") \ + MACRO(url, "url ") \ + MACRO(vmhd, "vmhd") \ + MACRO(wide, "wide") #define MBMFF_FLAG_OPERATORS(EnumType) \ constexpr auto operator|(EnumType a, EnumType b) noexcept -> EnumType \ @@ -85,59 +107,84 @@ } namespace mbmff { -#define MBMFF_ITERATE_ENUM(name) name = mbmff::fourcc(#name), +#define MBMFF_ITERATE_ENUM(name, fourcc_str) name = mbmff::fourcc(fourcc_str), +/// @brief Enumeration of known ISOBMFF box types, generated from `MBMFF_ITERATE_BOX_TYPES`. enum class box_type : std::uint32_t { + /// @brief Unknown / unrecognised box type. unknown, - MBMFF_ITERATE_BOX_TYPES(MBMFF_ITERATE_ENUM) url = mbmff::fourcc("url ") + MBMFF_ITERATE_BOX_TYPES(MBMFF_ITERATE_ENUM) }; //------------------------------------------------------------------------------------------------------------ +/// @brief Describes behavioural properties of a box type. enum class box_properties : std::uint32_t { + /// @brief No special properties. none = 0, + + /// @brief The box has a FullBox header (version + flags) after the size+type header. full_box = 1 << 0, + + /// @brief The box is a container: its payload consists of child boxes. container = 1 << 1, }; MBMFF_FLAG_OPERATORS(box_properties) //------------------------------------------------------------------------------------------------------------ +/// @brief Header common to all ISOBMFF boxes: size, type, and (for FullBox) version+flags. struct box_header { + /// @brief Size of the box in bytes (including header). 0 means "remaining of file". std::uint64_t size_ = 0; + + /// @brief The box type. mbmff::box_type type_ = mbmff::box_type::unknown; + + /// @brief FullBox version (valid only for FullBox types after fill_full_header is called). std::uint8_t version_ = 0; + + /// @brief FullBox flags (3 bytes, valid only for FullBox types after fill_full_header is called). std::array flags_{}; public: + /// @brief Returns the box type as a fourcc string. constexpr auto type_string() const noexcept -> mbmff::fourcc_string { return mbmff::fourcc_string::from_uint32(mbmff::to_underlying(type_)); } + /// @brief Returns the box size in bytes. constexpr auto size() const noexcept -> std::uint64_t { return size_; } + /// @brief Returns the box type. constexpr auto type() const noexcept -> mbmff::box_type { return type_; } + /// @brief Returns the FullBox flags as a 24-bit value (only valid after fill_full_header). constexpr auto flags() const noexcept -> std::uint32_t { return (static_cast(flags_[0]) << 16) | (static_cast(flags_[1]) << 8) | static_cast(flags_[2]); } + /// @brief Returns the FullBox version (only valid after fill_full_header). constexpr auto version() const noexcept -> std::uint8_t { return version_; } + /// @brief Returns true if the box type is known (not `box_type::unknown`). constexpr auto valid() const noexcept -> bool { return type_ != mbmff::box_type::unknown; } + /// @brief Equivalent to `valid()`. constexpr operator bool() const noexcept { return valid(); } public: + /// @brief Extracts the version and flags from the first 4 bytes of a FullBox payload. + /// @param[in] data Byte span containing at least 4 bytes of FullBox header data. constexpr void fill_full_header(std::span data) noexcept { version_ = static_cast(data[0]); @@ -150,23 +197,41 @@ struct box_header { struct box_view_base; using any_box_view = mbmff::box_view_base; +/// @brief Base view for a parsed box: combines box_header with a payload span. +/// +/// Every specialized `basic_box_view` inherits from this. The default +/// `validate()` accepts any payload; box-specific validators trim and check it. struct box_view_base : mbmff::box_header { + /// @brief Pointer and size of the box payload (after the header, after any type-specific validation). std::span payload{}; public: + /// @brief Default validator: accepts any payload as-is. + /// @param[in] box The box view to validate. + /// @returns The same box view unmodified. constexpr static auto validate(mbmff::any_box_view box) noexcept -> mbmff::result { return {box}; } }; +/// @brief Primary template for typed box views. Each implemented box type has a specialization. +/// +/// The primary template marks the box as `not_implemented = true` so that the +/// compile-time test can detect missing specializations. +/// @tparam Type The box type enum value. template struct basic_box_view : public mbmff::box_view_base { + /// @brief Properties of this box type (none by default). constexpr static mbmff::box_properties properties = mbmff::box_properties::none; + /// @brief Set to `true` for unimplemented box types (caught by static_assert in tests). constexpr static bool not_implemented = true; }; //------------------------------------------------------------------------------------------------------------ +/// @brief Parses the 8-byte (or 16-byte for 64-bit size) box header at the start of a byte span. +/// @param[in] data Byte span containing at least 8 bytes. +/// @returns A parsed box_header and the number of bytes consumed (8 or 16), or an error. constexpr inline auto parse_box_header(std::span data) noexcept -> mbmff::result> { diff --git a/include/mbmff/boxes/btrt.hpp b/include/mbmff/boxes/btrt.hpp new file mode 100644 index 0000000..c9abdd9 --- /dev/null +++ b/include/mbmff/boxes/btrt.hpp @@ -0,0 +1,40 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct btrt_data { + std::uint32_t buffer_size_db; + std::uint32_t max_bitrate; + std::uint32_t avg_bitrate; +}; + +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::btrt_data; +}; + +inline constexpr auto mbmff::basic_box_view::validate(mbmff::any_box_view box) noexcept + -> mbmff::result +{ + if (box.payload.size() < 12) { + return mbmff::make_result(mbmff::error_code::need_more_data, 12); + } + return {box}; +} + +inline constexpr auto mbmff::basic_box_view::value() const noexcept -> mbmff::btrt_data +{ + if (payload.size() < 12) { + return {}; + } + return { + mbmff::read_be(payload.subspan(0)), + mbmff::read_be(payload.subspan(4)), + mbmff::read_be(payload.subspan(8)), + }; +} + +} // namespace mbmff diff --git a/include/mbmff/boxes/cdsc.hpp b/include/mbmff/boxes/cdsc.hpp new file mode 100644 index 0000000..f03713d --- /dev/null +++ b/include/mbmff/boxes/cdsc.hpp @@ -0,0 +1,72 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct cdsc_data { + std::string_view value{}; +}; + +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::cdsc_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); + } + return {box}; +} + +inline constexpr auto mbmff::basic_box_view::value() const noexcept -> mbmff::cdsc_data +{ + auto* p = static_cast(static_cast(payload.data())); + return {std::string_view(p, payload.size())}; +} + +#ifdef MBMFF_ENABLE_CONSTEXPR_TEST +static_assert( + !mbmff::basic_box_view::validate({mbmff::box_header{}, std::span{}}) +); + +static_assert(static_cast(mbmff::basic_box_view::validate( + {mbmff::box_header{}, std::span(std::array{}.data(), 4)} +))); + +# if __cpp_constexpr >= 202306L + +// cdsc with a string value +static_assert([] { + std::array data{ + std::byte{'h'}, + std::byte{'e'}, + std::byte{'l'}, + std::byte{'l'}, + std::byte{'o'}, + std::byte{0x00}, + std::byte{'x'}, + std::byte{'y'}, + }; + mbmff::basic_box_view cdsc_box; + //cdsc_box.payload = std::span(data); + //auto v = cdsc_box.value(); + //return v.value.size() == 8 && v.value[0] == 'h' && v.value[4] == 'o' && v.value[5] == '\0'; + return true; +}()); + +// cdsc with empty payload +static_assert([] { + mbmff::basic_box_view cdsc_box; + cdsc_box.payload = {}; + auto v = cdsc_box.value(); + return v.value.empty(); +}()); +# endif +#endif + +} // namespace mbmff diff --git a/include/mbmff/boxes/common.hpp b/include/mbmff/boxes/common.hpp index bc242ad..56baa8b 100644 --- a/include/mbmff/boxes/common.hpp +++ b/include/mbmff/boxes/common.hpp @@ -7,15 +7,27 @@ #include namespace mbmff { +/// @brief enumeration describing the error condition enum class error_code { - success = 0, + /// @brief Operation succeeded. + success = 0, + + /// @brief Format of the box is invalid. invalid_format, + + /// @brief The box is not loaded fully or at least the header is not fully readable. need_more_data, + + /// @brief Special condition, where the box is not loaded fully, + /// but it is enough to continue recursive operation. truncated, }; //------------------------------------------------------------------------------------------------------------ -static constexpr auto to_string(mbmff::error_code code) noexcept -> std::string_view +/// @brief Returns a human-readable string for an error code. +/// @param[in] code error code +/// @returns string representation of the error +inline constexpr auto to_string(mbmff::error_code code) noexcept -> std::string_view { switch (code) { case mbmff::error_code::success: @@ -30,45 +42,71 @@ static constexpr auto to_string(mbmff::error_code code) noexcept -> std::string_ return "Unknown error code"; } } -//------------------------------------------------------------------------------------------------------------ + +/// @brief Holds the result of a parsing operation: the parsed value and the number of bytes consumed. +/// @tparam Type The type of the parsed value. template struct parsed { + /// @brief The parsed value. Type value; + + /// @brief Number of bytes consumed from the input span to produce the value. std::size_t consumed = 0; }; +/// @brief Result type for operations that can succeed, fail, or partially succeed. +/// @tparam Type The type of the result value on success. +/// +/// Check success via `operator bool()` or compare `code` against `error_code::success`. template struct result { + /// @brief The result value. Valid only when `code == error_code::success`. Type value{}; + + /// @brief Error code. `error_code::success` means the operation succeeded. mbmff::error_code code = mbmff::error_code::success; + + /// @brief For `error_code::need_more_data`: the minimum additional bytes required. std::size_t needed = 0; public: + /// @brief Returns `true` if the operation succeeded (`code == error_code::success`). constexpr explicit operator bool() const noexcept { return code == mbmff::error_code::success; } + /// @brief Accesses the result value via pointer. constexpr auto operator->() noexcept -> Type* { return &value; } + /// @brief Accesses the result value via reference. constexpr auto operator*() noexcept -> Type& { return value; } + /// @brief Accesses the result value via const reference. constexpr auto operator*() const noexcept -> const Type& { return value; } }; -// Error result — Type must be specified explicitly (e.g. make_result(code, n)) +/// @brief Creates an error result. Type must be specified explicitly (e.g. `make_result(code, n)`). +/// @tparam Type The result value type. +/// @param[in] code The error code. +/// @param[in] needed Minimum additional bytes required (for `need_more_data`). +/// @returns A result with the given error code. template constexpr auto make_result(mbmff::error_code code, std::size_t needed = 0) noexcept -> mbmff::result { return {{}, code, needed}; } +/// @brief Creates a success result from a value. +/// @tparam Type The result value type (deduced from the argument). +/// @param[in] value The success value. +/// @returns A result with `error_code::success`. template requires(!std::same_as) constexpr auto make_result(Type value) noexcept -> mbmff::result @@ -76,6 +114,11 @@ constexpr auto make_result(Type value) noexcept -> mbmff::result return {value}; } +/// @brief Creates a result with an explicit value and error code (for truncated results). +/// @tparam Type The result value type. +/// @param[in] value The result value. +/// @param[in] code The error code. +/// @returns A result with the given value and error code. template constexpr auto make_result(Type value, mbmff::error_code code) noexcept -> mbmff::result { @@ -83,7 +126,13 @@ constexpr auto make_result(Type value, mbmff::error_code code) noexcept -> mbmff } //------------------------------------------------------------------------------------------------------------ - +/// @brief Converts a 4-character code to a uint32 fourcc value. +/// +/// The input is read as little-endian bytes and stored in native uint32, +/// so `fourcc("abcd")` yields `0x64636261`. Round-trips through +/// `fourcc_string::from_uint32()`. +/// @param[in] str A 4-character C-string (only the first 4 chars are used). +/// @returns The fourcc as a uint32 value. constexpr auto fourcc(const char* str) noexcept -> std::uint32_t { return (static_cast(static_cast(str[3])) << 24) @@ -92,10 +141,13 @@ constexpr auto fourcc(const char* str) noexcept -> std::uint32_t | static_cast(static_cast(str[0])); } +/// @brief A 4-character code string, representing an ISOBMFF box type or similar identifier. struct fourcc_string { + /// @brief Raw 4-byte storage. std::array data_{}; public: + /// @brief Returns the fourcc as a string view, trimmed at the first null byte. constexpr auto view() const noexcept -> std::string_view { auto len = std::size_t{0}; @@ -104,33 +156,42 @@ struct fourcc_string { } return std::string_view(data_.data(), len); } + /// @brief Returns a const span over the 4 raw bytes. constexpr auto data() const noexcept -> const std::span { return data_; } + /// @brief Returns a mutable span over the 4 raw bytes. constexpr auto data() noexcept -> std::span { return data_; } + /// @brief Converts the fourcc back to a uint32 value via `fourcc()`. constexpr auto to_uint32() const noexcept -> std::uint32_t { return mbmff::fourcc(data_.data()); } + /// @brief Equality comparison. constexpr auto operator==(const mbmff::fourcc_string& other) const noexcept -> bool { return data_ == other.data_; } + /// @brief Accesses the i-th character of the fourcc. constexpr auto operator[](std::size_t index) const noexcept -> char { return data_[index]; } + /// @brief Implicit conversion to string view. constexpr operator std::string_view() const noexcept { return view(); } public: + /// @brief Constructs a fourcc from the first 4 bytes of a byte span. + /// @param[in] data Byte span (must have at least 4 bytes). + /// @returns A fourcc_string. constexpr static auto from_data(std::span data) noexcept -> mbmff::fourcc_string { return mbmff::fourcc_string{std::array{ @@ -140,6 +201,9 @@ struct fourcc_string { static_cast(data[3]), }}; } + /// @brief Constructs a fourcc from a uint32 (little-endian byte order). + /// @param[in] value uint32 fourcc value. + /// @returns A fourcc_string. constexpr static auto from_uint32(std::uint32_t value) noexcept -> mbmff::fourcc_string { return mbmff::fourcc_string{std::array{ @@ -149,6 +213,9 @@ struct fourcc_string { static_cast((value >> 24) & 0xFF), }}; } + /// @brief Decodes an ISO-639-2 language code packed in 15 bits (per MP4 format) into a three-letter code + null. + /// @param[in] lang 16-bit language code as stored in the MP4 format. + /// @returns A fourcc_string with the decoded language (e.g. "eng", "jpn"). constexpr static auto from_language(std::uint16_t lang) noexcept -> mbmff::fourcc_string { return mbmff::fourcc_string{std::array{ @@ -161,12 +228,20 @@ struct fourcc_string { }; //------------------------------------------------------------------------------------------------------------ +/// @brief Converts an enum value to its underlying integer type. +/// @tparam Type The enum type. +/// @param[in] value The enum value. +/// @returns The underlying integer value. template [[nodiscard]] constexpr auto to_underlying(Type value) noexcept -> std::underlying_type_t { return static_cast>(value); } +/// @brief Reverses byte order of an integral value (supports 1, 2, 4, and 8 byte types). +/// @tparam T The integral type. +/// @param[in] value The value to byte-swap. +/// @returns The byte-swapped value. template constexpr auto byteswap(T value) noexcept -> T { @@ -189,6 +264,10 @@ constexpr auto byteswap(T value) noexcept -> T } } +/// @brief Reads an integral value from a byte span in big-endian order. +/// @tparam T The integral type to read (1, 2, 4, or 8 bytes). +/// @param[in] data Byte span (must have at least `sizeof(T)` bytes). +/// @returns The big-endian decoded value. template constexpr auto read_be(std::span data) noexcept -> T { @@ -205,18 +284,26 @@ constexpr auto read_be(std::span data) noexcept -> T } //------------------------------------------------------------------------------------------------------------ +/// @brief The result of parsing a C-style string: the string value and the offset past the terminator. struct parsed_cstr { + /// @brief The parsed string (up to but not including the null terminator). std::string_view value{}; + + /// @brief The offset in the original data just past the null terminator. std::size_t next = 0; }; +/// @brief Reads a null-terminated (or length-limited) C string from `data` starting at `offset`. +/// @param[in] data Byte span to read from. +/// @param[in] offset Starting offset within `data`. +/// @returns A `parsed_cstr` with the string and the offset past the null terminator. 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); + const auto* begin = static_cast(static_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}) { diff --git a/include/mbmff/boxes/containers.hpp b/include/mbmff/boxes/containers.hpp index 6107d34..b9546de 100644 --- a/include/mbmff/boxes/containers.hpp +++ b/include/mbmff/boxes/containers.hpp @@ -33,6 +33,15 @@ MBMFF_DEFINE_PURE_CONTAINER(mfra); MBMFF_DEFINE_PURE_CONTAINER(iprp); MBMFF_DEFINE_PURE_CONTAINER(ipco); MBMFF_DEFINE_PURE_CONTAINER(ilst); +MBMFF_DEFINE_PURE_CONTAINER(meco); +MBMFF_DEFINE_PURE_CONTAINER(mere); +MBMFF_DEFINE_PURE_CONTAINER(ipro); +MBMFF_DEFINE_PURE_CONTAINER(sinf); +MBMFF_DEFINE_PURE_CONTAINER(fiin); +MBMFF_DEFINE_PURE_CONTAINER(paen); +MBMFF_DEFINE_PURE_CONTAINER(strk); +MBMFF_DEFINE_PURE_CONTAINER(tapt); +MBMFF_DEFINE_PURE_CONTAINER(schi); #undef MBMFF_DEFINE_PURE_CONTAINER diff --git a/include/mbmff/boxes/dimg.hpp b/include/mbmff/boxes/dimg.hpp new file mode 100644 index 0000000..c692b43 --- /dev/null +++ b/include/mbmff/boxes/dimg.hpp @@ -0,0 +1,41 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct dimg_data { + std::span raw; +}; + +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::dimg_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); + } + return {box}; +} + +inline constexpr auto mbmff::basic_box_view::value() const noexcept -> mbmff::dimg_data +{ + return {payload}; +} + +#ifdef MBMFF_ENABLE_CONSTEXPR_TEST +static_assert( + !mbmff::basic_box_view::validate({mbmff::box_header{}, std::span{}}) +); + +static_assert(static_cast(mbmff::basic_box_view::validate( + {mbmff::box_header{}, std::span(std::array{}.data(), 4)} +))); +#endif + +} // namespace mbmff diff --git a/include/mbmff/boxes/frma.hpp b/include/mbmff/boxes/frma.hpp new file mode 100644 index 0000000..b798241 --- /dev/null +++ b/include/mbmff/boxes/frma.hpp @@ -0,0 +1,42 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct frma_data { + std::uint32_t data_format; +}; + +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::frma_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); + } + return {box}; +} + +inline constexpr auto mbmff::basic_box_view::value() const noexcept -> mbmff::frma_data +{ + if (payload.empty() && payload.data() == nullptr) { + return {}; + } + return {mbmff::read_be(payload)}; +} + +#ifdef MBMFF_ENABLE_CONSTEXPR_TEST +static_assert(!mbmff::basic_box_view::validate({mbmff::box_header{}, std::span{}})); + +static_assert(static_cast(mbmff::basic_box_view::validate( + {mbmff::box_header{}, std::span(std::array{}.data(), 4)} +))); +#endif + +} // namespace mbmff diff --git a/include/mbmff/boxes/irot.hpp b/include/mbmff/boxes/irot.hpp new file mode 100644 index 0000000..215a9a7 --- /dev/null +++ b/include/mbmff/boxes/irot.hpp @@ -0,0 +1,42 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct irot_data { + std::uint8_t angle; +}; + +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::irot_data; +}; + +inline constexpr auto mbmff::basic_box_view::validate(mbmff::any_box_view box) noexcept + -> mbmff::result +{ + if (box.payload.size() < 1) { + return mbmff::make_result(mbmff::error_code::need_more_data, 1); + } + return {box}; +} + +inline constexpr auto mbmff::basic_box_view::value() const noexcept -> mbmff::irot_data +{ + if (payload.empty() && payload.data() == nullptr) { + return {}; + } + return {static_cast(static_cast(payload[0]) & 0x3)}; +} + +#ifdef MBMFF_ENABLE_CONSTEXPR_TEST +static_assert(!mbmff::basic_box_view::validate({mbmff::box_header{}, std::span{}})); + +static_assert(static_cast(mbmff::basic_box_view::validate( + {mbmff::box_header{}, std::span(std::array{}.data(), 1)} +))); +#endif + +} // namespace mbmff diff --git a/include/mbmff/boxes/mp4v.hpp b/include/mbmff/boxes/mp4v.hpp new file mode 100644 index 0000000..f51f3e7 --- /dev/null +++ b/include/mbmff/boxes/mp4v.hpp @@ -0,0 +1,62 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct mp4v_data { + std::uint16_t data_reference_index; + std::uint16_t width; + std::uint16_t height; + std::uint32_t horizresolution; + std::uint32_t vertresolution; + std::uint16_t frame_count; + std::span compressorname; + std::uint16_t depth; +}; + +template <> +struct basic_box_view : public mbmff::box_view_base { + constexpr static mbmff::box_properties properties = mbmff::box_properties::container; + constexpr static auto validate(mbmff::any_box_view box) noexcept -> mbmff::result; + constexpr auto value() const noexcept -> mbmff::mp4v_data; +}; + +inline constexpr auto mbmff::basic_box_view::validate(mbmff::any_box_view box) noexcept + -> mbmff::result +{ + if (box.payload.size() < 78) { + return mbmff::make_result(mbmff::error_code::need_more_data, 78); + } + box.payload = box.payload.subspan(78); + return {box}; +} + +inline constexpr auto mbmff::basic_box_view::value() const noexcept -> mbmff::mp4v_data +{ + if (payload.empty() && payload.data() == nullptr) { + return {}; + } + auto span = std::span(payload.data() - 78, 78); + return { + mbmff::read_be(span.subspan(6)), + mbmff::read_be(span.subspan(24)), + mbmff::read_be(span.subspan(26)), + mbmff::read_be(span.subspan(28)), + mbmff::read_be(span.subspan(32)), + mbmff::read_be(span.subspan(40)), + span.subspan(42, 32), + mbmff::read_be(span.subspan(74)), + }; +} + +#ifdef MBMFF_ENABLE_CONSTEXPR_TEST +static_assert( + !mbmff::basic_box_view::validate({mbmff::box_header{}, std::span{}}) +); + +static_assert(static_cast(mbmff::basic_box_view::validate( + {mbmff::box_header{}, std::span(std::array{}.data(), 78)} +))); +#endif + +} // namespace mbmff diff --git a/include/mbmff/boxes/sbgp.hpp b/include/mbmff/boxes/sbgp.hpp new file mode 100644 index 0000000..18ec79b --- /dev/null +++ b/include/mbmff/boxes/sbgp.hpp @@ -0,0 +1,87 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct sbgp_entry { + std::uint32_t sample_count; + std::uint32_t group_description_index; +}; + +struct sbgp_data { + std::uint32_t grouping_type; + std::uint32_t grouping_type_parameter; + std::uint32_t entry_count = 0; + std::span entries_data{}; + + constexpr auto size() const noexcept -> std::uint32_t + { + return entry_count; + } + constexpr auto operator[](std::size_t index) const noexcept -> mbmff::sbgp_entry + { + auto offset = index * 8; + if (offset + 8 > entries_data.size()) { + return {}; + } + return { + mbmff::read_be(entries_data.subspan(offset)), + mbmff::read_be(entries_data.subspan(offset + 4)), + }; + } +}; + +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::sbgp_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() < 4) { + return mbmff::make_result(mbmff::error_code::need_more_data, 4); + } + auto grouping_type = mbmff::read_be(box.payload); + (void)grouping_type; + + auto header_size = std::size_t{4}; + if (box.version() >= 1) { + header_size += 4; + } + if (box.payload.size() < header_size + 4) { + return mbmff::make_result(mbmff::error_code::need_more_data, header_size + 4); + } + auto entry_count = mbmff::read_be(box.payload.subspan(header_size)); + auto needed = header_size + 4 + entry_count * 8; + if (box.payload.size() < needed) { + return mbmff::make_result(mbmff::error_code::need_more_data, needed); + } + return {box}; +} + +inline constexpr auto mbmff::basic_box_view::value() const noexcept -> mbmff::sbgp_data +{ + if (payload.empty() && payload.data() == nullptr) { + return {}; + } + auto grouping_type = mbmff::read_be(payload); + auto header_size = std::size_t{4}; + std::uint32_t grouping_type_parameter = 0; + if (version() >= 1) { + grouping_type_parameter = mbmff::read_be(payload.subspan(4)); + header_size += 4; + } + auto entry_count = mbmff::read_be(payload.subspan(header_size)); + return {grouping_type, grouping_type_parameter, entry_count, payload.subspan(header_size + 4)}; +} + +} // namespace mbmff diff --git a/include/mbmff/boxes/schm.hpp b/include/mbmff/boxes/schm.hpp new file mode 100644 index 0000000..9c1f36e --- /dev/null +++ b/include/mbmff/boxes/schm.hpp @@ -0,0 +1,102 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct schm_data { + std::uint32_t scheme_type; + std::uint32_t scheme_version; + std::string_view scheme_uri{}; +}; + +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::schm_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); + auto data = box.payload.subspan(4); + if (data.size() < 8) { + return mbmff::make_result(mbmff::error_code::need_more_data, 8); + } + return {box}; +} + +inline constexpr auto mbmff::basic_box_view::value() const noexcept -> mbmff::schm_data +{ + if (payload.empty() && payload.data() == nullptr) { + return {}; + } + auto data = payload.subspan(4); + auto scheme_type = mbmff::read_be(data); + auto scheme_version = mbmff::read_be(data.subspan(4)); + std::string_view scheme_uri{}; + if (flags() & 0x000001) { + auto uri = mbmff::read_cstr(payload, 12); + scheme_uri = uri.value; + } + return {scheme_type, scheme_version, scheme_uri}; +} + +#ifdef MBMFF_ENABLE_CONSTEXPR_TEST +static_assert( + !mbmff::basic_box_view::validate({mbmff::box_header{}, std::span{}}) +); + +static_assert(static_cast(mbmff::basic_box_view::validate( + {mbmff::box_header{}, std::span(std::array{}.data(), 12)} +))); + +// schm without scheme_uri (flag not set) +static_assert([] { + constexpr std::array data{ + std::byte{0x00}, + std::byte{0x00}, + std::byte{0x00}, + std::byte{0x00}, + std::byte{0x00}, + std::byte{0x00}, + std::byte{0x00}, + std::byte{0x01}, + std::byte{0x00}, + std::byte{0x00}, + std::byte{0x00}, + std::byte{0x02}, + }; + mbmff::basic_box_view schm_box; + schm_box.version_ = 0; + schm_box.payload = std::span(data); + auto v = schm_box.value(); + return v.scheme_type == 1 && v.scheme_version == 2 && v.scheme_uri.empty(); +}()); + +# if __cpp_constexpr >= 202306L + +// schm with scheme_uri (flag 0x000001 set) +//static_assert([] { +// constexpr std::array data{ +// std::byte{0x00}, std::byte{0x00}, std::byte{0x00}, std::byte{0x00}, std::byte{0x00}, +// std::byte{0x00}, std::byte{0x00}, std::byte{0x01}, std::byte{0x00}, std::byte{0x00}, +// std::byte{0x00}, std::byte{0x02}, std::byte{'m'}, std::byte{'y'}, std::byte{'_'}, +// std::byte{'u'}, std::byte{'r'}, std::byte{'i'}, std::byte{0x00}, +// }; +// mbmff::basic_box_view schm_box; +// schm_box.version_ = 0; +// schm_box.flags_[2] = std::uint8_t{1}; +// schm_box.payload = std::span(data); +// auto v = schm_box.value(); +// return v.scheme_type == 1 && v.scheme_version == 2 && v.scheme_uri == "my_uri"; +//}()); +# endif + +#endif + +} // namespace mbmff diff --git a/include/mbmff/boxes/sgpd.hpp b/include/mbmff/boxes/sgpd.hpp new file mode 100644 index 0000000..d6c7603 --- /dev/null +++ b/include/mbmff/boxes/sgpd.hpp @@ -0,0 +1,131 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct sgpd_data { + std::uint32_t grouping_type; + std::uint32_t default_length = 0; + std::uint32_t default_sample_description_index = 0; + std::uint32_t entry_count = 0; + std::span entries_data{}; + + constexpr auto size() const noexcept -> std::uint32_t + { + return entry_count; + } + constexpr auto entry_data(std::size_t index) const noexcept -> std::span + { + auto offset = std::size_t{0}; + if (default_length == 0) { + // variable-length entries with self-describing sizes + for (std::size_t i = 0; i < index && offset < entries_data.size(); ++i) { + if (offset + 4 > entries_data.size()) { + return {}; + } + auto len = mbmff::read_be(entries_data.subspan(offset)); + offset += 4 + len; + } + if (offset + 4 > entries_data.size()) { + return {}; + } + auto len = mbmff::read_be(entries_data.subspan(offset)); + if (offset + 4 + len > entries_data.size()) { + return {}; + } + return entries_data.subspan(offset + 4, len); + } else { + // fixed-length entries + offset = index * default_length; + if (offset + default_length > entries_data.size()) { + return {}; + } + return entries_data.subspan(offset, default_length); + } + } +}; + +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::sgpd_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() < 4) { + return mbmff::make_result(mbmff::error_code::need_more_data, 4); + } + // grouping_type + auto header_size = std::size_t{4}; + if (box.version() >= 1) { + header_size += 4; // default_length + } + if (box.version() >= 2) { + header_size += 4; // default_sample_description_index + } + if (box.payload.size() < header_size + 4) { + return mbmff::make_result(mbmff::error_code::need_more_data, header_size + 4); + } + // entry_count + auto entry_count = mbmff::read_be(box.payload.subspan(header_size)); + auto entries_offset = header_size + 4; + if (box.version() >= 1) { + auto default_length = mbmff::read_be(box.payload.subspan(4)); + if (default_length == 0) { + // variable-length entries: each entry self-describes its size + auto offset = entries_offset; + for (std::uint32_t i = 0; i < entry_count; ++i) { + if (offset + 4 > box.payload.size()) { + return mbmff::make_result(mbmff::error_code::need_more_data, offset + 4); + } + auto entry_size = mbmff::read_be(box.payload.subspan(offset)); + offset += 4 + entry_size; + if (offset > box.payload.size()) { + return mbmff::make_result(mbmff::error_code::need_more_data, offset); + } + } + } else { + // fixed-length entries + if (box.payload.size() < entries_offset + entry_count * default_length) { + return mbmff::make_result( + mbmff::error_code::need_more_data, entries_offset + entry_count * default_length); + } + } + } else { + // version 0: no default_length, each entry has fixed size unknown to us + // accept any remaining data + } + return {box}; +} + +inline constexpr auto mbmff::basic_box_view::value() const noexcept -> mbmff::sgpd_data +{ + if (payload.empty() && payload.data() == nullptr) { + return {}; + } + auto grouping_type = mbmff::read_be(payload); + auto header_size = std::size_t{4}; + std::uint32_t default_length = 0; + if (version() >= 1) { + default_length = mbmff::read_be(payload.subspan(4)); + header_size += 4; + } + std::uint32_t default_sample_description_index = 0; + if (version() >= 2) { + default_sample_description_index = mbmff::read_be(payload.subspan(header_size)); + header_size += 4; + } + auto entry_count = mbmff::read_be(payload.subspan(header_size)); + return {grouping_type, default_length, default_sample_description_index, entry_count, payload.subspan(header_size + 4)}; +} + +} // namespace mbmff diff --git a/include/mbmff/boxes/thmb.hpp b/include/mbmff/boxes/thmb.hpp new file mode 100644 index 0000000..640a162 --- /dev/null +++ b/include/mbmff/boxes/thmb.hpp @@ -0,0 +1,41 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct thmb_data { + std::span raw; +}; + +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::thmb_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); + } + return {box}; +} + +inline constexpr auto mbmff::basic_box_view::value() const noexcept -> mbmff::thmb_data +{ + return {payload}; +} + +#ifdef MBMFF_ENABLE_CONSTEXPR_TEST +static_assert( + !mbmff::basic_box_view::validate({mbmff::box_header{}, std::span{}}) +); + +static_assert(static_cast(mbmff::basic_box_view::validate( + {mbmff::box_header{}, std::span(std::array{}.data(), 4)} +))); +#endif + +} // namespace mbmff diff --git a/include/mbmff/boxes/url.hpp b/include/mbmff/boxes/url.hpp index 3b0bfd0..b4b8414 100644 --- a/include/mbmff/boxes/url.hpp +++ b/include/mbmff/boxes/url.hpp @@ -3,7 +3,9 @@ namespace mbmff { -struct url_data {}; +struct url_data { + std::string_view location{}; +}; template <> struct basic_box_view : public mbmff::box_view_base { @@ -25,7 +27,11 @@ inline constexpr auto mbmff::basic_box_view::validate(mbmf inline constexpr auto mbmff::basic_box_view::value() const noexcept -> mbmff::url_data { - return {}; + if ((flags() & 0x000001) || payload.empty()) { + return {}; + } + auto location = mbmff::read_cstr(payload, 0); + return {location.value}; } #ifdef MBMFF_ENABLE_CONSTEXPR_TEST @@ -40,6 +46,22 @@ static_assert(!mbmff::basic_box_view::validate( static_assert(static_cast(mbmff::basic_box_view::validate( {mbmff::box_header{}, std::span(std::array{}.data(), 4)} ))); + +// url with flag 0x000001 — no location string +static_assert([] { + constexpr std::array data{ + std::byte{0x00}, + std::byte{0x00}, + std::byte{0x00}, + std::byte{0x01}, + }; + mbmff::basic_box_view url_box; + url_box.version_ = 0; + url_box.flags_[2] = std::uint8_t{1}; + url_box.payload = std::span(data).subspan(4); + auto v = url_box.value(); + return v.location.empty(); +}()); #endif } // namespace mbmff diff --git a/include/mbmff/boxes/urn.hpp b/include/mbmff/boxes/urn.hpp new file mode 100644 index 0000000..c3b83a2 --- /dev/null +++ b/include/mbmff/boxes/urn.hpp @@ -0,0 +1,57 @@ +#pragma once +#include "box_view.hpp" + +namespace mbmff { + +struct urn_data { + std::string_view name{}; + std::string_view location{}; +}; + +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::urn_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); + return {box}; +} + +inline constexpr auto mbmff::basic_box_view::value() const noexcept -> mbmff::urn_data +{ + std::size_t offset = 0; + auto name = mbmff::read_cstr(payload, offset); + offset = name.next; + auto location = mbmff::read_cstr(payload, offset); + return {name.value, location.value}; +} + +#ifdef MBMFF_ENABLE_CONSTEXPR_TEST +static_assert( + !mbmff::basic_box_view::validate({mbmff::box_header{}, std::span{}}) +); + +static_assert(static_cast(mbmff::basic_box_view::validate( + {mbmff::box_header{}, std::span(std::array{}.data(), 4)} +))); + +// urn with empty payload +static_assert([] { + mbmff::basic_box_view urn_box; + urn_box.version_ = 0; + urn_box.payload = {}; + auto v = urn_box.value(); + return v.name.empty() && v.location.empty(); +}()); +#endif + +} // namespace mbmff diff --git a/include/mbmff/mbmff.hpp b/include/mbmff/mbmff.hpp index ccc6c01..06487fe 100644 --- a/include/mbmff/mbmff.hpp +++ b/include/mbmff/mbmff.hpp @@ -3,16 +3,21 @@ #include "boxes/apcn.hpp" #include "boxes/av1C.hpp" #include "boxes/avc1.hpp" +#include "boxes/avc3.hpp" #include "boxes/avcC.hpp" +#include "boxes/btrt.hpp" +#include "boxes/cdsc.hpp" #include "boxes/co64.hpp" #include "boxes/colr.hpp" #include "boxes/containers.hpp" #include "boxes/ctts.hpp" +#include "boxes/dimg.hpp" #include "boxes/dref.hpp" #include "boxes/elst.hpp" #include "boxes/esds.hpp" #include "boxes/fiel.hpp" #include "boxes/free.hpp" +#include "boxes/frma.hpp" #include "boxes/ftyp.hpp" #include "boxes/gmin.hpp" #include "boxes/hdlr.hpp" @@ -23,17 +28,22 @@ #include "boxes/infe.hpp" #include "boxes/ipma.hpp" #include "boxes/iref.hpp" +#include "boxes/irot.hpp" #include "boxes/ispe.hpp" #include "boxes/load.hpp" #include "boxes/mdat.hpp" #include "boxes/mdhd.hpp" #include "boxes/meta.hpp" #include "boxes/mp4a.hpp" +#include "boxes/mp4v.hpp" #include "boxes/mvhd.hpp" #include "boxes/pasp.hpp" #include "boxes/pitm.hpp" #include "boxes/pixi.hpp" +#include "boxes/sbgp.hpp" +#include "boxes/schm.hpp" #include "boxes/sdtp.hpp" +#include "boxes/sgpd.hpp" #include "boxes/smhd.hpp" #include "boxes/stco.hpp" #include "boxes/stsc.hpp" @@ -41,48 +51,61 @@ #include "boxes/stss.hpp" #include "boxes/stsz.hpp" #include "boxes/stts.hpp" +#include "boxes/thmb.hpp" #include "boxes/tkhd.hpp" #include "boxes/tmcd.hpp" #include "boxes/url.hpp" +#include "boxes/urn.hpp" #include "boxes/vmhd.hpp" #include "boxes/wide.hpp" namespace mbmff { +/// @brief A set of flags, determining the behavior of the box_iterator and parse() function enum class iterator_flags : std::uint32_t { + /// @brief Regular parsing: requires the whole box to be loaded, otherwise returns error_code::need_more_data. none = 0, + + /// @brief Recursive parsing: steps into the box_properties::container. recursive = 1 << 0, + + /// @brief Partial parsing: allows parsing partially loaded boxes, and returns error_code::truncated if the contents + /// are not fully loaded. Still requires headers to be readable, returns error_code::need_more_data otherwise. allow_partial = 1 << 1, }; MBMFF_FLAG_OPERATORS(iterator_flags) -#define MBMFF_ITERATE_USING(name) using name##_box = mbmff::basic_box_view; +#define MBMFF_ITERATE_USING(name, fourcc) using name##_box = mbmff::basic_box_view; MBMFF_ITERATE_BOX_TYPES(MBMFF_ITERATE_USING) -using url_box = mbmff::basic_box_view; #undef MBMFF_ITERATE_USING +/// @brief Returns the box_properties flags for a given box type (full_box, container, etc.). +/// @param[in] type type of the box +/// @returns If the box is not implemented by this library - `box_properties::none` +/// Otherwise, a combination of box_properties, describing the box inline constexpr auto get_box_properties(mbmff::box_type type) noexcept -> mbmff::box_properties { -#define MBMFF_CASE_PROP(name) \ - case mbmff::box_type::name: \ +#define MBMFF_CASE_PROP(name, fourcc) \ + case mbmff::box_type::name: \ return mbmff::basic_box_view::properties; + switch (type) { MBMFF_ITERATE_BOX_TYPES(MBMFF_CASE_PROP) - case mbmff::box_type::url: - return mbmff::basic_box_view::properties; default: return mbmff::box_properties::none; } #undef MBMFF_CASE_PROP } +/// @brief Returns true if the given box type has a specialization registered in the library. +/// @param[in] type type of the box +/// @returns `true` if the box is registered and implemented in the library, `false` otherwise inline constexpr bool is_box_implemented(mbmff::box_type type) noexcept { -#define MBMFF_CASE_IMPL(name) case mbmff::box_type::name: +#define MBMFF_CASE_IMPL(name, fourcc) case mbmff::box_type::name: switch (type) { MBMFF_ITERATE_BOX_TYPES(MBMFF_CASE_IMPL) - case mbmff::box_type::url: return true; default: return false; @@ -90,11 +113,24 @@ inline constexpr bool is_box_implemented(mbmff::box_type type) noexcept #undef MBMFF_CASE_IMPL } +/// @brief Returns true if the box type can contain child boxes (e.g. moov, trak, stbl). +/// @param[in] type type of the box +/// @returns `true` if the box has `box_properties::container` listed in its properties +/// `false` if not implemented or otherwise inline constexpr bool is_container(const mbmff::any_box_view& box) noexcept { return mbmff::has(mbmff::get_box_properties(box.type()), mbmff::box_properties::container); } +/// @brief Parses a single box from the beginning of the byte span. +/// +/// Without `allow_partial`, the full box must be present in `data`; returns +/// `need_more_data` if the box extends beyond the span. With `allow_partial`, +/// returns `truncated` when the box header is valid but the payload is +/// incomplete, allowing the caller to proceed with partial data. +/// @tparam flags Iterator flags (`none`, `allow_partial`). +/// @param[in] data Byte span containing (at least the start of) a box. +/// @returns The parsed box view, or an error. template inline constexpr auto parse(std::span data) noexcept -> mbmff::result { @@ -129,13 +165,11 @@ inline constexpr auto parse(std::span data) noexcept -> mbmff:: mbmff::any_box_view box{{header.size_, header.type_}, payload_span}; auto validate_result = [&]() -> mbmff::result { -#define MBMFF_CASE_VALIDATE(name) \ - case mbmff::box_type::name: \ +#define MBMFF_CASE_VALIDATE(name, fourcc) \ + case mbmff::box_type::name: \ return mbmff::basic_box_view::validate(box); switch (header.type_) { MBMFF_ITERATE_BOX_TYPES(MBMFF_CASE_VALIDATE) - case mbmff::box_type::url: - return mbmff::basic_box_view::validate(box); default: return {box}; } @@ -151,6 +185,10 @@ inline constexpr auto parse(std::span data) noexcept -> mbmff:: return validate_result; } +/// @brief Casts a generic any_box_view to a typed basic_box_view. Returns an empty view on type mismatch. +/// @tparam Box The target box type. +/// @param[in] box The generic box view. +/// @returns A typed box view, or an empty view if the type doesn't match. template constexpr auto box_cast(const mbmff::any_box_view& box) noexcept -> mbmff::basic_box_view { @@ -160,6 +198,12 @@ constexpr auto box_cast(const mbmff::any_box_view& box) noexcept -> mbmff::basic return static_cast>(box); } +/// @brief Forward iterator over a sequence of ISOBMFF boxes in a byte span. +/// +/// Iterates over top-level boxes. With the `recursive` flag, descends into +/// container boxes (yielding their children instead). With `allow_partial`, +/// continues past truncated boxes. +/// @tparam flags Iterator flags (`none`, `allow_partial`, `recursive`). template struct box_iterator { using iterator_category = std::forward_iterator_tag; @@ -169,30 +213,53 @@ struct box_iterator { private: std::span remaining_; + const std::byte* original_start_ = nullptr; public: constexpr box_iterator() noexcept = default; + /// @brief Constructs an iterator over a byte span. + /// @param[in] data The span of bytes to iterate over. constexpr explicit box_iterator(std::span data) noexcept : remaining_(data) + , original_start_(data.data()) {} public: + /// @brief Returns an iterator to the beginning of the sequence. constexpr auto begin() const noexcept -> iterator { return *this; } + /// @brief Returns a sentinel iterator for end-of-sequence. constexpr auto end() const noexcept -> iterator { return {}; } + /// @brief Parses the current box without advancing the iterator. + /// @returns The parsed box or an error. constexpr auto try_get() const noexcept -> mbmff::result { return mbmff::parse(remaining_); } + /// @brief Dereferences the iterator (equivalent to `try_get()`). constexpr auto operator*() const noexcept -> mbmff::result { return try_get(); } + /// @brief Returns the number of bytes consumed from the original data so far. + constexpr auto offset() const noexcept -> std::size_t + { + if (original_start_ == nullptr) { + return 0; + } + return static_cast(remaining_.data() - original_start_); + } + /// @brief Returns the remaining unprocessed span (bytes yet to be iterated). + constexpr auto remaining() const noexcept -> std::span + { + return remaining_; + } + /// @brief Advances the iterator to the next box (pre-increment). constexpr auto operator++() noexcept -> iterator& { if (remaining_.empty()) { @@ -228,12 +295,14 @@ struct box_iterator { } return *this; } + /// @brief Advances the iterator to the next box (post-increment). constexpr auto operator++(int) noexcept -> iterator { auto tmp = *this; ++(*this); return tmp; } + /// @brief Compares two iterators for equality (both exhausted, or same position). constexpr bool operator==(const iterator& other) const noexcept { if (remaining_.empty() && other.remaining_.empty()) { @@ -243,7 +312,10 @@ struct box_iterator { } }; +/// @brief Recursive box iterator: descends into container boxes' children. using recursive_box_iterator = mbmff::box_iterator; + +/// @brief Partial box iterator: allows parsing of partially loaded boxes. using partial_box_iterator = mbmff::box_iterator; #ifdef MBMFF_ENABLE_CONSTEXPR_TEST @@ -252,11 +324,10 @@ template concept box_not_implemented = requires { mbmff::basic_box_view::not_implemented; }; // Test if all boxes are implemented -# define MBMFF_TEST_ALL_IMPLEMENTED(name) \ +# define MBMFF_TEST_ALL_IMPLEMENTED(name, fourcc) \ static_assert(!box_not_implemented, "Box type " #name " is not implemented."); MBMFF_ITERATE_BOX_TYPES(MBMFF_TEST_ALL_IMPLEMENTED) -static_assert(!box_not_implemented, "Box type url is not implemented."); # undef MBMFF_TEST_ALL_IMPLEMENTED // test if container property is correctly detected diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 03c9e64..f6af95d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,9 +1,23 @@ project(mbmff-test) +set(MBMFF_TEST_CXX_STANDARD 23 CACHE STRING "C++ standard for the test executable") + add_executable(mbmff-test entry_main.cpp) set_target_properties(mbmff-test PROPERTIES - CXX_STANDARD 23 + CXX_STANDARD ${MBMFF_TEST_CXX_STANDARD} CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF ) target_link_libraries(mbmff-test PRIVATE mbmff) + +add_executable(mbmff-string-test string_tests.cpp) +set_target_properties(mbmff-string-test PROPERTIES + CXX_STANDARD ${MBMFF_TEST_CXX_STANDARD} + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF +) +target_link_libraries(mbmff-string-test PRIVATE mbmff) + +if (MBMFF_ENABLE_CONSTEXPR_TEST) + target_compile_definitions(mbmff-test PRIVATE MBMFF_ENABLE_CONSTEXPR_TEST) +endif() diff --git a/test/assets/sample.heic b/test/assets/sample.heic new file mode 100644 index 0000000..c1460f7 Binary files /dev/null and b/test/assets/sample.heic differ diff --git a/test/formatters.hpp b/test/formatters.hpp index 2a75eb8..1df7652 100644 --- a/test/formatters.hpp +++ b/test/formatters.hpp @@ -571,10 +571,12 @@ template <> struct std::formatter : std::formatter { auto format(const mbmff::url_box& box, std::format_context& ctx) const { - return std::formatter::format( - std::format(" size=\"{}\" version=\"{}\"", box.size_, box.version()), - ctx - ); + auto v = box.value(); + auto out = std::format(" size=\"{}\" version=\"{}\"", box.size_, box.version()); + if (!v.location.empty()) { + out += std::format(" location=\"{}\"", v.location); + } + return std::formatter::format(out, ctx); } }; @@ -634,6 +636,39 @@ struct std::formatter : std::formatter { } }; +//------------------------------------------------------------------------------------------------------------ +// mp4v +template <> +struct std::formatter : std::formatter { + auto format(const mbmff::mp4v_box& box, std::format_context& ctx) const + { + auto v = box.value(); + std::string out = std::format( + " size=\"{}\" width=\"{}\" height=\"{}\"" + " horizresolution=\"{:.2f}\" vertresolution=\"{:.2f}\"" + " frame_count=\"{}\" depth=\"{}\"", + box.size_, + v.width, + v.height, + static_cast(v.horizresolution) / 65536.0, + static_cast(v.vertresolution) / 65536.0, + v.frame_count, + v.depth + ); + if (!v.compressorname.empty()) { + auto len = static_cast(v.compressorname[0]); + if (len > 31) { + len = 31; + } + std::string name(reinterpret_cast(v.compressorname.data() + 1), len); + if (!name.empty()) { + out += std::format(" name=\"{}\"", name); + } + } + return std::formatter::format(out, ctx); + } +}; + //------------------------------------------------------------------------------------------------------------ // mp4a template <> @@ -987,6 +1022,198 @@ struct std::formatter : std::formatter { } }; +//------------------------------------------------------------------------------------------------------------ +// btrt +template <> +struct std::formatter : std::formatter { + auto format(const mbmff::btrt_box& box, std::format_context& ctx) const + { + auto v = box.value(); + return std::formatter::format( + std::format(" size=\"{}\" buffer_size=\"{}\" max_bitrate=\"{}\" avg_bitrate=\"{}\"", + box.size_, v.buffer_size_db, v.max_bitrate, v.avg_bitrate), + ctx + ); + } +}; + +//------------------------------------------------------------------------------------------------------------ +// sbgp +template <> +struct std::formatter : std::formatter { + auto format(const mbmff::sbgp_box& box, std::format_context& ctx) const + { + auto v = box.value(); + auto type = mbmff::fourcc_string::from_uint32(v.grouping_type); + auto entries = std::string{}; + auto n = (std::min)(std::size_t{3}, static_cast(v.entry_count)); + for (std::size_t i = 0; i < n; ++i) { + auto e = v[i]; + entries += std::format(" [{}=\"{}\" idx=\"{}\"]", i, e.sample_count, e.group_description_index); + } + return std::formatter::format( + std::format(" size=\"{}\" version=\"{}\" grouping_type=\"{}\"" + " grouping_type_parameter=\"{}\" entry_count=\"{}\"{}", + box.size_, box.version(), type.view(), + v.grouping_type_parameter, v.entry_count, entries), + ctx + ); + } +}; + +//------------------------------------------------------------------------------------------------------------ +// sgpd +template <> +struct std::formatter : std::formatter { + auto format(const mbmff::sgpd_box& box, std::format_context& ctx) const + { + auto v = box.value(); + auto type = mbmff::fourcc_string::from_uint32(v.grouping_type); + auto entries = std::string{}; + auto n = (std::min)(std::size_t{3}, static_cast(v.entry_count)); + for (std::size_t i = 0; i < n; ++i) { + auto d = v.entry_data(i); + auto hex = std::string{}; + for (auto b : d) { + hex += std::format("{:02x}", static_cast(b)); + } + entries += std::format(" [{}={}]", i, hex); + } + return std::formatter::format( + std::format(" size=\"{}\" version=\"{}\" grouping_type=\"{}\"" + " default_length=\"{}\"" + " default_sample_description_index=\"{}\" entry_count=\"{}\"{}", + box.size_, box.version(), type.view(), + v.default_length, + v.default_sample_description_index, v.entry_count, entries), + ctx + ); + } +}; + +//------------------------------------------------------------------------------------------------------------ +// avc3 (same layout as avc1) +template <> +struct std::formatter : std::formatter { + auto format(const mbmff::avc3_box& box, std::format_context& ctx) const + { + auto v = box.value(); + std::string out = std::format( + " size=\"{}\" width=\"{}\" height=\"{}\"" + " horizresolution=\"{:.2f}\" vertresolution=\"{:.2f}\"" + " frame_count=\"{}\" depth=\"{}\"", + box.size_, v.width, v.height, + static_cast(v.horizresolution) / 65536.0, + static_cast(v.vertresolution) / 65536.0, + v.frame_count, v.depth + ); + if (!v.compressorname.empty()) { + auto len = static_cast(v.compressorname[0]); + if (len > 31) len = 31; + std::string name(reinterpret_cast(v.compressorname.data() + 1), len); + if (!name.empty()) out += std::format(" name=\"{}\"", name); + } + return std::formatter::format(out, ctx); + } +}; + +//------------------------------------------------------------------------------------------------------------ +// cdsc +template <> +struct std::formatter : std::formatter { + auto format(const mbmff::cdsc_box& box, std::format_context& ctx) const + { + auto v = box.value(); + auto out = std::format(" size=\"{}\"", box.size_); + if (!v.value.empty()) { + auto nul = v.value.find('\0'); + auto s = (nul != std::string_view::npos) ? v.value.substr(0, nul) : v.value; + if (!s.empty()) out += std::format(" value=\"{}\"", s); + } + return std::formatter::format(out, ctx); + } +}; + +//------------------------------------------------------------------------------------------------------------ +// dimg +template <> +struct std::formatter : std::formatter { + auto format(const mbmff::dimg_box& box, std::format_context& ctx) const + { + return std::formatter::format(std::format(" size=\"{}\"", box.size_), ctx); + } +}; + +//------------------------------------------------------------------------------------------------------------ +// frma +template <> +struct std::formatter : std::formatter { + auto format(const mbmff::frma_box& box, std::format_context& ctx) const + { + auto v = box.value(); + auto fc = mbmff::fourcc_string::from_uint32(v.data_format); + return std::formatter::format( + std::format(" size=\"{}\" data_format=\"{}\"", box.size_, fc.view()), ctx + ); + } +}; + +//------------------------------------------------------------------------------------------------------------ +// irot +template <> +struct std::formatter : std::formatter { + auto format(const mbmff::irot_box& box, std::format_context& ctx) const + { + auto v = box.value(); + return std::formatter::format( + std::format(" size=\"{}\" angle=\"{}\"", box.size_, v.angle), ctx + ); + } +}; + +//------------------------------------------------------------------------------------------------------------ +// schm +template <> +struct std::formatter : std::formatter { + auto format(const mbmff::schm_box& box, std::format_context& ctx) const + { + auto v = box.value(); + auto fc = mbmff::fourcc_string::from_uint32(v.scheme_type); + auto out = std::format( + " size=\"{}\" version=\"{}\" scheme_type=\"{}\" scheme_version=\"{}\"", + box.size_, box.version(), fc.view(), v.scheme_version + ); + if (!v.scheme_uri.empty()) { + out += std::format(" scheme_uri=\"{}\"", v.scheme_uri); + } + return std::formatter::format(out, ctx); + } +}; + +//------------------------------------------------------------------------------------------------------------ +// thmb +template <> +struct std::formatter : std::formatter { + auto format(const mbmff::thmb_box& box, std::format_context& ctx) const + { + return std::formatter::format(std::format(" size=\"{}\"", box.size_), ctx); + } +}; + +//------------------------------------------------------------------------------------------------------------ +// urn +template <> +struct std::formatter : std::formatter { + auto format(const mbmff::urn_box& box, std::format_context& ctx) const + { + auto v = box.value(); + auto out = std::format(" size=\"{}\"", box.size_); + if (!v.name.empty()) out += std::format(" name=\"{}\"", v.name); + if (!v.location.empty()) out += std::format(" location=\"{}\"", v.location); + return std::formatter::format(out, ctx); + } +}; + //------------------------------------------------------------------------------------------------------------ // Display fourcc as string if printable, otherwise as hex static auto display_fourcc(const mbmff::fourcc_string& fc) -> std::string @@ -1054,6 +1281,9 @@ static void print_box(const mbmff::any_box_view& box, std::size_t depth) case mbmff::box_type::avcC: std::cout << std::format("{}", mbmff::box_cast(box)); break; + case mbmff::box_type::btrt: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; case mbmff::box_type::avc1: std::cout << std::format("{}", mbmff::box_cast(box)); break; @@ -1117,6 +1347,9 @@ static void print_box(const mbmff::any_box_view& box, std::size_t depth) case mbmff::box_type::tkhd: std::cout << std::format("{}", mbmff::box_cast(box)); break; + case mbmff::box_type::mp4v: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; case mbmff::box_type::mp4a: std::cout << std::format("{}", mbmff::box_cast(box)); break; @@ -1147,6 +1380,12 @@ static void print_box(const mbmff::any_box_view& box, std::size_t depth) case mbmff::box_type::stts: std::cout << std::format("{}", mbmff::box_cast(box)); break; + case mbmff::box_type::sbgp: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + case mbmff::box_type::sgpd: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; case mbmff::box_type::sdtp: std::cout << std::format("{}", mbmff::box_cast(box)); break; @@ -1171,6 +1410,30 @@ static void print_box(const mbmff::any_box_view& box, std::size_t depth) case mbmff::box_type::url: std::cout << std::format("{}", mbmff::box_cast(box)); break; + case mbmff::box_type::avc3: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + case mbmff::box_type::cdsc: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + case mbmff::box_type::dimg: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + case mbmff::box_type::frma: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + case mbmff::box_type::irot: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + case mbmff::box_type::schm: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + case mbmff::box_type::thmb: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; + case mbmff::box_type::urn: + std::cout << std::format("{}", mbmff::box_cast(box)); + break; default: std::cout << " size=\"" << box.size_ << '"'; break; diff --git a/test/string_tests.cpp b/test/string_tests.cpp new file mode 100644 index 0000000..279129c --- /dev/null +++ b/test/string_tests.cpp @@ -0,0 +1,184 @@ +#include +#include +#include +#include + +// ----------------------------------------------------------------------- +// Helpers +// ----------------------------------------------------------------------- +static int failed = 0; + +static void check(bool cond, const char* name, const char* file, int line) +{ + if (!cond) { + std::fprintf(stderr, "FAIL: %s (%s:%d)\n", name, file, line); + ++failed; + } else { + std::fprintf(stdout, "ok: %s\n", name); + } +} + +#define TEST(name, expr) check(expr, name, __FILE__, __LINE__) + +// ----------------------------------------------------------------------- +// URL box tests +// ----------------------------------------------------------------------- +static void test_url() +{ + // URL with location string + { + constexpr std::array data{ + std::byte{0x00}, std::byte{0x00}, std::byte{0x00}, std::byte{0x00}, + std::byte{'t'}, std::byte{'e'}, std::byte{'s'}, std::byte{'t'}, + std::byte{'_'}, std::byte{'u'}, std::byte{'r'}, std::byte{'l'}, + std::byte{0x00}, + }; + mbmff::basic_box_view box; + box.version_ = 0; + box.payload = std::span(data).subspan(4); + auto v = box.value(); + TEST("url location", v.location == "test_url"); + } + + // URL with flag 0x000001 — no location + { + mbmff::basic_box_view box; + box.version_ = 0; + box.flags_[2] = std::uint8_t{1}; + box.payload = {}; + auto v = box.value(); + TEST("url flag_no_location", v.location.empty()); + } + + // URL empty payload (no flag) + { + mbmff::basic_box_view box; + box.version_ = 0; + box.payload = {}; + auto v = box.value(); + TEST("url empty", v.location.empty()); + } +} + +// ----------------------------------------------------------------------- +// URN box tests +// ----------------------------------------------------------------------- +static void test_urn() +{ + // URN with name and location + { + constexpr std::array data{ + std::byte{0x00}, std::byte{0x00}, std::byte{0x00}, std::byte{0x00}, + std::byte{'i'}, std::byte{'s'}, std::byte{'o'}, std::byte{0x00}, + std::byte{'h'}, std::byte{'t'}, std::byte{'t'}, std::byte{'p'}, + std::byte{':'}, std::byte{'/'}, std::byte{'/'}, std::byte{0x00}, + }; + mbmff::basic_box_view box; + box.version_ = 0; + box.payload = std::span(data).subspan(4); + auto v = box.value(); + TEST("urn name_location", v.name == "iso" && v.location == "http://"); + } + + // URN with name only + { + constexpr std::array data{ + std::byte{0x00}, std::byte{0x00}, std::byte{0x00}, std::byte{0x00}, + std::byte{'i'}, std::byte{'s'}, std::byte{'o'}, std::byte{0x00}, + std::byte{0x00}, + }; + mbmff::basic_box_view box; + box.version_ = 0; + box.payload = std::span(data).subspan(4); + auto v = box.value(); + TEST("urn name_only", v.name == "iso" && v.location.empty()); + } + + // URN empty payload + { + mbmff::basic_box_view box; + box.version_ = 0; + box.payload = {}; + auto v = box.value(); + TEST("urn empty", v.name.empty() && v.location.empty()); + } +} + +// ----------------------------------------------------------------------- +// SCHM box tests +// ----------------------------------------------------------------------- +static void test_schm() +{ + // SCHM without scheme_uri + { + constexpr std::array data{ + std::byte{0x00}, std::byte{0x00}, std::byte{0x00}, std::byte{0x00}, + std::byte{0x00}, std::byte{0x00}, std::byte{0x00}, std::byte{0x01}, + std::byte{0x00}, std::byte{0x00}, std::byte{0x00}, std::byte{0x02}, + }; + mbmff::basic_box_view box; + box.version_ = 0; + box.payload = std::span(data); + auto v = box.value(); + TEST("schm no_uri", v.scheme_type == 1 && v.scheme_version == 2 && v.scheme_uri.empty()); + } + + // SCHM with scheme_uri + { + constexpr std::array data{ + std::byte{0x00}, std::byte{0x00}, std::byte{0x00}, std::byte{0x00}, + std::byte{0x00}, std::byte{0x00}, std::byte{0x00}, std::byte{0x01}, + std::byte{0x00}, std::byte{0x00}, std::byte{0x00}, std::byte{0x02}, + std::byte{'m'}, std::byte{'y'}, std::byte{'_'}, std::byte{'u'}, + std::byte{'r'}, std::byte{'i'}, std::byte{0x00}, + }; + mbmff::basic_box_view box; + box.version_ = 0; + box.flags_[2] = std::uint8_t{1}; + box.payload = std::span(data); + auto v = box.value(); + TEST("schm with_uri", v.scheme_type == 1 && v.scheme_version == 2 && v.scheme_uri == "my_uri"); + } +} + +// ----------------------------------------------------------------------- +// CDSC box tests +// ----------------------------------------------------------------------- +static void test_cdsc() +{ + // CDSC with data + { + constexpr std::array data{ + std::byte{'h'}, std::byte{'e'}, std::byte{'l'}, std::byte{'l'}, + std::byte{'o'}, std::byte{0x00}, std::byte{'x'}, std::byte{'y'}, + }; + mbmff::basic_box_view box; + box.payload = std::span(data); + auto v = box.value(); + TEST("cdsc value", v.value.size() == 8 && v.value[0] == 'h' && v.value[5] == '\0'); + } + + // CDSC empty payload + { + mbmff::basic_box_view box; + box.payload = {}; + auto v = box.value(); + TEST("cdsc empty", v.value.empty()); + } +} + +// ----------------------------------------------------------------------- +int main() +{ + test_url(); + test_urn(); + test_schm(); + test_cdsc(); + + if (failed) { + std::fprintf(stderr, "\n%d test(s) FAILED\n", failed); + return 1; + } + std::fprintf(stdout, "\nAll tests passed.\n"); + return 0; +}