From af058ed07194e51559e3a3738897f17bb78a0b22 Mon Sep 17 00:00:00 2001 From: Rainbaby Date: Tue, 1 Nov 2022 13:33:16 +0800 Subject: [PATCH] Initial commit --- .github/workflows/ci.yml | 36 + .github/workflows/release.yml | 96 +++ .gitignore | 1 + Cargo.lock | 1025 +++++++++++++++++++++++ Cargo.toml | 29 + LICENSE | 21 + README.md | 76 ++ build.rs | 11 + rust-toolchain.toml | 2 + src/commands/convert.rs | 69 ++ src/commands/mod.rs | 14 + src/functions/convert.rs | 336 ++++++++ src/functions/mod.rs | 3 + src/main.rs | 28 + src/metadata/cmv29/display.rs | 43 + src/metadata/cmv29/frame.rs | 14 + src/metadata/cmv29/mod.rs | 140 ++++ src/metadata/cmv29/shot.rs | 77 ++ src/metadata/cmv29/track.rs | 103 +++ src/metadata/cmv40/display.rs | 67 ++ src/metadata/cmv40/frame.rs | 40 + src/metadata/cmv40/mod.rs | 178 ++++ src/metadata/cmv40/shot.rs | 208 +++++ src/metadata/cmv40/track.rs | 207 +++++ src/metadata/display/characteristics.rs | 264 ++++++ src/metadata/display/chromaticity.rs | 34 + src/metadata/display/mod.rs | 89 ++ src/metadata/display/primary.rs | 128 +++ src/metadata/levels/level1.rs | 41 + src/metadata/levels/level11.rs | 38 + src/metadata/levels/level2.rs | 96 +++ src/metadata/levels/level254.rs | 32 + src/metadata/levels/level3.rs | 22 + src/metadata/levels/level5.rs | 86 ++ src/metadata/levels/level6.rs | 31 + src/metadata/levels/level8.rs | 66 ++ src/metadata/levels/level9.rs | 32 + src/metadata/levels/mod.rs | 171 ++++ src/metadata/mod.rs | 349 ++++++++ 39 files changed, 4303 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build.rs create mode 100644 rust-toolchain.toml create mode 100644 src/commands/convert.rs create mode 100644 src/commands/mod.rs create mode 100644 src/functions/convert.rs create mode 100644 src/functions/mod.rs create mode 100644 src/main.rs create mode 100644 src/metadata/cmv29/display.rs create mode 100644 src/metadata/cmv29/frame.rs create mode 100644 src/metadata/cmv29/mod.rs create mode 100644 src/metadata/cmv29/shot.rs create mode 100644 src/metadata/cmv29/track.rs create mode 100644 src/metadata/cmv40/display.rs create mode 100644 src/metadata/cmv40/frame.rs create mode 100644 src/metadata/cmv40/mod.rs create mode 100644 src/metadata/cmv40/shot.rs create mode 100644 src/metadata/cmv40/track.rs create mode 100644 src/metadata/display/characteristics.rs create mode 100644 src/metadata/display/chromaticity.rs create mode 100644 src/metadata/display/mod.rs create mode 100644 src/metadata/display/primary.rs create mode 100644 src/metadata/levels/level1.rs create mode 100644 src/metadata/levels/level11.rs create mode 100644 src/metadata/levels/level2.rs create mode 100644 src/metadata/levels/level254.rs create mode 100644 src/metadata/levels/level3.rs create mode 100644 src/metadata/levels/level5.rs create mode 100644 src/metadata/levels/level6.rs create mode 100644 src/metadata/levels/level8.rs create mode 100644 src/metadata/levels/level9.rs create mode 100644 src/metadata/levels/mod.rs create mode 100644 src/metadata/mod.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..aa6c38b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + ci: + name: Check, test, rustfmt and clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install Rust, clippy and rustfmt + uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + + - name: Check + run: | + cargo check --workspace --all-features + +# TODO: Test +# - name: Test +# run: | +# cargo test --workspace --all-features + + - name: Rustfmt + run: | + cargo fmt --all --check + + - name: Clippy + run: | + cargo clippy --workspace --all-features --all-targets --tests -- --deny warnings --verbose diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c4ab10c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,96 @@ +on: + workflow_dispatch: + +name: Artifacts + +env: + RELEASE_BIN: dovi_meta + RELEASE_DIR: artifacts + WINDOWS_TARGET: x86_64-pc-windows-msvc + MACOS_TARGET: x86_64-apple-darwin + LINUX_TARGET: x86_64-unknown-linux-musl + +jobs: + build: + name: Build artifacts + runs-on: ${{ matrix.os }} + strategy: + matrix: + build: [Linux, macOS, Windows] + include: + - build: Linux + os: ubuntu-latest + - build: macOS + os: macos-latest + - build: Windows + os: windows-latest + + steps: + - uses: actions/checkout@v3 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Get the version + shell: bash + run: | + echo "RELEASE_PKG_VERSION=$(cargo pkgid | cut -d# -f2 | cut -d: -f2)" >> $GITHUB_ENV + + - name: Install musl-tools (Linux) + if: matrix.build == 'Linux' + run: | + sudo apt-get update -y + sudo apt-get install musl-tools -y + + - name: Build (Linux) + if: matrix.build == 'Linux' + run: | + rustup target add ${{ env.LINUX_TARGET }} + cargo build --release --target ${{ env.LINUX_TARGET }} + + - name: Build (macOS) + if: matrix.build == 'macOS' + run: cargo build --release + + - name: Build (Windows) + if: matrix.build == 'Windows' + run: cargo build --release + + - name: Install cargo-c (Windows) + if: matrix.build == 'Windows' + run: | + $LINK = "https://github.com/lu-zero/cargo-c/releases/latest/download" + $CARGO_C_FILE = "cargo-c-windows-msvc" + curl -LO "$LINK/$CARGO_C_FILE.zip" + 7z e -y "$CARGO_C_FILE.zip" -o"${env:USERPROFILE}\.cargo\bin" + + - name: Create artifact directory + run: | + mkdir ${{ env.RELEASE_DIR }} + + - name: Create tarball (Linux) + if: matrix.build == 'Linux' + run: | + strip ./target/${{ env.LINUX_TARGET }}/release/${{ env.RELEASE_BIN }} + mv ./target/${{ env.LINUX_TARGET }}/release/${{ env.RELEASE_BIN }} ./${{ env.RELEASE_BIN }} + tar -cvzf ./${{ env.RELEASE_DIR }}/${{ env.RELEASE_BIN }}-${{ env.RELEASE_PKG_VERSION }}-${{ env.LINUX_TARGET }}.tar.gz ./${{ env.RELEASE_BIN }} + + - name: Create zipfile (Windows) + if: matrix.build == 'Windows' + shell: bash + run: | + mv ./target/release/${{ env.RELEASE_BIN }}.exe ./${{ env.RELEASE_BIN }}.exe + 7z a ./${{ env.RELEASE_DIR }}/${{ env.RELEASE_BIN }}-${{ env.RELEASE_PKG_VERSION }}-${{ env.WINDOWS_TARGET }}.zip ./${{ env.RELEASE_BIN }}.exe + + - name: Create zipfile (macOS) + if: matrix.build == 'macOS' + run: | + strip ./target/release/${{ env.RELEASE_BIN }} + mv ./target/release/${{ env.RELEASE_BIN }} ./${{ env.RELEASE_BIN }} + zip -9 ./${{ env.RELEASE_DIR }}/${{ env.RELEASE_BIN }}-${{ env.RELEASE_PKG_VERSION }}-${{ env.MACOS_TARGET }}.zip ./${{ env.RELEASE_BIN }} + + - name: Upload Zip + uses: actions/upload-artifact@v1 + with: + name: ${{ matrix.build }} + path: ./${{ env.RELEASE_DIR }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f7dd981 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1025 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "bitvec_helpers" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3737c8719330551a609e76c47c15d4521161d252f73085ffac23ae9f6aa72e" +dependencies = [ + "anyhow", + "bitvec", + "funty", +] + +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] +name = "cc" +version = "1.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time 0.1.44", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "clap" +version = "4.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335867764ed2de42325fafe6d18b8af74ba97ee0c590fa016f157535b42ab04b" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "once_cell", + "strsim", + "termcolor", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1b0f6422af32d5da0c58e2703320f379216ee70198241c84173a8c5ac28f3" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "crc" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53757d12b596c16c78b83458d732a5d1a17ab3f53f2f7412f6fb57cc8a140ab3" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff" + +[[package]] +name = "cxx" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7d4e43b25d3c994662706a1d4fcfc32aaa6afd287502c111b237093bb23f3a" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84f8829ddc213e2c1368e51a2564c552b65a8cb6a28f31e576270ac81d5e5827" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72537424b474af1460806647c41d4b6d35d09ef7fe031c5c2fa5766047cc56a" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "309e4fb93eed90e1e14bea0da16b209f81813ba9fc7830c20ed151dd7bc0a4d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dolby_vision" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05802139755786548d86819f6ee5fb09ebb9736038f1e88228deb6f255c730bd" +dependencies = [ + "anyhow", + "bitvec", + "bitvec_helpers", + "crc", +] + +[[package]] +name = "dovi_meta" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "clap", + "dolby_vision", + "itertools", + "num-derive", + "num-traits", + "quick-xml", + "serde", + "serde-aux", + "uuid", + "vergen", +] + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "enum-iterator" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a0ac4aeb3a18f92eaf09c6bb9b3ac30ff61ca95514fc58cbead1c9a6bf5401" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "828de45d0ca18782232dfb8f3ea9cc428e8ced380eb26a520baaacfc70de39ce" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getset" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "git2" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0155506aab710a86160ddb504a480d2964d7ab5b9e62419be69e0032bc5931c" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "url", +] + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "io-lifetimes" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e481ccbe3dea62107216d0d1138bb8ad8e5e5c43009a098bd1990272c497b0" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" + +[[package]] +name = "jobserver" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" + +[[package]] +name = "libgit2-sys" +version = "0.13.4+1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0fa6563431ede25f5cc7f6d803c6afbc1c5d3ad3d4925d12c882bf2b526f5d1" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "libz-sys" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" +dependencies = [ + "cc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + +[[package]] +name = "os_str_bytes" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9" + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rustix" +version = "0.35.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985947f9b6423159c4726323f373be0a21bdb514c5af06a849cb3d2dce2d01e8" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "scratch" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" + +[[package]] +name = "serde" +version = "1.0.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-aux" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c79c1a5a310c28bf9f7a4b9bd848553051120d80a5952f993c7eb62f6ed6e4c5" +dependencies = [ + "chrono", + "serde", + "serde_json", +] + +[[package]] +name = "serde_derive" +version = "1.0.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8440c860cf79def6164e4a0a983bcc2305d82419177a0e0c71930d049e3ac5a1" +dependencies = [ + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fab5c8b9980850e06d92ddbe3ab839c062c801f3927c0fb8abd6fc8e918fbca" +dependencies = [ + "itoa", + "libc", + "num_threads", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bb801831d812c562ae7d2bfb531f26e66e4e1f6b17307ba4149c5064710e5b" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83" +dependencies = [ + "getrandom", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vergen" +version = "7.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ba753d713ec3844652ad2cb7eb56bc71e34213a14faddac7852a10ba88f61e" +dependencies = [ + "anyhow", + "cfg-if", + "enum-iterator", + "getset", + "git2", + "rustversion", + "thiserror", + "time 0.3.16", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "wyz" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b31594f29d27036c383b53b59ed3476874d518f0efb151b27a4c275141390e" +dependencies = [ + "tap", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..11662e0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "dovi_meta" +version = "0.1.0" +edition = "2021" +authors = ["Rainbaby"] +rust-version = "1.64.0" +license = "MIT" +build = "build.rs" + +[dependencies] +num-traits = "0.2.15" +num-derive = "0.3.3" + +uuid = { version = "1.1.2", features = ["v4"] } + +dolby_vision = "2.0.0" +# TODO: Use timecode as unique id, as an option. +#vtc = "0.1.9" +chrono = "0.4.22" +serde = { version = "1.0.144", features = ["derive"] } +serde-aux = "4.0.0" +quick-xml = { version = "0.26.0", features = ["serialize"]} + +clap = { version = "4.0.18", features = ["derive", "wrap_help"] } +anyhow = "1.0.62" +itertools = "0.10.3" + +[build-dependencies] +vergen = { version = "7.4.2", default-features = false, features = ["git"] } \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..17a33d2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Rainbaby + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b5df80f --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# **dovi_meta** [![CI](https://github.com/saindriches/dovi_meta/workflows/CI/badge.svg)](https://github.com/saindriches/dovi_meta/actions/workflows/ci.yml) [![Artifacts](https://github.com/saindriches/dovi_meta/workflows/Artifacts/badge.svg)](https://github.com/saindriches/dovi_meta/actions/workflows/release.yml) + +**`dovi_meta`** is a CLI tool for creating Dolby Vision XML metadata from an encoded deliverable with binary metadata. + +## **Building** +### **Toolchain** + +The minimum Rust version to build **`dovi_meta`** is 1.64.0. + +### **Release binary** +To build release binary in `target/release/dovi_meta` run: +```console +cargo build --release +``` + +## Usage +```properties +dovi_meta [OPTIONS] +``` +**To get more detailed options for a subcommand** +```properties +dovi_meta --help +``` + +## All options +- `--help`, `--version` +## All subcommands +Currently, the only available subcommand is **`convert`** + +**More information and detailed examples for the subcommands below.** + + +* ### **convert** + Convert a binary RPU to XML Metadata (DolbyLabsMDF). + * Currently, it should support RPU with any Dolby Vision profile using **PQ** as EOTF. + * Supported XML Version: **CM v2.9** (v2.0.5), **CM v4.0** (v4.0.2 and v5.1.0) + - The output version is determined by input automatically. + + **Arguments** + * `INPUT` Set the input RPU file to use. + - No limitation for RPU file extension. + * `OUTPUT` Set the output XML file location. + - When `OUTPUT` is not set, the output file is `metadata.xml` at current path. + + **Options** + * `-s`, `--size` Set the canvas size. Use `x` as delimiter. + - Default value is `3840x2160` + * `-r`, `--rate` Set the frame rate. Format: integer `NUM` or `NUM/DENOM` + - Default value is `24000/1001` + * `-t`, `--skip` Set the number of frames to be skipped from start + - Default value is `0` + * `-n`, `--count` Set the number of frames to be parsed + + **Flags** + * `-6`, `--use-level6` Use MaxCLL and MaxFALL from RPU, if possible + - It's not a default behavior, as ST.2086 metadata is not required for a Dolby Vision deliverable. + * `-d`, `--drop-per-frame` Drop per-frame metadata in shots + * `-k`, `--keep-offset` Keep the offset of frames when `--skip` is set + + **Example to get metadata for RPU from a 29.97 fps HD video, dropping first 24 frames**: + + ```console + dovi_meta convert RPU.bin metadata.xml --skip 24 --rate 30000/1001 --size 1920x1080 + ``` + The default color space of mastering display is **BT.2020**, the default EOTF is **PQ**. + + The default color space of target display (except the anchor target) is **P3 D65** for CM v2.9 XML, also for CM v4.0 XML when it can't be determined by input. + + +## **Notes** +The current build only support RPU as input. To extract RPU from an HEVC file, see [dovi_tool](https://github.com/quietvoid/dovi_tool) for more info. + + +Build artifacts can be found in the GitHub Actions. +More features may or may not be added in the future. +Please report an issue if you have any question. \ No newline at end of file diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..19619d6 --- /dev/null +++ b/build.rs @@ -0,0 +1,11 @@ +use vergen::{vergen, Config, SemverKind, ShaKind}; + +fn main() { + let mut config = Config::default(); + + *config.git_mut().sha_kind_mut() = ShaKind::Short; + *config.git_mut().semver_kind_mut() = SemverKind::Lightweight; + + // Generate the instructions + vergen(config).unwrap() +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..31578d3 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" \ No newline at end of file diff --git a/src/commands/convert.rs b/src/commands/convert.rs new file mode 100644 index 0000000..7257820 --- /dev/null +++ b/src/commands/convert.rs @@ -0,0 +1,69 @@ +use clap::{Args, ValueHint}; +use std::path::PathBuf; + +#[derive(Args, Debug)] +pub struct ConvertArgs { + #[clap( + help = "Set the input RPU file to use", + value_hint = ValueHint::FilePath + )] + pub input: Option, + + #[clap( + help = "Set the output XML file location", + value_hint = ValueHint::FilePath + )] + pub output: Option, + + #[clap( + short = 's', + long, + default_value = "3840x2160", + use_value_delimiter = true, + // FIXME: Clap bug? values with custom delimiter is parsed as one value + value_delimiter = 'x', + num_args(1..=2), + help = "Set the canvas size" + )] + pub size: Vec, + + #[clap( + short = 'r', + long, + default_value = "24000/1001", + use_value_delimiter = true, + value_delimiter = '/', + num_args(1..=2), + help = "Set the frame rate. Format: integer NUM or NUM/DENOM" + )] + pub rate: Vec, + + #[clap( + short = '6', + long, + help = "Use MaxCLL and MaxFALL from RPU, if possible" + )] + pub use_level6: bool, + + #[clap(short = 'd', long, help = "Drop per-frame metadata in shots")] + pub drop_per_frame: bool, + + #[clap( + short = 't', + long, + default_value = "0", + help = "Set the number of frames to be skipped from start" + )] + pub skip: usize, + + #[clap(short = 'n', long, help = "Set the number of frames to be parsed")] + pub count: Option, + + #[clap( + short = 'k', + long, + requires = "trim", + help = "Keep the offset of frames when --skip is set" + )] + pub keep_offset: bool, +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..134644e --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,14 @@ +pub mod convert; + +// use crate::commands::analyze::AnalyzeArgs; +use crate::commands::convert::ConvertArgs; +use clap::Parser; + +#[derive(Parser, Debug)] +pub enum Command { + #[clap( + about = "Convert a binary RPU to XML Metadata (DolbyLabsMDF)", + arg_required_else_help(true) + )] + Convert(ConvertArgs), +} diff --git a/src/functions/convert.rs b/src/functions/convert.rs new file mode 100644 index 0000000..ac5c1f5 --- /dev/null +++ b/src/functions/convert.rs @@ -0,0 +1,336 @@ +use std::collections::HashMap; +use std::fs::File; +use std::io::{BufWriter, Write}; + +use anyhow::{bail, ensure, Result}; +use dolby_vision::rpu::utils::parse_rpu_file; +use itertools::{Itertools, MinMaxResult}; +use quick_xml::events::Event; +use quick_xml::se::Serializer; +use quick_xml::{Reader, Writer}; +use serde::Serialize; + +use crate::cmv40::{EditRate, Output, Shot, Track}; +use crate::commands::convert::ConvertArgs; +use crate::metadata::levels::Level11; +use crate::metadata::levels::Level5; +use crate::MDFType::{CMV29, CMV40}; +use crate::{cmv40, IntoCMV29, Level254, Level6, UHD_AR, UHD_HEIGHT, UHD_WIDTH, XML_PREFIX}; + +#[derive(Debug, Default)] +pub struct Converter { + frame_index: usize, + // scene_count: usize, + invalid_frame_count: usize, + first_valid_frame_index: Option, + shots: Option>, + last_shot: Shot, + track: Track, + level5: Option, + level11: Option, + level254: Option, +} + +impl Converter { + pub fn convert(args: ConvertArgs) -> Result<()> { + let input = match args.input { + Some(input) => input, + None => bail!("No input file provided."), + }; + + ensure!(args.count != Some(0), "Invalid specified frame count."); + + let canvas = Converter::parse_canvas_ar(args.size)?; + + println!("Parsing RPU file..."); + + let rpus = parse_rpu_file(&input)?; + + let mut count = if let Some(count) = args.count { + if count + args.skip > rpus.len() { + println!("Specified frame count exceeds the end."); + rpus.len() + } else { + count + } + } else { + rpus.len() + }; + + let mut converter = Converter::default(); + + let edit_rate = EditRate::from(args.rate); + edit_rate.validate()?; + + println!("Converting RPU file..."); + + // Parse shot-based and frame-based metadata + for rpu in rpus { + if count > 0 { + if let Some(ref vdr) = rpu.vdr_dm_data { + if converter.frame_index >= args.skip { + let frame_index = converter.frame_index - args.skip; + // TODO: Use real offset if first valid frame index is not 0? + + if converter.first_valid_frame_index.is_none() + || vdr.scene_refresh_flag == 1 + { + match &mut converter.shots { + // Initialize + None => { + converter.shots = Some(Vec::new()); + } + Some(shots) => { + shots.push(converter.last_shot.clone()); + } + } + + converter.last_shot = Shot::with_canvas(vdr, canvas); + converter.last_shot.update_record(Some(frame_index), None); + + // FIXME: Assume input rpu file is valid, + // so only use the first valid frame to get global information we need + if converter.first_valid_frame_index.is_none() { + if converter.invalid_frame_count > 0 { + println!( + "Skipped {} invalid frame(s) from start.", + converter.invalid_frame_count + ); + converter.invalid_frame_count = 0; + } + if args.keep_offset { + converter.last_shot.update_record(None, Some(args.skip)); + converter.frame_index += args.skip; + } + converter.first_valid_frame_index = Some(frame_index); + converter.track = Track::with_single_vdr(vdr); + + if !args.use_level6 { + converter.track.level6 = Some(Level6::default()); + } + + converter.level254 = converter.track.plugin_node.level254.clone(); + + converter.track.edit_rate = if converter.level254.is_none() { + CMV29(edit_rate) + } else { + CMV40(edit_rate) + }; + }; + } else { + converter.last_shot.update_record(None, None); + if !args.drop_per_frame { + converter + .last_shot + .append_metadata(&Shot::with_canvas(vdr, canvas)); + } + } + + count -= 1; + } + + converter.frame_index += 1; + } else { + // Should not happen + if converter.first_valid_frame_index.is_some() { + // Invalid RPU in the middle of sequence, use last valid frame + converter.frame_index += 1; + converter.last_shot.update_record(None, None); + if let Some(ref mut frames) = converter.last_shot.frames { + if let Some(frame) = frames.pop() { + frames.push(frame.clone()); + frames.push(frame); + } + } + + count -= 1; + } + + converter.invalid_frame_count += 1; + } + } + } + + if converter.invalid_frame_count > 0 { + println!( + "Skipped {} invalid frame(s) in the middle, replaced with previous metadata.", + converter.invalid_frame_count + ); + } + + // Push remained shot + if converter.shots.is_none() { + converter.shots = Some(Vec::new()); + } + + if let Some(ref mut shots) = converter.shots { + shots.push(converter.last_shot.clone()); + + // FIXME: THE IMPLEMENTATION FOR LEVEL5 IS WRONG !!! + let mut level5_map = HashMap::new(); + let mut level11_map = HashMap::new(); + + shots.iter().for_each(|shot| { + *level5_map + .entry(&shot.plugin_node.dv_dynamic_data.level5) + .or_insert(0) += 1_usize; + + *level11_map.entry(&shot.plugin_node.level11).or_insert(0) += 1_usize; + }); + + converter.level5 = Some(Self::get_global_ar(level5_map, canvas)); + + // Choose the most common level11 as track-level metadata, + converter.level11 = Self::get_common(level11_map); + + // and remove them in shot-level. + shots.iter_mut().for_each(|shot| { + if shot.plugin_node.dv_dynamic_data.level5 == converter.level5 { + shot.plugin_node.dv_dynamic_data.level5 = None; + }; + + if let Some(ref mut frames) = shot.frames { + frames.iter_mut().for_each(|frame| { + if frame.dv_dynamic_data.level5 == converter.level5 { + frame.dv_dynamic_data.level5 = None; + } + }) + } + + if shot.plugin_node.level11 == converter.level11 { + shot.plugin_node.level11 = None; + }; + }); + } + + converter.track.shots = converter.shots; + converter.track.plugin_node.level11 = converter.level11; + + let output = Output::with_level5(converter.track, converter.level5); + + let md = cmv40::DolbyLabsMDF::with_single_output(output)?; + + let mut serializer_buffer = Vec::new(); + let writer = Writer::new(&mut serializer_buffer); + let mut ser = Serializer::new(writer.into_inner()); + + if converter.level254.is_none() { + println!("CM v2.9 RPU found, saving as v2.0.5 XML..."); + md.into_cmv29().serialize(&mut ser)?; + } else { + println!("CM v4.0 RPU found, saving as v{} XML...", md.version); + md.serialize(&mut ser)?; + } + + let output = if let Some(output) = args.output { + output + } else { + println!("No output file provided, writing to metadata.xml at current path..."); + "./metadata.xml".into() + }; + + let mut output_buffer = BufWriter::new(File::create(output)?); + write!( + output_buffer, + "{}{}", + XML_PREFIX, + Self::prettify_xml(String::from_utf8(serializer_buffer)?) + )?; + + Ok(()) + } + + /// None: Standard UHD + fn parse_canvas_ar(vec: Vec) -> Result> { + ensure!( + vec.len() == 2, + "Invalid canvas size. Use 'x' as delimiter, like 3840x2160" + ); + ensure!(vec[0] != 0 && vec[1] != 0, "Invalid canvas size."); + let canvas = (vec[0], vec[1]); + let canvas = if canvas == (UHD_WIDTH, UHD_HEIGHT) { + None + } else { + Some(canvas) + }; + + // stdout().flush().ok(); + + Ok(canvas) + } + + fn get_global_ar( + map: HashMap<&Option, usize>, + canvas: Option<(usize, usize)>, + ) -> Level5 { + let canvas_ar = match canvas { + Some((width, height)) => width as f32 / height as f32, + None => UHD_AR, + }; + + let minmax = map + .into_iter() + .filter(|(value, _)| value.is_some()) + .map(|(value, _)| value.clone().unwrap()) + .minmax(); + + match minmax { + MinMaxResult::NoElements => Level5::from(canvas_ar), + MinMaxResult::OneElement(b) => b, + MinMaxResult::MinMax(b_min, b_max) => { + let b_canvas = Level5::from(canvas_ar); + // if b_min > b_canvas { + // // all letterbox types are up/bottom + // b_min + // } else if b_max < b_canvas { + // // all letterbox types are left/right + // b_max + // } else { + // // Mixed type, or no letterbox + // b_canvas + // } + b_canvas.clamp(b_min, b_max) + } + } + } + + fn get_common(map: HashMap<&Option, V>) -> Option + where + K: Clone, + V: Copy + Ord, + { + map.into_iter() + .filter(|(value, _)| value.is_some()) + .max_by_key(|&(_, count)| count) + .and_then(|(value, _)| value.clone()) + } + + // https://gist.github.com/lwilli/14fb3178bd9adac3a64edfbc11f42e0d/forks + fn prettify_xml(xml: String) -> String { + let mut buf = Vec::new(); + + let mut reader = Reader::from_str(&xml); + reader.trim_text(true); + + let mut writer = Writer::new_with_indent(Vec::new(), b' ', 2); + + loop { + let ev = reader.read_event_into(&mut buf); + + match ev { + Ok(Event::Eof) => break, + Ok(event) => writer.write_event(event), + Err(e) => panic!("Error at position {}: {:?}", reader.buffer_position(), e), + } + .expect("Failed to parse XML"); + + buf.clear(); + } + + let result = std::str::from_utf8(&writer.into_inner()) + .expect("Failed to convert a slice of bytes to a string slice") + .to_string(); + + result + } +} diff --git a/src/functions/mod.rs b/src/functions/mod.rs new file mode 100644 index 0000000..09ea32e --- /dev/null +++ b/src/functions/mod.rs @@ -0,0 +1,3 @@ +pub use convert::Converter; + +mod convert; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..5e57d83 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,28 @@ +use anyhow::Result; +use clap::Parser; + +use crate::commands::Command; +use crate::functions::Converter; +use crate::levels::*; +use crate::metadata::*; +use crate::Command::Convert; + +mod commands; +mod functions; +mod metadata; + +// Some Clap features are broken, keep it for now. +#[derive(Parser, Debug)] +#[clap(name = env!("CARGO_PKG_NAME"), author = "Rainbaby", about = "CLI tool for creating Dolby Vision XML metadata from an encoded deliverable with binary metadata.", version = env!("VERGEN_GIT_SEMVER_LIGHTWEIGHT"))] +struct Opt { + #[clap(subcommand)] + cmd: Command, +} + +fn main() -> Result<()> { + let opt = Opt::parse(); + + match opt.cmd { + Convert(args) => Converter::convert(args), + } +} diff --git a/src/metadata/cmv29/display.rs b/src/metadata/cmv29/display.rs new file mode 100644 index 0000000..9f2aa2d --- /dev/null +++ b/src/metadata/cmv29/display.rs @@ -0,0 +1,43 @@ +use serde::Serialize; + +use crate::display::Chromaticity; +use crate::{ColorSpace, Eotf, MDFType, Primaries, SignalRange}; + +#[derive(Debug, Serialize)] +pub struct Characteristics { + // 0 + pub level: usize, + #[serde(rename = "MasteringDisplay")] + pub mastering_display: CharacteristicsLegacy, + #[serde(rename = "TargetDisplay")] + #[serde(skip_serializing_if = "Option::is_none")] + pub target_displays: Option>, +} + +#[derive(Debug, Serialize)] +pub struct CharacteristicsLegacy { + // 0 + pub level: usize, + #[serde(rename = "$unflatten=ID")] + pub id: usize, + #[serde(rename = "$unflatten=Name")] + pub name: String, + #[serde(rename = "Primaries")] + pub primaries: Primaries, + #[serde(rename = "$unflatten=WhitePoint")] + pub white_point: MDFType, + #[serde(rename = "$unflatten=PeakBrightness")] + pub peak_brightness: usize, + #[serde(rename = "$unflatten=MinimumBrightness")] + pub minimum_brightness: f32, + #[serde(rename = "$unflatten=DiagonalSize")] + pub diagonal_size: usize, + #[serde(rename = "$unflatten=Encoding")] + pub encoding: Eotf, + #[serde(rename = "$unflatten=BitDepth")] + pub bit_depth: usize, + #[serde(rename = "$unflatten=ColorSpace")] + pub color_space: ColorSpace, + #[serde(rename = "$unflatten=SignalRange")] + pub signal_range: SignalRange, +} diff --git a/src/metadata/cmv29/frame.rs b/src/metadata/cmv29/frame.rs new file mode 100644 index 0000000..a95b15d --- /dev/null +++ b/src/metadata/cmv29/frame.rs @@ -0,0 +1,14 @@ +use serde::Serialize; + +use crate::cmv29::ShotPluginNode; +use crate::UUIDv4; + +#[derive(Debug, Serialize)] +pub struct Frame { + #[serde(rename = "UniqueID")] + pub unique_id: UUIDv4, + #[serde(rename = "$unflatten=EditOffset")] + pub edit_offset: usize, + #[serde(rename = "PluginNode")] + pub plugin_node: ShotPluginNode, +} diff --git a/src/metadata/cmv29/mod.rs b/src/metadata/cmv29/mod.rs new file mode 100644 index 0000000..ab6af84 --- /dev/null +++ b/src/metadata/cmv29/mod.rs @@ -0,0 +1,140 @@ +use serde::Serialize; +use std::array; + +pub use display::*; +pub use frame::*; +pub use shot::*; +pub use track::*; + +use crate::{RevisionHistory, UUIDv4, Version}; + +mod display; +mod frame; +mod shot; +mod track; + +#[derive(Debug, Serialize)] +pub struct DolbyLabsMDF { + pub version: Version, + #[serde(rename = "xmlns:xsd")] + pub xmlns_xsd: String, + #[serde(rename = "xmlns:xsi")] + pub xmlns_xsi: String, + #[serde(rename = "SourceList")] + #[serde(skip_serializing_if = "Option::is_none")] + pub source_list: Option, + #[serde(rename = "RevisionHistory")] + #[serde(skip_serializing_if = "Option::is_none")] + pub revision_history: Option, + #[serde(rename = "Outputs")] + #[serde(skip_serializing_if = "Option::is_none")] + pub outputs: Option, +} + +#[derive(Debug, Serialize)] +pub struct SourceList { + #[serde(rename = "Source")] + #[serde(skip_serializing_if = "Option::is_none")] + pub sources: Option>, +} + +// TODO: Some other fields are available here +#[derive(Debug, Serialize)] +pub struct Source { + #[serde(rename = "type")] + pub type_: String, + #[serde(rename = "UniqueID")] + pub unique_id: UUIDv4, + #[serde(rename = "$unflatten=In")] + pub in_: usize, + #[serde(rename = "$unflatten=Duration")] + pub duration: usize, +} + +#[derive(Debug, Serialize)] +pub struct Outputs { + #[serde(rename = "Output")] + #[serde(skip_serializing_if = "Option::is_none")] + pub outputs: Option>, +} + +impl Outputs { + pub fn get_source_list(&self) -> Option { + if let Some(sources) = self.outputs.as_ref().map(|outputs| { + outputs + .iter() + .filter_map(|output| { + output + .video + .tracks + .last() + .and_then(|track| track.shots.as_ref()) + .and_then(|shots| shots.last()) + .map(|shot| { + let record = shot.record.clone(); + let source = shot.source.clone(); + + let duration = record.in_ + record.duration; + let unique_id = source.parent_id; + + Source { + type_: "Video".to_string(), + unique_id, + in_: 0, + duration, + } + }) + }) + .collect::>() + }) { + let source_list = SourceList { + sources: Some(sources), + }; + + Some(source_list) + } else { + None + } + } +} + +#[derive(Debug, Serialize)] +pub struct Output { + pub name: String, + #[serde(rename = "UniqueID")] + pub unique_id: UUIDv4, + #[serde(rename = "$unflatten=NumberVideoTracks")] + pub number_video_tracks: usize, + #[serde(rename = "$unflatten=NumberAudioTracks")] + pub number_audio_tracks: usize, + #[serde(rename = "$unflatten=CanvasAspectRatio")] + pub canvas_aspect_ratio: f32, + #[serde(rename = "$unflatten=ImageAspectRatio")] + pub image_aspect_ratio: f32, + #[serde(rename = "Video")] + pub video: Video, +} + +#[derive(Debug, Serialize)] +pub struct Video { + #[serde(rename = "Track")] + pub tracks: Vec, +} + +#[derive(Debug, Clone, Copy)] +pub struct AlgorithmVersions([usize; 2]); + +impl Default for AlgorithmVersions { + fn default() -> Self { + Self([2, 1]) + } +} + +impl IntoIterator for AlgorithmVersions { + type Item = usize; + type IntoIter = array::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} diff --git a/src/metadata/cmv29/shot.rs b/src/metadata/cmv29/shot.rs new file mode 100644 index 0000000..5c4a0b5 --- /dev/null +++ b/src/metadata/cmv29/shot.rs @@ -0,0 +1,77 @@ +use serde::Serialize; + +use crate::cmv29::{Frame, Source}; +use crate::cmv40::DVDynamicData; +use crate::{cmv40, IntoCMV29, Level1, Level2, Level5, UUIDv4}; + +#[derive(Debug, Serialize)] +pub struct Shot { + #[serde(rename = "UniqueID")] + pub unique_id: UUIDv4, + #[serde(rename = "Source")] + pub source: ShotSource, + #[serde(rename = "Record")] + pub record: Record, + #[serde(rename = "PluginNode")] + pub plugin_node: ShotPluginNode, + #[serde(rename = "Frame")] + #[serde(skip_serializing_if = "Option::is_none")] + pub frames: Option>, +} + +// CMv2.9 only +#[derive(Debug, Clone, Default, Serialize)] +pub struct ShotSource { + #[serde(rename = "ParentID")] + pub parent_id: UUIDv4, + #[serde(rename = "$unflatten=In")] + pub in_: usize, +} + +impl From for ShotSource { + fn from(s: Source) -> Self { + Self { + parent_id: s.unique_id, + in_: s.in_, + } + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct Record { + #[serde(rename = "$unflatten=In")] + pub in_: usize, + #[serde(rename = "$unflatten=Duration")] + pub duration: usize, +} + +impl From for Record { + fn from(record: cmv40::Record) -> Self { + Self { + in_: record.in_, + duration: record.duration, + } + } +} + +#[derive(Debug, Serialize)] +pub struct ShotPluginNode { + #[serde(rename = "DolbyEDR")] + pub level1: Level1, + #[serde(rename = "DolbyEDR")] + #[serde(skip_serializing_if = "Option::is_none")] + pub level2: Option>, + #[serde(rename = "DolbyEDR")] + #[serde(skip_serializing_if = "Option::is_none")] + pub level5: Option, +} + +impl From for ShotPluginNode { + fn from(data: DVDynamicData) -> Self { + Self { + level1: data.level1.into_cmv29(), + level2: data.level2.into_cmv29(), + level5: data.level5.into_cmv29(), + } + } +} diff --git a/src/metadata/cmv29/track.rs b/src/metadata/cmv29/track.rs new file mode 100644 index 0000000..c384cab --- /dev/null +++ b/src/metadata/cmv29/track.rs @@ -0,0 +1,103 @@ +use serde::Serialize; + +use crate::cmv29::{AlgorithmVersions, Characteristics, Shot}; +use crate::display::Chromaticity; +use crate::{cmv40, ColorSpace, Eotf, IntoCMV29, Level6, MDFType, Primaries, SignalRange, UUIDv4}; + +#[derive(Debug, Serialize)] +pub struct Track { + pub name: String, + #[serde(rename = "UniqueID")] + pub unique_id: UUIDv4, + #[serde(rename = "Rate")] + pub rate: Rate, + #[serde(rename = "ColorEncoding")] + pub color_encoding: ColorEncoding, + #[serde(rename = "Level6")] + #[serde(skip_serializing_if = "Option::is_none")] + pub level6: Option, + #[serde(rename = "PluginNode")] + pub plugin_node: TrackPluginNode, + #[serde(rename = "Shot")] + #[serde(skip_serializing_if = "Option::is_none")] + pub shots: Option>, +} + +#[derive(Debug, Serialize)] +pub struct Rate { + #[serde(rename = "$unflatten=n")] + pub n: usize, + #[serde(rename = "$unflatten=d")] + pub d: usize, +} + +#[derive(Debug, Serialize)] +pub struct ColorEncoding { + #[serde(rename = "Primaries")] + pub primaries: Primaries, + // Format: f32,f32 + #[serde(rename = "$unflatten=WhitePoint")] + pub white_point: MDFType, + #[serde(rename = "$unflatten=PeakBrightness")] + pub peak_brightness: usize, + #[serde(rename = "$unflatten=MinimumBrightness")] + pub minimum_brightness: usize, + #[serde(rename = "$unflatten=Encoding")] + pub encoding: Eotf, + #[serde(rename = "$unflatten=BitDepth")] + pub bit_depth: usize, + #[serde(rename = "$unflatten=ColorSpace")] + pub color_space: ColorSpace, + // FIXME: use usize? + #[serde(rename = "$unflatten=ChromaFormat")] + pub chroma_format: String, + #[serde(rename = "$unflatten=SignalRange")] + pub signal_range: SignalRange, +} + +impl From for ColorEncoding { + fn from(c: cmv40::ColorEncoding) -> Self { + Self { + primaries: c.primaries.into_cmv29(), + white_point: c.white_point.into_cmv29(), + peak_brightness: c.peak_brightness, + minimum_brightness: c.minimum_brightness, + encoding: c.encoding, + // TODO: as an option? + bit_depth: 16, + color_space: c.color_space, + chroma_format: "444".to_string(), + signal_range: SignalRange::Computer, + } + } +} + +#[derive(Debug, Serialize)] +pub struct TrackPluginNode { + #[serde(rename = "DolbyEDR")] + pub dolby_edr: TrackDolbyEDR, +} + +impl From for TrackPluginNode { + fn from(t: cmv40::TrackPluginNode) -> Self { + Self { + dolby_edr: TrackDolbyEDR { + algorithm_versions: Default::default(), + characteristics: Characteristics { + level: 0, + mastering_display: t.dv_global_data.mastering_display.into_cmv29(), + target_displays: t.dv_global_data.target_displays.into_cmv29(), + }, + }, + } + } +} + +#[derive(Debug, Serialize)] +pub struct TrackDolbyEDR { + // Format: usize,usize + #[serde(rename = "$unflatten=AlgorithmVersions")] + pub algorithm_versions: MDFType, + #[serde(rename = "Characteristics")] + pub characteristics: Characteristics, +} diff --git a/src/metadata/cmv40/display.rs b/src/metadata/cmv40/display.rs new file mode 100644 index 0000000..34a67e5 --- /dev/null +++ b/src/metadata/cmv40/display.rs @@ -0,0 +1,67 @@ +use serde::Serialize; + +use crate::cmv29::CharacteristicsLegacy; +use crate::display::Chromaticity; +use crate::MDFType::CMV40; +use crate::{ + display, ApplicationType, ColorSpace, Eotf, IntoCMV29, MDFType, Primaries, SignalRange, +}; + +#[derive(Debug, Clone, Default, Serialize)] +pub struct Characteristics { + #[serde(rename = "$unflatten=ID")] + pub id: usize, + #[serde(rename = "$unflatten=Name")] + pub name: String, + #[serde(rename = "Primaries")] + pub primaries: Primaries, + #[serde(rename = "$unflatten=WhitePoint")] + pub white_point: MDFType, + #[serde(rename = "$unflatten=PeakBrightness")] + pub peak_brightness: usize, + #[serde(rename = "$unflatten=MinimumBrightness")] + pub minimum_brightness: f32, + #[serde(rename = "$unflatten=EOTF")] + pub eotf: Eotf, + #[serde(rename = "$unflatten=DiagonalSize")] + pub diagonal_size: usize, + // Version 5.0.0+ + #[serde(rename = "$unflatten=ApplicationType")] + #[serde(skip_serializing_if = "Option::is_none")] + pub application_type: Option, +} + +impl From for Characteristics { + fn from(d: display::Characteristics) -> Self { + Self { + id: d.id, + name: d.name, + primaries: d.primaries.into(), + white_point: CMV40(d.primaries.white_point), + peak_brightness: d.peak_brightness, + minimum_brightness: d.minimum_brightness, + eotf: d.eotf, + diagonal_size: d.diagonal_size, + application_type: None, + } + } +} + +impl IntoCMV29 for Characteristics { + fn into_cmv29(self) -> CharacteristicsLegacy { + CharacteristicsLegacy { + level: 0, + id: self.id, + name: self.name, + primaries: self.primaries, + white_point: self.white_point.into_cmv29(), + peak_brightness: self.peak_brightness, + minimum_brightness: self.minimum_brightness, + diagonal_size: self.diagonal_size, + encoding: self.eotf, + bit_depth: 16, + color_space: ColorSpace::Rgb, + signal_range: SignalRange::Computer, + } + } +} diff --git a/src/metadata/cmv40/frame.rs b/src/metadata/cmv40/frame.rs new file mode 100644 index 0000000..9da4a9b --- /dev/null +++ b/src/metadata/cmv40/frame.rs @@ -0,0 +1,40 @@ +use serde::Serialize; + +use crate::cmv40::{DVDynamicData, Shot}; +use crate::{cmv29, IntoCMV29, UUIDv4}; + +#[derive(Debug, Clone, Default, Serialize)] +pub struct Frame { + #[serde(rename = "$unflatten=EditOffset")] + pub edit_offset: usize, + #[serde(rename = "DVDynamicData")] + pub dv_dynamic_data: DVDynamicData, +} + +impl Frame { + pub fn with_offset(shot: &Shot, offset: usize) -> Self { + let mut dv_dynamic_data = shot.plugin_node.dv_dynamic_data.clone(); + // Remove Level 9 in per-frame metadata + dv_dynamic_data.level9 = None; + Self { + edit_offset: offset, + dv_dynamic_data, + } + } +} + +impl From<&Shot> for Frame { + fn from(shot: &Shot) -> Self { + Self::with_offset(shot, 0) + } +} + +impl IntoCMV29 for Frame { + fn into_cmv29(self) -> cmv29::Frame { + cmv29::Frame { + unique_id: UUIDv4::new(), + edit_offset: self.edit_offset, + plugin_node: self.dv_dynamic_data.into(), + } + } +} diff --git a/src/metadata/cmv40/mod.rs b/src/metadata/cmv40/mod.rs new file mode 100644 index 0000000..14f2b4a --- /dev/null +++ b/src/metadata/cmv40/mod.rs @@ -0,0 +1,178 @@ +use anyhow::{Context, Result}; +use serde::Serialize; + +pub use display::*; +pub use frame::*; +pub use shot::*; +pub use track::*; + +use crate::XMLVersion::{V402, V510}; +use crate::{ + cmv29, ApplicationType, IntoCMV29, Level5, RevisionHistory, UUIDv4, Version, XMLVersion, + CMV40_MIN_VERSION, UHD_AR, +}; + +mod display; +mod frame; +mod shot; +mod track; + +#[derive(Debug, Serialize)] +pub struct DolbyLabsMDF { + pub xmlns: String, + #[serde(rename = "$unflatten=Version")] + pub version: Version, + #[serde(rename = "RevisionHistory")] + #[serde(skip_serializing_if = "Option::is_none")] + pub revision_history: Option, + #[serde(rename = "Outputs")] + pub outputs: Outputs, +} + +impl DolbyLabsMDF { + pub fn with_single_output(output: Output) -> Result { + let mut output = output; + + let has_level11 = output + .video + .tracks + .first() + .map(|track| track.plugin_node.level11.is_some()) + .context("No track in output.")?; + + let version: Version = match has_level11 { + true => V510, + false => V402, + } + .into(); + + if version > CMV40_MIN_VERSION { + output.video.tracks.iter_mut().for_each(|track| { + track + .plugin_node + .dv_global_data + .mastering_display + .application_type = Some(ApplicationType::All); + + if let Some(ds) = track.plugin_node.dv_global_data.target_displays.as_mut() { + ds.iter_mut() + .for_each(|d| d.application_type = Some(ApplicationType::Home)) + } + }); + } + + Ok(Self { + xmlns: version.get_dolby_xmlns(), + version, + revision_history: Some(RevisionHistory::new()), + outputs: Outputs { + outputs: vec![output], + }, + }) + } +} + +impl IntoCMV29 for DolbyLabsMDF { + fn into_cmv29(self) -> cmv29::DolbyLabsMDF { + let outputs = self.outputs.into_cmv29(); + + cmv29::DolbyLabsMDF { + version: XMLVersion::V205.into(), + xmlns_xsd: "http://www.w3.org/2001/XMLSchema".to_string(), + xmlns_xsi: "http://www.w3.org/2001/XMLSchema-instance".to_string(), + source_list: outputs.get_source_list(), + revision_history: self.revision_history, + outputs: Some(outputs), + } + } +} + +#[derive(Debug, Serialize)] +pub struct Outputs { + #[serde(rename = "Output")] + pub outputs: Vec, +} + +impl IntoCMV29 for Outputs { + fn into_cmv29(self) -> cmv29::Outputs { + cmv29::Outputs { + outputs: Some(self.outputs.into_cmv29()), + } + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct Output { + #[serde(rename = "$unflatten=CompositionName")] + pub composition_name: String, + #[serde(rename = "UniqueID")] + pub unique_id: UUIDv4, + #[serde(rename = "$unflatten=NumberVideoTracks")] + pub number_video_tracks: usize, + #[serde(rename = "$unflatten=CanvasAspectRatio")] + pub canvas_aspect_ratio: f32, + #[serde(rename = "$unflatten=ImageAspectRatio")] + pub image_aspect_ratio: f32, + #[serde(rename = "Video")] + pub video: Video, +} + +impl IntoCMV29 for Output { + fn into_cmv29(self) -> cmv29::Output { + let mut video = self.video.into_cmv29(); + let parent_id = UUIDv4::new(); + video.tracks.iter_mut().for_each(|track| { + track.shots.iter_mut().for_each(|shots| { + shots.iter_mut().for_each(|shot| { + shot.source.parent_id = parent_id.clone(); + }) + }) + }); + + cmv29::Output { + name: self.composition_name, + unique_id: self.unique_id, + number_video_tracks: self.number_video_tracks, + number_audio_tracks: 0, + canvas_aspect_ratio: self.canvas_aspect_ratio, + image_aspect_ratio: self.image_aspect_ratio, + video, + } + } +} + +impl Output { + pub fn with_level5(track: Track, level5: Option) -> Self { + let (canvas_aspect_ratio, image_aspect_ratio) = if let Some(level5) = level5 { + level5.get_ar() + } else { + // Should not happen + (UHD_AR, UHD_AR) + }; + + Self { + composition_name: "Timeline".to_string(), + unique_id: UUIDv4::new(), + number_video_tracks: 1, + canvas_aspect_ratio, + image_aspect_ratio, + video: Video { + tracks: vec![track], + }, + } + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct Video { + #[serde(rename = "Track")] + pub tracks: Vec, +} + +impl IntoCMV29 for Video { + fn into_cmv29(self) -> cmv29::Video { + cmv29::Video { + tracks: self.tracks.into_cmv29(), + } + } +} diff --git a/src/metadata/cmv40/shot.rs b/src/metadata/cmv40/shot.rs new file mode 100644 index 0000000..546b027 --- /dev/null +++ b/src/metadata/cmv40/shot.rs @@ -0,0 +1,208 @@ +use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlock; +use dolby_vision::rpu::vdr_dm_data::VdrDmData; +use serde::Serialize; + +use crate::cmv40::Frame; +use crate::levels::*; +use crate::{cmv29, IntoCMV29, UUIDv4}; + +#[derive(Debug, Clone, Default, Serialize)] +pub struct Shot { + #[serde(rename = "UniqueID")] + pub unique_id: UUIDv4, + #[serde(rename = "Record")] + pub record: Record, + #[serde(rename = "PluginNode")] + pub plugin_node: ShotPluginNode, + #[serde(rename = "Frame")] + #[serde(skip_serializing_if = "Option::is_none")] + pub frames: Option>, +} + +impl Shot { + pub fn update_record(&mut self, index: Option, duration_override: Option) { + match index { + Some(index) => { + self.record.in_ = index; + self.record.duration = 1; + } + None => { + // FIXME: dirty + self.record.duration += 1; + } + } + + if let Some(duration) = duration_override { + self.record.duration = duration + 1; + } + } + + pub fn with_canvas(vdr: &VdrDmData, canvas: Option<(usize, usize)>) -> Self { + Self { + unique_id: UUIDv4::new(), + record: Default::default(), + plugin_node: ShotPluginNode::with_canvas(vdr, canvas), + frames: None, + } + } + + pub fn append_metadata(&mut self, other: &Self) { + match &mut self.frames { + Some(ref mut frames) => { + // Always parse per-frame metadata until next shot + let offset = self.record.duration - 1; + let new_frame = Frame::with_offset(other, offset); + frames.push(new_frame); + } + None => { + if self.plugin_node != other.plugin_node { + self.frames = Some(Vec::new()); + // FIXME: Recursive + self.append_metadata(other); + } + } + } + } +} + +impl From<&VdrDmData> for Shot { + fn from(vdr: &VdrDmData) -> Self { + Self::with_canvas(vdr, None) + } +} + +impl IntoCMV29 for Shot { + fn into_cmv29(self) -> cmv29::Shot { + cmv29::Shot { + unique_id: self.unique_id, + source: cmv29::ShotSource::default(), + record: self.record.into(), + plugin_node: self.plugin_node.dv_dynamic_data.into(), + frames: self.frames.into_cmv29(), + } + } +} + +#[derive(Debug, Clone, Default, PartialEq, Serialize)] +pub struct ShotPluginNode { + #[serde(rename = "DVDynamicData")] + pub dv_dynamic_data: DVDynamicData, + // Version 5.1.0+ + #[serde(rename = "Level11")] + #[serde(skip_serializing_if = "Option::is_none")] + pub level11: Option, +} + +impl ShotPluginNode { + fn with_canvas(vdr: &VdrDmData, canvas: Option<(usize, usize)>) -> Self { + let level11 = vdr.get_block(11).and_then(|b| match b { + ExtMetadataBlock::Level11(b) => Some(Level11::from(b)), + _ => None, + }); + + Self { + dv_dynamic_data: DVDynamicData::with_canvas(vdr, canvas), + level11, + } + } +} + +impl From<&VdrDmData> for ShotPluginNode { + fn from(vdr: &VdrDmData) -> Self { + Self::with_canvas(vdr, None) + } +} + +#[derive(Debug, Clone, Default, PartialEq, Serialize)] +pub struct DVDynamicData { + #[serde(rename = "Level1")] + pub level1: Level1, + #[serde(rename = "Level2")] + #[serde(skip_serializing_if = "Option::is_none")] + pub level2: Option>, + #[serde(rename = "Level3")] + #[serde(skip_serializing_if = "Option::is_none")] + pub level3: Option, + #[serde(rename = "Level5")] + #[serde(skip_serializing_if = "Option::is_none")] + pub level5: Option, + #[serde(rename = "Level8")] + #[serde(skip_serializing_if = "Option::is_none")] + pub level8: Option>, + #[serde(rename = "Level9")] + #[serde(skip_serializing_if = "Option::is_none")] + pub level9: Option, +} + +impl DVDynamicData { + pub fn with_canvas(vdr: &VdrDmData, canvas: Option<(usize, usize)>) -> Self { + let level1 = if let Some(ExtMetadataBlock::Level1(block)) = vdr.get_block(1) { + Level1::from(block) + } else { + Level1::default() + }; + + let mut primary = None; + + let level9 = vdr.get_block(9).and_then(|b| match b { + ExtMetadataBlock::Level9(b) => { + primary = Some(b.source_primary_index as usize); + Some(Level9::from(b)) + } + _ => None, + }); + + let level2 = vdr + .level_blocks_iter(2) + .map(|b| match b { + ExtMetadataBlock::Level2(b) => Some(Level2::with_primary_index(b, primary)), + _ => None, + }) + .collect::>>(); + + let level3 = vdr.get_block(3).and_then(|b| match b { + ExtMetadataBlock::Level3(b) => Some(Level3::from(b)), + _ => None, + }); + + let level5 = vdr.get_block(5).and_then(|b| match b { + ExtMetadataBlock::Level5(b) => match canvas { + Some(canvas) => Some(Level5::with_canvas(b, canvas)), + None => Some(Level5::from(b)), + }, + _ => None, + }); + + let level8 = vdr + .level_blocks_iter(8) + .map(|b| match b { + ExtMetadataBlock::Level8(b) => Some(Level8::from(b)), + _ => None, + }) + .collect::>>(); + + Self { + level1, + level2, + level3, + level5, + level8, + level9, + } + } +} + +impl From<&VdrDmData> for DVDynamicData { + fn from(vdr: &VdrDmData) -> Self { + Self::with_canvas(vdr, None) + } +} + +// TODO: Start duration is 1 +#[derive(Debug, Clone, Default, Serialize)] +pub struct Record { + #[serde(rename = "$unflatten=In")] + pub in_: usize, + #[serde(rename = "$unflatten=Duration")] + pub duration: usize, +} diff --git a/src/metadata/cmv40/track.rs b/src/metadata/cmv40/track.rs new file mode 100644 index 0000000..eb3272e --- /dev/null +++ b/src/metadata/cmv40/track.rs @@ -0,0 +1,207 @@ +use std::array; + +use anyhow::{ensure, Result}; +use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlock; +use dolby_vision::rpu::vdr_dm_data::VdrDmData; +use serde::Serialize; + +use crate::cmv29::Rate; +use crate::cmv40::display::Characteristics; +use crate::cmv40::Shot; +use crate::display::Chromaticity; +use crate::levels::*; +use crate::MDFType::CMV40; +use crate::{cmv29, display, ColorSpace, Eotf, IntoCMV29, MDFType, Primaries, SignalRange, UUIDv4}; + +#[derive(Debug, Clone, Default, Serialize)] +pub struct Track { + #[serde(rename = "$unflatten=TrackName")] + pub track_name: String, + #[serde(rename = "UniqueID")] + pub unique_id: UUIDv4, + #[serde(rename = "$unflatten=EditRate")] + pub edit_rate: MDFType, + #[serde(rename = "ColorEncoding")] + pub color_encoding: ColorEncoding, + #[serde(rename = "Level6")] + #[serde(skip_serializing_if = "Option::is_none")] + pub level6: Option, + #[serde(rename = "PluginNode")] + pub plugin_node: TrackPluginNode, + #[serde(rename = "Shot")] + #[serde(skip_serializing_if = "Option::is_none")] + pub shots: Option>, +} + +impl Track { + pub fn with_single_vdr(vdr: &VdrDmData) -> Self { + let level6 = match vdr.get_block(6) { + Some(ExtMetadataBlock::Level6(b)) => Some(Level6::from(b)), + _ => None, + }; + + Self { + // TODO: as option + track_name: "V1".to_string(), + unique_id: UUIDv4::new(), + edit_rate: CMV40(EditRate::default()), + color_encoding: Default::default(), + level6, + plugin_node: vdr.into(), + shots: None, + } + } +} + +impl IntoCMV29 for Track { + fn into_cmv29(self) -> cmv29::Track { + cmv29::Track { + name: self.track_name, + unique_id: self.unique_id, + rate: self.edit_rate.into_inner().into_cmv29(), + color_encoding: self.color_encoding.into(), + level6: self.level6, + plugin_node: self.plugin_node.into(), + // Source UUID in each shot is not updated yet + shots: self.shots.into_cmv29(), + } + } +} + +#[derive(Clone, Copy, Debug, Serialize)] +pub struct EditRate(pub [usize; 2]); + +impl EditRate { + pub fn validate(&self) -> Result<()> { + ensure!(self.0[0] != 0 && self.0[1] != 0, "Invalid frame rate."); + + Ok(()) + } +} + +impl Default for EditRate { + fn default() -> Self { + // TODO + Self([24000, 1001]) + } +} + +impl From> for EditRate { + fn from(vec: Vec) -> Self { + let mut array = [1; 2]; + + vec.iter().enumerate().for_each(|(i, n)| array[i] = *n); + + Self(array) + } +} + +impl IntoIterator for EditRate { + type Item = usize; + type IntoIter = array::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl IntoCMV29 for EditRate { + fn into_cmv29(self) -> Rate { + Rate { + n: self.0[0], + d: self.0[1], + } + } +} + +#[derive(Debug, Clone, Default, Serialize)] +pub struct TrackPluginNode { + #[serde(rename = "DVGlobalData")] + pub dv_global_data: DVGlobalData, + // Version 5.1.0+ + #[serde(rename = "Level11")] + #[serde(skip_serializing_if = "Option::is_none")] + pub level11: Option, + // For Version 4.0.2+, level254 should not be None. + #[serde(rename = "Level254")] + #[serde(skip_serializing_if = "Option::is_none")] + pub level254: Option, +} + +impl From<&VdrDmData> for TrackPluginNode { + fn from(vdr: &VdrDmData) -> Self { + let level11 = vdr.get_block(11).and_then(|b| match b { + ExtMetadataBlock::Level11(b) => Some(Level11::from(b)), + _ => None, + }); + + let level254 = vdr.get_block(254).and_then(|b| match b { + ExtMetadataBlock::Level254(b) => Some(Level254::from(b)), + _ => None, + }); + + let mastering_display = display::Characteristics::get_source_or_default(vdr).into(); + + let target_displays = display::Characteristics::get_targets(vdr).map(|d| { + d.iter() + .map(|c| Characteristics::from(c.clone())) + .collect::>() + }); + + Self { + dv_global_data: DVGlobalData { + level: 0, + mastering_display, + target_displays, + }, + level11, + level254, + } + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct ColorEncoding { + #[serde(rename = "Primaries")] + pub primaries: Primaries, + #[serde(rename = "$unflatten=WhitePoint")] + pub white_point: MDFType, + #[serde(rename = "$unflatten=PeakBrightness")] + pub peak_brightness: usize, + #[serde(rename = "$unflatten=MinimumBrightness")] + pub minimum_brightness: usize, + #[serde(rename = "$unflatten=Encoding")] + pub encoding: Eotf, + #[serde(rename = "$unflatten=ColorSpace")] + pub color_space: ColorSpace, + #[serde(rename = "$unflatten=SignalRange")] + pub signal_range: SignalRange, +} + +// TODO: Default is BT.2020 PQ, should provide other options +impl Default for ColorEncoding { + fn default() -> Self { + let p = display::Primaries::get_index_primary(2, false).unwrap_or_default(); + + Self { + primaries: p.into(), + white_point: CMV40(p.white_point), + peak_brightness: 10000, + minimum_brightness: 0, + encoding: Eotf::Pq, + color_space: ColorSpace::Rgb, + signal_range: SignalRange::Computer, + } + } +} + +#[derive(Debug, Clone, Default, Serialize)] +pub struct DVGlobalData { + // 0 + pub level: usize, + #[serde(rename = "MasteringDisplay")] + pub mastering_display: Characteristics, + #[serde(rename = "TargetDisplay")] + #[serde(skip_serializing_if = "Option::is_none")] + pub target_displays: Option>, +} diff --git a/src/metadata/display/characteristics.rs b/src/metadata/display/characteristics.rs new file mode 100644 index 0000000..8176620 --- /dev/null +++ b/src/metadata/display/characteristics.rs @@ -0,0 +1,264 @@ +use std::hash::{Hash, Hasher}; +use std::intrinsics::transmute; + +use dolby_vision::rpu::extension_metadata::blocks::{ + ExtMetadataBlock, ExtMetadataBlockInfo, ExtMetadataBlockLevel10, ExtMetadataBlockLevel2, + ExtMetadataBlockLevel8, +}; +use dolby_vision::rpu::vdr_dm_data::VdrDmData; +use itertools::Itertools; + +use crate::display::{PREDEFINED_MASTERING_DISPLAYS, PREDEFINED_TARGET_DISPLAYS, RPU_PQ_MAX}; +use crate::metadata::display::primary::Primaries; +use crate::{display, Eotf}; + +#[derive(Debug, Clone)] +pub struct Characteristics { + pub name: String, + pub id: usize, + pub primary_index: usize, + pub primaries: Primaries, + pub peak_brightness: usize, + pub minimum_brightness: f32, + pub eotf: Eotf, + pub diagonal_size: usize, +} + +impl PartialEq for Characteristics { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.id == other.id + && self.primary_index == other.primary_index + && self.primaries == other.primaries + && self.peak_brightness == other.peak_brightness + && self.minimum_brightness.to_bits() == other.minimum_brightness.to_bits() + && self.eotf == other.eotf + && self.diagonal_size == other.diagonal_size + } +} + +impl Eq for Characteristics {} + +impl Hash for Characteristics { + fn hash(&self, state: &mut H) { + self.name.hash(state); + self.id.hash(state); + self.primary_index.hash(state); + self.primaries.hash(state); + self.peak_brightness.hash(state); + self.minimum_brightness.to_bits().hash(state); + self.eotf.hash(state); + self.diagonal_size.hash(state); + } +} + +impl Characteristics { + pub fn update_name(&mut self) { + let color_model = match self.primary_index { + 0 => "P3, D65", + 1 => "BT.709", + 2 => "BT.2020", + 5 => "P3, DCI", + 9 => "WCG, D65", + _ => "Custom", + }; + + let eotf = match self.eotf { + Eotf::Pq => "ST.2084", + Eotf::Linear => "Linear", + Eotf::GammaBT1886 => "BT.1886", + Eotf::GammaDCI => "Gamma2.6", + Eotf::Gamma22 => "Gamma2.2", + Eotf::Gamma24 => "Gamma2.4", + Eotf::Hlg => "HLG", + }; + + self.name = format!( + "{}-nits, {}, {}, Full", + self.peak_brightness, color_model, eotf + ) + } + + pub fn max_u16_from_rpu_pq_u12(u: u16) -> usize { + match u { + // Common cases + 2081 => 100, + 2851 => 600, + 3079 => 1000, + 3696 => 4000, + _ => { + let n = display::pq2l(u as f32 / RPU_PQ_MAX).round(); + // smooth large values + if n > 500.0 { + (n / 50.0) as usize * 50 + } else { + n as usize + } + } + } + } + + fn min_f32_from_rpu_pq_u12(u: u16) -> f32 { + match u { + // Common cases + 0 => 0.0, + 7 => 0.0001, + 26 => 0.001, + 62 => 0.005, + _ => display::pq2l(u as f32 / RPU_PQ_MAX), + } + } + + fn get_primary_target(block: &ExtMetadataBlockLevel2, primary: Primaries) -> Option { + let max_luminance = Self::max_u16_from_rpu_pq_u12(block.target_max_pq); + + let primary = if let Some(primary) = primary.get_index() { + if max_luminance == 100 { + 1 + } else { + primary + } + } else { + 0 + }; + + Self::get_display(PREDEFINED_TARGET_DISPLAYS, max_luminance, primary) + } + + fn get_target(block: &ExtMetadataBlockLevel8) -> Option { + let index = block.target_display_index as usize; + + PREDEFINED_TARGET_DISPLAYS + .iter() + .find(|d| (**d)[0] == index) + .map(|d| Self::from(*d)) + } + + pub fn get_targets(vdr: &VdrDmData) -> Option> { + let mut targets = Vec::new(); + + let primary = Primaries::from(vdr); + + vdr.level_blocks_iter(10).for_each(|b| { + if let ExtMetadataBlock::Level10(b) = b { + let d = Self::from(b); + targets.push(d); + } + }); + + vdr.level_blocks_iter(8).for_each(|b| { + if let ExtMetadataBlock::Level8(b) = b { + if let Some(d) = Self::get_target(b) { + targets.push(d) + } + } + }); + + vdr.level_blocks_iter(2).for_each(|b| { + if let ExtMetadataBlock::Level2(b) = b { + if let Some(d) = Self::get_primary_target(b, primary) { + targets.push(d) + } + } + }); + + let mut targets = targets + .into_iter() + .unique() + .sorted_by_key(|c| c.id) + .collect::>(); + + if targets.is_empty() { + // 100-nit, BT.709 + targets.push(Self::default()) + } + + Some(targets) + } + + pub fn default_source() -> Self { + Self::from(PREDEFINED_MASTERING_DISPLAYS[0]) + } + + pub fn get_source_or_default(vdr: &VdrDmData) -> Self { + let primary = Primaries::from(vdr).get_index().unwrap_or(0); + + // Prefer level 6 metadata + let max_luminance = match vdr.get_block(6) { + Some(ExtMetadataBlock::Level6(b)) => b.max_display_mastering_luminance as usize, + _ => Characteristics::max_u16_from_rpu_pq_u12(vdr.source_max_pq), + }; + + Self::get_display(PREDEFINED_MASTERING_DISPLAYS, max_luminance, primary) + .unwrap_or_else(Self::default_source) + } + + /*pub fn update_luminance_range_with_l6_block(&mut self, block: &ExtMetadataBlockLevel6) { + self.peak_brightness = block.max_display_mastering_luminance as usize; + self.minimum_brightness = block.min_display_mastering_luminance as f32 / RPU_L6_MIN_FACTOR; + }*/ + + fn get_display(list: &[[usize; 6]], max_luminance: usize, primary: usize) -> Option { + list.iter() + .find(|d| (**d)[2] == max_luminance && (**d)[1] == primary) + .map(|d| Self::from(*d)) + } +} + +impl Default for Characteristics { + fn default() -> Self { + Self::from(PREDEFINED_TARGET_DISPLAYS[0]) + } +} + +impl From<[usize; 6]> for Characteristics { + fn from(input: [usize; 6]) -> Self { + let mut result = Self { + name: String::new(), + id: input[0], + primary_index: input[1], + primaries: Primaries::get_index_primary(input[1], true).unwrap_or_default(), + peak_brightness: input[2], + minimum_brightness: Self::min_f32_from_rpu_pq_u12(input[3] as u16), + // :( + eotf: unsafe { transmute::(input[4]) }, + // TODO + diagonal_size: 42, + }; + + result.update_name(); + result + } +} + +impl From<&ExtMetadataBlockLevel10> for Characteristics { + fn from(block: &ExtMetadataBlockLevel10) -> Self { + let mut result = Self { + id: block.target_display_index as usize, + primary_index: block.target_primary_index as usize, + primaries: match block.bytes_size() { + 21 => Primaries::from([ + block.target_primary_red_x, + block.target_primary_red_y, + block.target_primary_green_x, + block.target_primary_green_y, + block.target_primary_blue_x, + block.target_primary_blue_y, + block.target_primary_white_x, + block.target_primary_white_y, + ]), + 5 => Primaries::get_index_primary(block.target_primary_index as usize, true) + .unwrap_or_default(), + _ => unreachable!(), + }, + peak_brightness: Self::max_u16_from_rpu_pq_u12(block.target_max_pq), + minimum_brightness: Self::min_f32_from_rpu_pq_u12(block.target_min_pq), + eotf: Eotf::Pq, + diagonal_size: 42, + ..Default::default() + }; + + result.update_name(); + result + } +} diff --git a/src/metadata/display/chromaticity.rs b/src/metadata/display/chromaticity.rs new file mode 100644 index 0000000..d699490 --- /dev/null +++ b/src/metadata/display/chromaticity.rs @@ -0,0 +1,34 @@ +use std::array; +use std::hash::{Hash, Hasher}; + +use crate::display::CHROMATICITY_EPSILON; + +#[derive(Clone, Copy, Default, Debug)] +pub struct Chromaticity(pub(crate) [f32; 2]); + +impl PartialEq for Chromaticity { + fn eq(&self, other: &Self) -> bool { + let dx = (self.0[0] - other.0[0]).abs(); + let dy = (self.0[1] - other.0[1]).abs(); + + dx <= CHROMATICITY_EPSILON && dy <= CHROMATICITY_EPSILON + } +} + +impl Eq for Chromaticity {} + +impl Hash for Chromaticity { + fn hash(&self, state: &mut H) { + self.0[0].to_bits().hash(state); + self.0[1].to_bits().hash(state); + } +} + +impl IntoIterator for Chromaticity { + type Item = f32; + type IntoIter = array::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} diff --git a/src/metadata/display/mod.rs b/src/metadata/display/mod.rs new file mode 100644 index 0000000..1177aa0 --- /dev/null +++ b/src/metadata/display/mod.rs @@ -0,0 +1,89 @@ +pub use characteristics::Characteristics; +pub use chromaticity::Chromaticity; +pub use primary::Primaries; + +mod characteristics; +mod chromaticity; +mod primary; + +pub const RPU_PQ_MAX: f32 = 4095.0; +pub const CHROMATICITY_EPSILON: f32 = 1.0 / 32767.0; +// pub const RPU_L6_MIN_FACTOR: f32 = 10000.0; + +#[rustfmt::skip] +pub const PREDEFINED_COLORSPACE_PRIMARIES: &[[f32; 8]] = &[ + [0.68 , 0.32 , 0.265 , 0.69 , 0.15 , 0.06 , 0.3127 , 0.329 ], // 0, DCI-P3 D65 + [0.64 , 0.33 , 0.30 , 0.60 , 0.15 , 0.06 , 0.3127 , 0.329 ], // 1, BT.709 + [0.708 , 0.292 , 0.170 , 0.797 , 0.131 , 0.046 , 0.3127 , 0.329 ], // 2, BT.2020 + [0.63 , 0.34 , 0.31 , 0.595 , 0.155 , 0.07 , 0.3127 , 0.329 ], // 3, BT.601 NTSC / SMPTE-C + [0.64 , 0.33 , 0.29 , 0.60 , 0.15 , 0.06 , 0.3127 , 0.329 ], // 4, BT.601 PAL / BT.470 BG + [0.68 , 0.32 , 0.265 , 0.69 , 0.15 , 0.06 , 0.314 , 0.351 ], // 5, DCI-P3 + [0.7347, 0.2653, 0.0 , 1.0 , 0.0001,-0.077 , 0.32168, 0.33767], // 6, ACES + [0.73 , 0.28 , 0.14 , 0.855 , 0.10 ,-0.05 , 0.3127 , 0.329 ], // 7, S-Gamut + [0.766 , 0.275 , 0.225 , 0.80 , 0.089 ,-0.087 , 0.3127 , 0.329 ], // 8, S-Gamut-3.Cine + [0.693 , 0.304 , 0.208 , 0.761 , 0.1467, 0.0527, 0.3127 , 0.329 ], + [0.6867, 0.3085, 0.231 , 0.69 , 0.1489, 0.0638, 0.3127 , 0.329 ], + [0.6781, 0.3189, 0.2365, 0.7048, 0.141 , 0.0489, 0.3127 , 0.329 ], + [0.68 , 0.32 , 0.265 , 0.69 , 0.15 , 0.06 , 0.3127 , 0.329 ], + [0.7042, 0.294 , 0.2271, 0.725 , 0.1416, 0.0516, 0.3127 , 0.329 ], + [0.6745, 0.310 , 0.2212, 0.7109, 0.152 , 0.0619, 0.3127 , 0.329 ], + [0.6805, 0.3191, 0.2522, 0.6702, 0.1397, 0.0554, 0.3127 , 0.329 ], + [0.6838, 0.3085, 0.2709, 0.6378, 0.1478, 0.0589, 0.3127 , 0.329 ], + [0.6753, 0.3193, 0.2636, 0.6835, 0.1521, 0.0627, 0.3127 , 0.329 ], + [0.6981, 0.2898, 0.1814, 0.7189, 0.1517, 0.0567, 0.3127 , 0.329 ], +]; + +/// Format: `[id, primary_index, peak_brightness, min_pq, eotf(enum usize), range]` +#[rustfmt::skip] +pub const PREDEFINED_MASTERING_DISPLAYS: &[[usize; 6]] = &[ + [ 7, 0, 4000, 62, 0, 0], // Default: 4000-nit, P3, D65, ST.2084 + [ 8, 2, 4000, 62, 0, 0], + [20, 0, 1000, 7, 0, 0], + [21, 2, 1000, 7, 0, 0], + [30, 0, 2000, 7, 0, 0], + [31, 2, 2000, 7, 0, 0], +]; + +// pub const CMV29_MASTERING_DISPLAYS_LIST: &[u8] = &[7, 8, 20, 21, 30, 31]; + +/// Only HOME targets are included. +/// +/// Format: `[id, primary_index, peak_brightness, min_pq, eotf(enum usize), range]` +#[rustfmt::skip] +pub const PREDEFINED_TARGET_DISPLAYS: &[[usize; 6]] = &[ + [ 1, 1, 100, 62, 2, 0], + [ 27, 0, 600, 0, 0, 0], + [ 28, 2, 600, 0, 0, 0], + [ 37, 0, 2000, 0, 0, 0], + [ 38, 2, 2000, 0, 0, 0], + [ 48, 0, 1000, 0, 0, 0], + [ 49, 2, 1000, 0, 0, 0], + [9003, 1, 600, 7, 2, 0], // BETA +]; + +// pub const CMV29_TARGET_DISPLAYS_LIST: &[u8] = &[1, 27, 28, 37, 38, 48, 49]; + +const ST2084_Y_MAX: f32 = 10000.0; +const ST2084_M1: f32 = 2610.0 / 16384.0; +const ST2084_M2: f32 = (2523.0 / 4096.0) * 128.0; +const ST2084_C1: f32 = 3424.0 / 4096.0; +const ST2084_C2: f32 = (2413.0 / 4096.0) * 32.0; +const ST2084_C3: f32 = (2392.0 / 4096.0) * 32.0; + +pub fn pq2l(pq: f32) -> f32 { + let y = ((pq.powf(1.0 / ST2084_M2) - ST2084_C1) + / (ST2084_C2 - ST2084_C3 * pq.powf(1.0 / ST2084_M2))) + .powf(1.0 / ST2084_M1); + + y * ST2084_Y_MAX +} + +pub fn find_target_id(max: usize, primary: usize) -> usize { + get_display_id(PREDEFINED_TARGET_DISPLAYS, max, primary).unwrap_or(0) +} + +fn get_display_id(list: &[[usize; 6]], max_luminance: usize, primary: usize) -> Option { + list.iter() + .find(|t| (**t)[2] == max_luminance && (**t)[1] == primary) + .map(|d| d[0]) +} diff --git a/src/metadata/display/primary.rs b/src/metadata/display/primary.rs new file mode 100644 index 0000000..670b811 --- /dev/null +++ b/src/metadata/display/primary.rs @@ -0,0 +1,128 @@ +use std::array; + +use dolby_vision::rpu::extension_metadata::blocks::{ + ExtMetadataBlock, ExtMetadataBlockInfo, ExtMetadataBlockLevel9, +}; +use dolby_vision::rpu::vdr_dm_data::VdrDmData; + +use crate::display::chromaticity::Chromaticity; +use crate::display::PREDEFINED_COLORSPACE_PRIMARIES; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Primaries { + pub red: Chromaticity, + pub green: Chromaticity, + pub blue: Chromaticity, + pub white_point: Chromaticity, +} + +impl Primaries { + pub fn f32_from_rpu_u16(u: u16) -> f32 { + (match u { + 0..=32767 => u as f32, + // input value 32768 is undefined, should not happen + _ => u as f32 - 65536.0, + }) / 32767.0 + } + + pub fn get_index(&self) -> Option { + PREDEFINED_COLORSPACE_PRIMARIES + .iter() + .enumerate() + .find(|(_, p)| Self::from(**p) == *self) + .map(|(i, _)| i) + } + + pub fn get_index_primary(index: usize, is_target: bool) -> Option { + let index_max = PREDEFINED_COLORSPACE_PRIMARIES.len(); + let index = if index >= index_max || is_target && index > 8 { + None + } else { + Some(index) + }; + + index.map(|index| Primaries::from(PREDEFINED_COLORSPACE_PRIMARIES[index])) + } +} + +impl IntoIterator for Primaries { + type Item = f32; + type IntoIter = array::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + let mut result = [0.0; 8]; + + // We know size is 8 + let vec = [self.red, self.green, self.blue, self.white_point] + .into_iter() + .flatten() + .collect::>(); + + for (i, v) in result.iter_mut().zip(vec) { + *i = v + } + + result.into_iter() + } +} + +impl Default for Primaries { + fn default() -> Self { + Self::from(PREDEFINED_COLORSPACE_PRIMARIES[0]) + } +} + +impl From<[f32; 8]> for Primaries { + fn from(p: [f32; 8]) -> Self { + Self { + red: Chromaticity([p[0], p[1]]), + green: Chromaticity([p[2], p[3]]), + blue: Chromaticity([p[4], p[5]]), + white_point: Chromaticity([p[6], p[7]]), + } + } +} + +impl From<[u16; 8]> for Primaries { + fn from(p: [u16; 8]) -> Self { + let mut result = [0.0f32; 8]; + + for (i, j) in result.iter_mut().zip(p) { + *i = Self::f32_from_rpu_u16(j) + } + + Primaries::from(result) + } +} + +impl From<&ExtMetadataBlockLevel9> for Primaries { + fn from(block: &ExtMetadataBlockLevel9) -> Self { + match block.bytes_size() { + 1 => Primaries::get_index_primary(block.source_primary_index as usize, false) + .unwrap_or_default(), + 17 => Primaries::from([ + block.source_primary_red_x, + block.source_primary_red_y, + block.source_primary_green_x, + block.source_primary_green_y, + block.source_primary_blue_x, + block.source_primary_blue_y, + block.source_primary_white_x, + block.source_primary_white_y, + ]), + _ => unreachable!(), + } + } +} + +// For source display +impl From<&VdrDmData> for Primaries { + fn from(vdr: &VdrDmData) -> Self { + vdr.get_block(9) + .and_then(|b| match b { + ExtMetadataBlock::Level9(b) => Some(Self::from(b)), + _ => None, + }) + .unwrap_or_default() + } +} diff --git a/src/metadata/levels/level1.rs b/src/metadata/levels/level1.rs new file mode 100644 index 0000000..cabd000 --- /dev/null +++ b/src/metadata/levels/level1.rs @@ -0,0 +1,41 @@ +use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlockLevel1; +use serde::Serialize; + +use crate::metadata::MDFType::*; +use crate::metadata::{IntoCMV29, MDFType}; + +use super::ImageCharacter; + +#[derive(Debug, Clone, PartialEq, Serialize)] +pub struct Level1 { + pub level: u8, + #[serde(rename = "$unflatten=ImageCharacter")] + pub image_character: MDFType, +} + +impl From<&ExtMetadataBlockLevel1> for Level1 { + fn from(block: &ExtMetadataBlockLevel1) -> Self { + Self { + level: 1, + image_character: CMV40(block.into()), + } + } +} + +impl IntoCMV29 for Level1 { + fn into_cmv29(self) -> Self { + Self { + level: 1, + image_character: self.image_character.into_cmv29(), + } + } +} + +impl Default for Level1 { + fn default() -> Self { + Self { + level: 0, + image_character: CMV40(ImageCharacter([0.0; 3])), + } + } +} diff --git a/src/metadata/levels/level11.rs b/src/metadata/levels/level11.rs new file mode 100644 index 0000000..c713c1c --- /dev/null +++ b/src/metadata/levels/level11.rs @@ -0,0 +1,38 @@ +use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlockLevel11; +use serde::Serialize; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +pub struct Level11 { + pub level: u8, + #[serde(rename = "$unflatten=ContentType")] + pub content_type: u8, + #[serde(rename = "$unflatten=IntendedWhitePoint")] + pub intended_white_point: u8, + // FIXME: Rename + #[serde(rename = "$unflatten=ExtensionProperties")] + #[serde(skip_serializing_if = "Option::is_none")] + pub extension_properties: Option, +} + +impl Default for Level11 { + fn default() -> Self { + Self { + level: 11, + content_type: 1, // Movies + intended_white_point: 0, + extension_properties: None, + } + } +} + +impl From<&ExtMetadataBlockLevel11> for Level11 { + fn from(block: &ExtMetadataBlockLevel11) -> Self { + Self { + level: 11, + content_type: block.content_type, + intended_white_point: block.whitepoint, + // TODO: byte3? + extension_properties: None, + } + } +} diff --git a/src/metadata/levels/level2.rs b/src/metadata/levels/level2.rs new file mode 100644 index 0000000..6f6b148 --- /dev/null +++ b/src/metadata/levels/level2.rs @@ -0,0 +1,96 @@ +use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlockLevel2; +use serde::ser::SerializeStruct; +use serde::{Serialize, Serializer}; + +use crate::display::find_target_id; +use crate::f32_from_rpu_u12_with_bias; +use crate::metadata::display::Characteristics; +use crate::metadata::MDFType::*; +use crate::metadata::{IntoCMV29, MDFType}; + +use super::TrimSixField; + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Level2 { + pub level: u8, + // #[serde(rename = "$unflatten=TID")] + pub tid: u8, + // Format: 0 0 0 f32 f32 f32 f32 f32 f32 + // #[serde(rename = "$unflatten=Trim")] + pub trim: MDFType, +} + +impl Serialize for Level2 { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut trim = self.trim; + let mut new_trim = [0.0; 9]; + new_trim + .iter_mut() + .skip(3) + .zip(self.trim.into_inner().0) + .for_each(|(t, s)| *t = s); + + let mut state = serializer.serialize_struct("Level2", 3)?; + + state.serialize_field("level", &self.level)?; + state.serialize_field("$unflatten=TID", &self.tid)?; + state.serialize_field("$unflatten=Trim", &trim.with_new_inner(new_trim))?; + + state.end() + } +} + +impl IntoCMV29 for Level2 { + fn into_cmv29(self) -> Self { + Self { + level: 2, + tid: self.tid, + trim: self.trim.into_cmv29(), + } + } +} + +impl Level2 { + pub fn with_primary_index(block: &ExtMetadataBlockLevel2, primary: Option) -> Self { + // Actually the only possible value is -1 + let ms_weight = if block.ms_weight < 0 { + -1.0 + } else { + f32_from_rpu_u12_with_bias(block.ms_weight as u16) + }; + + let luminance = Characteristics::max_u16_from_rpu_pq_u12(block.target_max_pq); + let primary = if luminance == 100 { + 1 + } else { + primary.unwrap_or(2) + }; + + let mut trim = TrimSixField([ + f32_from_rpu_u12_with_bias(block.trim_slope), + f32_from_rpu_u12_with_bias(block.trim_offset), + f32_from_rpu_u12_with_bias(block.trim_power), + f32_from_rpu_u12_with_bias(block.trim_chroma_weight), + f32_from_rpu_u12_with_bias(block.trim_saturation_gain), + ms_weight, + ]); + + trim.sop_to_lgg(); + + Self { + level: 2, + tid: find_target_id(luminance, primary) as u8, + trim: CMV40(trim), + } + } +} + +impl From<&ExtMetadataBlockLevel2> for Level2 { + fn from(block: &ExtMetadataBlockLevel2) -> Self { + // BT.2020 + Self::with_primary_index(block, None) + } +} diff --git a/src/metadata/levels/level254.rs b/src/metadata/levels/level254.rs new file mode 100644 index 0000000..71961ef --- /dev/null +++ b/src/metadata/levels/level254.rs @@ -0,0 +1,32 @@ +use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlockLevel254; +use serde::Serialize; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct Level254 { + pub level: u8, + #[serde(rename = "$unflatten=DMMode")] + pub dm_mode: u8, + #[serde(rename = "$unflatten=DMVersion")] + pub dm_version: u8, + // Format: u8 u8 + #[serde(rename = "$unflatten=CMVersion")] + pub cm_version: String, +} + +impl Default for Level254 { + fn default() -> Self { + (&ExtMetadataBlockLevel254::cmv402_default()).into() + } +} + +impl From<&ExtMetadataBlockLevel254> for Level254 { + fn from(block: &ExtMetadataBlockLevel254) -> Self { + Self { + level: 254, + dm_mode: block.dm_mode, + dm_version: block.dm_version_index, + // FIXME: Hardcode + cm_version: "4 1".to_string(), + } + } +} diff --git a/src/metadata/levels/level3.rs b/src/metadata/levels/level3.rs new file mode 100644 index 0000000..df3aa1a --- /dev/null +++ b/src/metadata/levels/level3.rs @@ -0,0 +1,22 @@ +use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlockLevel3; +use serde::Serialize; + +use crate::MDFType::CMV40; +use crate::{ImageCharacter, MDFType}; + +#[derive(Debug, Clone, PartialEq, Serialize)] +pub struct Level3 { + pub level: u8, + // Format: f32 f32 f32 + #[serde(rename = "$unflatten=L1Offset")] + pub l1_offset: MDFType, +} + +impl From<&ExtMetadataBlockLevel3> for Level3 { + fn from(block: &ExtMetadataBlockLevel3) -> Self { + Self { + level: 3, + l1_offset: CMV40(ImageCharacter::from(block)), + } + } +} diff --git a/src/metadata/levels/level5.rs b/src/metadata/levels/level5.rs new file mode 100644 index 0000000..e061f90 --- /dev/null +++ b/src/metadata/levels/level5.rs @@ -0,0 +1,86 @@ +use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlockLevel5; +use serde::Serialize; +use std::cmp::Ordering; + +use crate::MDFType::CMV40; +use crate::{IntoCMV29, MDFType, UHD_HEIGHT, UHD_WIDTH}; + +use super::AspectRatio; + +#[derive(Debug, Clone, Serialize, Hash, PartialEq, Eq)] +pub struct Level5 { + pub level: u8, + // Format: f32 f32 + #[serde(rename = "$unflatten=AspectRatio")] + pub aspect_ratio: MDFType, +} + +impl Level5 { + pub fn get_ar(&self) -> (f32, f32) { + let ar = self.aspect_ratio.into_inner().0; + (ar[0], ar[1]) + } +} + +// For convenience, it assumes the canvas is standard UHD +impl From<&ExtMetadataBlockLevel5> for Level5 { + fn from(block: &ExtMetadataBlockLevel5) -> Self { + Self::with_canvas(block, (UHD_WIDTH, UHD_HEIGHT)) + } +} + +impl From for Level5 { + fn from(ar: f32) -> Self { + Self { + level: 5, + aspect_ratio: CMV40(AspectRatio([ar, ar])), + } + } +} + +impl IntoCMV29 for Level5 { + fn into_cmv29(self) -> Self { + Self { + level: 5, + aspect_ratio: self.aspect_ratio.into_cmv29(), + } + } +} + +impl Level5 { + pub fn with_canvas(block: &ExtMetadataBlockLevel5, canvas: (usize, usize)) -> Self { + let (width, height) = canvas; + let canvas_ar = width as f32 / height as f32; + + let horizontal_crop = block.active_area_left_offset + block.active_area_right_offset; + let vertical_crop = block.active_area_top_offset + block.active_area_bottom_offset; + + let image_ar = if horizontal_crop > 0 { + (width as f32 - horizontal_crop as f32) / height as f32 + } else { + // Ok because only one of the crop types will be 0 + width as f32 / (height as f32 - vertical_crop as f32) + }; + + Self { + level: 5, + aspect_ratio: CMV40(AspectRatio([canvas_ar, image_ar])), + } + } +} + +impl PartialOrd for Level5 { + fn partial_cmp(&self, other: &Self) -> Option { + self.aspect_ratio + .into_inner() + .partial_cmp(&other.aspect_ratio.into_inner()) + } +} + +impl Ord for Level5 { + fn cmp(&self, other: &Self) -> Ordering { + self.aspect_ratio + .into_inner() + .cmp(&other.aspect_ratio.into_inner()) + } +} diff --git a/src/metadata/levels/level6.rs b/src/metadata/levels/level6.rs new file mode 100644 index 0000000..9ce70c5 --- /dev/null +++ b/src/metadata/levels/level6.rs @@ -0,0 +1,31 @@ +use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlockLevel6; +use serde::Serialize; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct Level6 { + pub level: usize, + #[serde(rename = "$unflatten=MaxCLL")] + pub max_cll: usize, + #[serde(rename = "$unflatten=MaxFALL")] + pub max_fall: usize, +} + +impl Default for Level6 { + fn default() -> Self { + Self { + level: 6, + max_cll: 0, + max_fall: 0, + } + } +} + +impl From<&ExtMetadataBlockLevel6> for Level6 { + fn from(block: &ExtMetadataBlockLevel6) -> Self { + Self { + level: 6, + max_cll: block.max_content_light_level as usize, + max_fall: block.max_frame_average_light_level as usize, + } + } +} diff --git a/src/metadata/levels/level8.rs b/src/metadata/levels/level8.rs new file mode 100644 index 0000000..c4ac328 --- /dev/null +++ b/src/metadata/levels/level8.rs @@ -0,0 +1,66 @@ +use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlockLevel8; +use serde::Serialize; + +use crate::MDFType; +use crate::MDFType::CMV40; + +use super::TrimSixField; + +#[derive(Debug, Clone, PartialEq, Serialize)] +pub struct Level8 { + pub level: u8, + #[serde(rename = "$unflatten=TID")] + pub tid: u8, + // Format: f32 f32 f32 f32 f32 f32 + #[serde(rename = "$unflatten=L8Trim")] + pub l8_trim: MDFType, + #[serde(rename = "$unflatten=MidContrastBias")] + pub mid_contrast_bias: f32, + #[serde(rename = "$unflatten=HighlightClipping")] + pub highlight_clipping: f32, + // Format: f32 f32 f32 f32 f32 f32 + #[serde(rename = "$unflatten=SaturationVectorField")] + pub sat_vector_field: MDFType, + // Format: f32 f32 f32 f32 f32 f32 + #[serde(rename = "$unflatten=HueVectorField")] + pub hue_vector_field: MDFType, +} + +impl From<&ExtMetadataBlockLevel8> for Level8 { + fn from(block: &ExtMetadataBlockLevel8) -> Self { + let mut trim = TrimSixField([ + crate::f32_from_rpu_u12_with_bias(block.trim_slope), + crate::f32_from_rpu_u12_with_bias(block.trim_offset), + crate::f32_from_rpu_u12_with_bias(block.trim_power), + crate::f32_from_rpu_u12_with_bias(block.trim_chroma_weight), + crate::f32_from_rpu_u12_with_bias(block.trim_saturation_gain), + crate::f32_from_rpu_u12_with_bias(block.ms_weight), + ]); + + trim.sop_to_lgg(); + + Self { + level: 8, + tid: block.target_display_index, + l8_trim: CMV40(trim), + mid_contrast_bias: crate::f32_from_rpu_u12_with_bias(block.target_mid_contrast), + highlight_clipping: crate::f32_from_rpu_u12_with_bias(block.clip_trim), + sat_vector_field: CMV40(TrimSixField([ + crate::f32_from_rpu_u8_with_bias(block.saturation_vector_field0), + crate::f32_from_rpu_u8_with_bias(block.saturation_vector_field1), + crate::f32_from_rpu_u8_with_bias(block.saturation_vector_field2), + crate::f32_from_rpu_u8_with_bias(block.saturation_vector_field3), + crate::f32_from_rpu_u8_with_bias(block.saturation_vector_field4), + crate::f32_from_rpu_u8_with_bias(block.saturation_vector_field5), + ])), + hue_vector_field: CMV40(TrimSixField([ + crate::f32_from_rpu_u8_with_bias(block.hue_vector_field0), + crate::f32_from_rpu_u8_with_bias(block.hue_vector_field1), + crate::f32_from_rpu_u8_with_bias(block.hue_vector_field2), + crate::f32_from_rpu_u8_with_bias(block.hue_vector_field3), + crate::f32_from_rpu_u8_with_bias(block.hue_vector_field4), + crate::f32_from_rpu_u8_with_bias(block.hue_vector_field5), + ])), + } + } +} diff --git a/src/metadata/levels/level9.rs b/src/metadata/levels/level9.rs new file mode 100644 index 0000000..3b13059 --- /dev/null +++ b/src/metadata/levels/level9.rs @@ -0,0 +1,32 @@ +use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlockLevel9; +use serde::Serialize; + +use crate::MDFType::CMV40; +use crate::{display, MDFType}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct Level9 { + pub level: u8, + // 255 + #[serde(rename = "$unflatten=SourceColorModel")] + pub source_color_model: u8, + // Format: f32 f32 f32 f32 f32 f32 f32 f32 + #[serde(rename = "$unflatten=SourceColorPrimary")] + pub source_color_primary: MDFType, +} + +impl From for Level9 { + fn from(p: display::Primaries) -> Self { + Self { + level: 9, + source_color_model: 255, + source_color_primary: CMV40(p), + } + } +} + +impl From<&ExtMetadataBlockLevel9> for Level9 { + fn from(block: &ExtMetadataBlockLevel9) -> Self { + display::Primaries::from(block).into() + } +} diff --git a/src/metadata/levels/mod.rs b/src/metadata/levels/mod.rs new file mode 100644 index 0000000..a1df48c --- /dev/null +++ b/src/metadata/levels/mod.rs @@ -0,0 +1,171 @@ +use std::array; +use std::cmp::Ordering; +use std::hash::{Hash, Hasher}; +use std::ops::Add; + +use dolby_vision::rpu::extension_metadata::blocks::{ + ExtMetadataBlockLevel1, ExtMetadataBlockLevel3, +}; + +pub use level1::Level1; +pub use level11::Level11; +pub use level2::Level2; +pub use level254::Level254; +pub use level3::Level3; +pub use level5::Level5; +pub use level6::Level6; +pub use level8::Level8; +pub use level9::Level9; + +mod level1; +mod level11; +mod level2; +mod level254; +mod level3; +mod level5; +mod level6; +mod level8; +mod level9; + +pub const RPU_PQ_MAX: f32 = 4095.0; +// pub const RPU_PQ_OFFSET: f32 = 2048.0; +pub const RPU_U8_BIAS: f32 = 128.0; +pub const RPU_U12_BIAS: f32 = 2048.0; +pub const UHD_WIDTH: usize = 3840; +pub const UHD_HEIGHT: usize = 2160; +pub const UHD_AR: f32 = 16.0 / 9.0; + +pub fn f32_from_rpu_u12_with_bias(u: u16) -> f32 { + (u as f32 - RPU_U12_BIAS) / RPU_U12_BIAS +} + +pub fn f32_from_rpu_u8_with_bias(u: u8) -> f32 { + (u as f32 - RPU_U8_BIAS) / RPU_U8_BIAS +} + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct TrimSixField([f32; 6]); + +impl TrimSixField { + pub fn sop_to_lgg(&mut self) { + let slope = self.0[0]; + let offset = self.0[1]; + let power = self.0[2]; + + let gain = slope + offset; + let lift = 2.0 * offset / (gain + 2.0); + let gamma = (4.0 / (power + 2.0) - 2.0).min(1.0); + + self.0[0] = lift; + self.0[1] = gain; + self.0[2] = gamma; + } +} + +impl IntoIterator for TrimSixField { + type Item = f32; + type IntoIter = array::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ImageCharacter([f32; 3]); + +impl ImageCharacter { + pub fn new() -> Self { + Self([0.0; 3]) + } +} + +impl Add for ImageCharacter { + type Output = ImageCharacter; + + fn add(self, rhs: Self) -> Self::Output { + let mut result = Self::new(); + let Self(lhs) = self; + let Self(rhs) = rhs; + + for ((i, a), b) in result.0.iter_mut().zip(&lhs).zip(&rhs) { + *i = a + b; + } + + result + } +} + +impl From<&ExtMetadataBlockLevel1> for ImageCharacter { + fn from(block: &ExtMetadataBlockLevel1) -> Self { + Self([ + block.min_pq as f32 / RPU_PQ_MAX, + block.avg_pq as f32 / RPU_PQ_MAX, + block.max_pq as f32 / RPU_PQ_MAX, + ]) + } +} + +impl From<&ExtMetadataBlockLevel3> for ImageCharacter { + fn from(block: &ExtMetadataBlockLevel3) -> Self { + Self([ + f32_from_rpu_u12_with_bias(block.min_pq_offset), + f32_from_rpu_u12_with_bias(block.avg_pq_offset), + f32_from_rpu_u12_with_bias(block.max_pq_offset), + ]) + } +} + +impl IntoIterator for ImageCharacter { + type Item = f32; + type IntoIter = array::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +#[derive(Clone, Copy, Debug)] +pub struct AspectRatio([f32; 2]); + +impl IntoIterator for AspectRatio { + type Item = f32; + type IntoIter = array::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl PartialEq for AspectRatio { + fn eq(&self, other: &Self) -> bool { + let self_ar = self.0[1] / self.0[0]; + let other_ar = other.0[1] / other.0[0]; + + self_ar.to_bits() == other_ar.to_bits() + } +} + +impl PartialOrd for AspectRatio { + fn partial_cmp(&self, other: &Self) -> Option { + let self_ar = self.0[1] / self.0[0]; + let other_ar = other.0[1] / other.0[0]; + + self_ar.partial_cmp(&other_ar) + } +} + +// NaN should not happen for AspectRatio +impl Ord for AspectRatio { + fn cmp(&self, other: &Self) -> Ordering { + self.partial_cmp(other).unwrap_or(Ordering::Equal) + } +} + +impl Eq for AspectRatio {} + +impl Hash for AspectRatio { + fn hash(&self, state: &mut H) { + self.0.iter().for_each(|f| f.to_bits().hash(state)) + } +} diff --git a/src/metadata/mod.rs b/src/metadata/mod.rs new file mode 100644 index 0000000..7f9bfae --- /dev/null +++ b/src/metadata/mod.rs @@ -0,0 +1,349 @@ +use std::array; +use std::fmt::{Debug, Display, Formatter}; + +use chrono::{SecondsFormat, Utc}; +use itertools::Itertools; +use serde::{Deserialize, Serialize, Serializer}; +// use serde_aux::prelude::serde_introspect; +use uuid::Uuid; + +use display::Chromaticity; + +use crate::MDFType::{CMV29, CMV40}; + +pub mod cmv29; +pub mod cmv40; +pub mod display; +pub mod levels; + +pub const XML_PREFIX: &str = "\n"; +pub const DOLBY_XMLNS_PREFIX: &str = "http://www.dolby.com/schemas/dvmd/"; + +/// UUID v4. +#[derive(Debug, Clone, Serialize)] +pub struct UUIDv4(String); + +impl UUIDv4 { + pub fn new() -> Self { + Self(Uuid::new_v4().to_string()) + } +} + +impl Default for UUIDv4 { + fn default() -> Self { + Self(Uuid::default().to_string()) + } +} + +pub const CMV40_MIN_VERSION: Version = Version { + major: 4, + minor: 0, + revision: 2, +}; + +// #[derive(Debug)] +// pub enum CMVersion { +// CMV29, +// CMV40, +// } +// +// impl Default for CMVersion { +// fn default() -> Self { +// Self::CMV40 +// } +// } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum MDFType { + CMV29(T), + CMV40(T), +} + +impl MDFType { + pub fn into_inner(self) -> T { + match self { + CMV29(t) | CMV40(t) => t, + } + } + + pub fn with_new_inner(&mut self, value: U) -> MDFType { + match self { + CMV29(_) => CMV29(value), + CMV40(_) => CMV40(value), + } + } +} + +impl Default for MDFType +where + T: Default, +{ + fn default() -> Self { + CMV40(T::default()) + } +} + +impl Serialize for MDFType +where + T: IntoIterator + Copy, + I: Display, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("{}", &self)) + } +} + +impl Display for MDFType +where + T: IntoIterator + Copy, + I: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let join_str = match &self { + CMV29(_) => ",", + CMV40(_) => " ", + }; + + write!(f, "{}", self.into_inner().into_iter().join(join_str)) + } +} + +pub trait IntoCMV29 { + /// Convert inner `MDFType` to `CMV29(T)`. + fn into_cmv29(self) -> T; +} + +impl IntoCMV29> for Option +where + T: IntoCMV29, +{ + fn into_cmv29(self) -> Option { + self.map(|i| i.into_cmv29()) + } +} + +impl IntoCMV29> for Vec +where + T: IntoCMV29, +{ + fn into_cmv29(self) -> Vec { + self.into_iter().map(|b| b.into_cmv29()).collect::>() + } +} + +impl IntoCMV29 for MDFType { + fn into_cmv29(self) -> Self { + match self { + CMV29(t) | CMV40(t) => CMV29(t), + } + } +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[repr(usize)] +pub enum Eotf { + #[serde(rename = "$primitive=pq")] + #[default] + Pq, + #[serde(rename = "$primitive=linear")] + Linear, + #[serde(rename = "$primitive=gamma_bt1886")] + GammaBT1886, + #[serde(rename = "$primitive=gamma_dci")] + GammaDCI, + #[serde(rename = "$primitive=gamma_22")] + Gamma22, + #[serde(rename = "$primitive=gamma_24")] + Gamma24, + #[serde(rename = "$primitive=hlg")] + Hlg, +} + +#[derive(Debug, Clone, Serialize)] +pub enum ColorSpace { + #[serde(rename = "$primitive=rgb")] + Rgb, + // #[serde(rename = "$primitive=xyz")] + // Xyz, + // #[serde(rename = "$primitive=ycbcr_bt709")] + // YCbCrBT709, + // #[serde(rename = "$primitive=ycbcr_bt2020")] + // YCbCrBT2020, + // #[serde(rename = "$primitive=ycbcr_native")] + // YCbCrNative, +} + +#[derive(Debug, Clone, Copy, Serialize)] +pub enum ApplicationType { + #[serde(rename = "$primitive=ALL")] + All, + #[serde(rename = "$primitive=HOME")] + Home, + // #[serde(rename = "$primitive=CINEMA")] + // Cinema, +} + +#[derive(Debug, Clone, Serialize)] +pub enum SignalRange { + #[serde(rename = "$primitive=computer")] + Computer, + // #[serde(rename = "$primitive=video")] + // Video, +} + +pub const XML_VERSION_LIST: &[[usize; 3]] = &[[2, 0, 5], [4, 0, 2], [5, 1, 0]]; + +pub enum XMLVersion { + V205, + V402, + V510, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)] +pub struct Version { + major: usize, + minor: usize, + revision: usize, +} + +impl Version { + pub fn get_dolby_xmlns(&self) -> String { + DOLBY_XMLNS_PREFIX.to_string() + &self.into_iter().join("_") + } +} + +impl From<[usize; 3]> for Version { + fn from(u: [usize; 3]) -> Self { + Self { + major: u[0], + minor: u[1], + revision: u[2], + } + } +} + +impl From for Version { + fn from(u: XMLVersion) -> Self { + Self::from(XML_VERSION_LIST[u as usize]) + } +} + +impl Default for Version { + fn default() -> Self { + Self::from(XML_VERSION_LIST[0]) + } +} + +impl Display for Version { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + // write!(f, "{}.{}.{}", &self.major, &self.minor, &self.revision) + write!(f, "{}", self.into_iter().join(".")) + } +} + +impl IntoIterator for Version { + type Item = usize; + type IntoIter = array::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + [self.major, self.minor, self.revision].into_iter() + } +} + +impl Serialize for Version { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl Version { + // pub fn from_summary(summary:) +} + +#[derive(Debug, Serialize)] +pub struct RevisionHistory { + #[serde(rename = "Revision")] + #[serde(skip_serializing_if = "Option::is_none")] + pub revisions: Option>, +} + +impl RevisionHistory { + pub fn new() -> Self { + Self { + revisions: Some(vec![Revision::new()]), + } + } +} + +#[derive(Debug, Serialize)] +pub struct Revision { + #[serde(rename = "DateTime")] + pub date_time: DateTime, + #[serde(rename = "$unflatten=Author")] + pub author: String, + #[serde(rename = "$unflatten=Software")] + pub software: String, + #[serde(rename = "$unflatten=SoftwareVersion")] + pub software_version: String, + #[serde(rename = "$unflatten=Comment")] + #[serde(skip_serializing_if = "Option::is_none")] + pub comment: Option, +} + +impl Revision { + pub fn new() -> Self { + Self { + date_time: DateTime::new(), + author: env!("CARGO_PKG_AUTHORS").to_string(), + software: env!("CARGO_PKG_NAME").to_string(), + software_version: env!("CARGO_PKG_VERSION").to_string(), + comment: None, + } + } +} + +#[derive(Debug, Serialize)] +pub struct DateTime(String); + +impl DateTime { + pub fn new() -> Self { + Self(Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true)) + } +} + +// Format: f32,f32 in CMv2.9, f32 f32 in CMv4.0 +#[derive(Debug, Clone, Default, Serialize)] +pub struct Primaries { + #[serde(rename = "$unflatten=Red")] + pub red: MDFType, + #[serde(rename = "$unflatten=Green")] + pub green: MDFType, + #[serde(rename = "$unflatten=Blue")] + pub blue: MDFType, +} + +impl From for Primaries { + fn from(p: display::Primaries) -> Self { + Self { + red: CMV40(p.red), + green: CMV40(p.green), + blue: CMV40(p.blue), + } + } +} + +impl IntoCMV29 for Primaries { + fn into_cmv29(self) -> Self { + Self { + red: self.red.into_cmv29(), + green: self.green.into_cmv29(), + blue: self.blue.into_cmv29(), + } + } +}