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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
42 changes: 42 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -360,4 +360,8 @@ MigrationBackup/
.ionide/

# Fody - auto-generated XML schema
FodyWeavers.xsd
FodyWeavers.xsd

ISOBMFF-master/
include/mbmff/ignored/
build/
53 changes: 43 additions & 10 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -15,22 +42,28 @@ 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
NAMESPACE mbmff::
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()
106 changes: 57 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 <mbmff/mbmff.hpp>

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<char> file_data((std::istreambuf_iterator<char>(avif_file)), std::istreambuf_iterator<char>());
std::span<const std::byte> file_data_span(reinterpret_cast<const std::byte*>(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 <fstream>
#include <iostream>
#include <vector>

int main(int argc, char* argv[])
{
std::ifstream file(argv[1], std::ios::binary);
std::vector<char> data((std::istreambuf_iterator<char>(file)), {});

for (const auto& result : mbmff::box_iterator(
std::span(reinterpret_cast<const std::byte*>(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<mbmff::box_type::ftyp>(box).value().major_brand.view() << '\n';
break;
case mbmff::box_type::hdlr:
std::cout << "hdlr: " << mbmff::box_cast<mbmff::box_type::hdlr>(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<mbmff::box_type::ftyp>(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 `<mbmff/mbmff.hpp>` |
| **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).
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.1.0
8 changes: 3 additions & 5 deletions conanfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,28 @@ 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()

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 = []

Expand Down
Loading
Loading