├── .github ├── actions │ └── setup-release-env │ │ └── action.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── assets ├── NotoSans-Regular.ttf ├── av1-rpu │ ├── fel_orig.bin │ ├── p5-01-ref.bin │ ├── p5-01.bin │ ├── p84-01-ref.bin │ └── p84-01.bin ├── editor_examples │ ├── active_area.json │ ├── active_area_all.json │ ├── crop.json │ ├── duplicate.json │ ├── fel_to_p81_encode.json │ ├── l9_and_l11.json │ ├── level6_metadata.json │ ├── levels.json │ ├── mode.json │ ├── p5_to_p81.json │ ├── remove.json │ ├── remove_cmv4.json │ ├── scene_cuts.json │ └── source_rpu.json ├── generator_examples │ ├── default_cmv29.json │ ├── default_cmv40.json │ ├── full_example.json │ ├── l1_cmv29.json │ ├── l1_cmv29_override_avg_cmv40.json │ ├── l1_cmv40.json │ └── no_duration.json ├── hevc_tests │ ├── no_aud_bl.hevc │ ├── no_aud_injected.hevc │ ├── no_aud_muxed.hevc │ ├── regular.hevc │ ├── regular.mkv │ ├── regular_bl_start_code_4.hevc │ ├── regular_bl_start_code_4_shorter.hevc │ ├── regular_convert_annexb.hevc │ ├── regular_demux_bl_annexb.hevc │ ├── regular_inject_annexb.hevc │ ├── regular_rpu.bin │ ├── regular_rpu_mel.bin │ ├── regular_start_code_4.hevc │ ├── regular_start_code_4_muxed_el.hevc │ ├── regular_start_code_4_shorter_muxed_el.hevc │ ├── sei-double-3byte-case.hevc │ ├── sei-double-3byte-start-code-4.hevc │ ├── sei-suffix-muxed-rpu.hevc │ └── yusesope_regular_muxed.hevc └── tests │ ├── cmv2_9.xml │ ├── cmv2_9_xml_with_l5_rpu.bin │ ├── cmv40_full_rpu.bin │ ├── cmv4_0_2.xml │ ├── cmv4_0_2_custom_displays.xml │ ├── cmv4_0_2_custom_displays_xml_rpu.bin │ ├── cmv4_0_2_xml_rpu.bin │ ├── cmv4_0_2_xml_with_l5_rpu.bin │ ├── cmv4_2_510_xml_rpu.bin │ ├── cmv4_2_xml_510.xml │ ├── data_before_crc32.bin │ ├── empty_dmv1_blocks.bin │ ├── eof_rpu.bin │ ├── fel_orig.bin │ ├── fel_rpu.bin │ ├── fel_to_81.bin │ ├── fel_to_mel.bin │ ├── fix_se_write.bin │ ├── hdr10plus_metadata.json │ ├── level6_decimals.xml │ ├── mel_orig.bin │ ├── mel_rpu.bin │ ├── mel_to_81.bin │ ├── mel_to_mel.bin │ ├── mel_variable_l8_length13.bin │ ├── p8_001_end_crc32.bin │ ├── poly_coef_int_logic.bin │ ├── profile20_apple.bin │ ├── profile4.bin │ ├── profile5-02.bin │ ├── profile5.bin │ ├── profile8.bin │ ├── profile84.bin │ ├── profile8_from_profile5-02.bin │ ├── source_p5_to_p8_001_end_crc32.bin │ ├── source_rpu_replaced_fel_orig.bin │ ├── st2094_10_level3.bin │ ├── trailing_bytes_rpu.bin │ └── unordered_l8_blocks.bin ├── build.rs ├── docs ├── README.md ├── editor.md ├── generator.md └── profiles.md ├── dolby_vision ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── benches │ ├── bench_main.rs │ └── benchmarks │ │ ├── mod.rs │ │ ├── parsing.rs │ │ └── rewriting.rs ├── cbindgen.toml ├── examples │ ├── capi_rpu_file.c │ ├── capi_single_rpu.c │ ├── helpers.h │ └── simple_edit.cpp └── src │ ├── av1 │ ├── emdf.rs │ └── mod.rs │ ├── c_structs │ ├── buffers.rs │ ├── extension_metadata.rs │ ├── mod.rs │ ├── rpu.rs │ ├── rpu_data_header.rs │ ├── rpu_data_mapping.rs │ ├── rpu_data_nlq.rs │ └── vdr_dm_data.rs │ ├── capi.rs │ ├── lib.rs │ ├── rpu │ ├── dovi_rpu.rs │ ├── extension_metadata │ │ ├── blocks │ │ │ ├── level1.rs │ │ │ ├── level10.rs │ │ │ ├── level11.rs │ │ │ ├── level2.rs │ │ │ ├── level254.rs │ │ │ ├── level255.rs │ │ │ ├── level3.rs │ │ │ ├── level4.rs │ │ │ ├── level5.rs │ │ │ ├── level6.rs │ │ │ ├── level8.rs │ │ │ ├── level9.rs │ │ │ ├── mod.rs │ │ │ └── reserved.rs │ │ ├── cmv29.rs │ │ ├── cmv40.rs │ │ ├── mod.rs │ │ └── primaries.rs │ ├── generate.rs │ ├── mod.rs │ ├── profiles │ │ ├── mod.rs │ │ ├── profile4.rs │ │ ├── profile5.rs │ │ ├── profile7.rs │ │ ├── profile81.rs │ │ └── profile84.rs │ ├── rpu_data_header.rs │ ├── rpu_data_mapping.rs │ ├── rpu_data_nlq.rs │ ├── utils.rs │ └── vdr_dm_data.rs │ ├── st2094_10 │ ├── itu_t35 │ │ ├── cm_data.rs │ │ ├── dm_data.rs │ │ └── mod.rs │ └── mod.rs │ ├── utils.rs │ └── xml │ ├── mod.rs │ ├── parser.rs │ └── tests.rs ├── src ├── commands │ ├── convert.rs │ ├── demux.rs │ ├── editor.rs │ ├── export.rs │ ├── extract_rpu.rs │ ├── generate.rs │ ├── info.rs │ ├── inject_rpu.rs │ ├── mod.rs │ ├── mux.rs │ ├── plot.rs │ └── remove.rs ├── dovi │ ├── converter.rs │ ├── demuxer.rs │ ├── editor.rs │ ├── exporter.rs │ ├── general_read_write.rs │ ├── generator.rs │ ├── hdr10plus_utils.rs │ ├── mod.rs │ ├── muxer.rs │ ├── plotter.rs │ ├── remover.rs │ ├── rpu_extractor.rs │ ├── rpu_info.rs │ └── rpu_injector.rs ├── main.rs └── tests │ ├── av1_rpu.rs │ ├── mod.rs │ └── rpu.rs └── tests ├── hevc ├── convert.rs ├── demux.rs ├── extract_rpu.rs ├── inject_rpu.rs ├── mod.rs ├── mux.rs └── remove.rs ├── mod.rs └── rpu ├── editor.rs ├── export.rs ├── generate.rs ├── info.rs ├── mod.rs └── plot.rs /.github/actions/setup-release-env/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup release env 2 | runs: 3 | using: "composite" 4 | steps: 5 | - name: Install Rust 6 | uses: dtolnay/rust-toolchain@stable 7 | 8 | - name: Get the package versions 9 | shell: bash 10 | run: | 11 | RELEASE_PKG_VERSION=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[].version') 12 | LIBDOVI_PKG_VERSION=$(cargo metadata --format-version 1 --no-deps --manifest-path dolby_vision/Cargo.toml | jq -r '.packages[].version') 13 | 14 | echo "RELEASE_PKG_VERSION=${RELEASE_PKG_VERSION}" >> $GITHUB_ENV 15 | echo "LIBDOVI_PKG_VERSION=${LIBDOVI_PKG_VERSION}" >> $GITHUB_ENV 16 | echo "ARCHIVE_PREFIX=${{ env.RELEASE_BIN }}-${RELEASE_PKG_VERSION}" >> $GITHUB_ENV 17 | 18 | - name: Create artifacts directory 19 | shell: bash 20 | run: | 21 | mkdir ${{ env.RELEASE_DIR }} 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | ci: 11 | name: Check, test, rustfmt and clippy 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Install fontconfig 17 | run: | 18 | sudo apt-get update 19 | sudo apt-get install libfontconfig-dev 20 | 21 | - name: Install Rust, clippy and rustfmt 22 | uses: dtolnay/rust-toolchain@stable 23 | with: 24 | components: clippy, rustfmt 25 | 26 | - name: Check 27 | run: | 28 | cargo check --all-features 29 | 30 | cargo check --all-features \ 31 | --manifest-path dolby_vision/Cargo.toml 32 | 33 | - name: Test 34 | run: | 35 | cargo test --all-features 36 | cargo test --all-features --bins 37 | 38 | cargo test --all-features --all-targets \ 39 | --manifest-path dolby_vision/Cargo.toml 40 | 41 | - name: Rustfmt 42 | run: | 43 | cargo fmt --check 44 | 45 | cargo fmt --check \ 46 | --manifest-path dolby_vision/Cargo.toml 47 | 48 | - name: Clippy 49 | run: | 50 | cargo clippy --all-features \ 51 | --all-targets --tests -- --deny warnings 52 | 53 | cargo clippy --all-features \ 54 | --manifest-path dolby_vision/Cargo.toml \ 55 | --all-targets --tests -- --deny warnings 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | **/*.rs.bk 3 | /*.mkv 4 | /*.hevc 5 | /*.txt 6 | /*.vpy 7 | /*.bin 8 | /*.ffindex 9 | /*.json 10 | /*.xml 11 | .vscode 12 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dovi_tool" 3 | version = "2.3.0" 4 | authors = ["quietvoid"] 5 | edition = "2024" 6 | rust-version = "1.85.0" 7 | license = "MIT" 8 | repository = "https://github.com/quietvoid/dovi_tool" 9 | build = "build.rs" 10 | 11 | [[bin]] 12 | name = "dovi_tool" 13 | path = "src/main.rs" 14 | 15 | [dependencies] 16 | dolby_vision = { path = "dolby_vision", "features" = ["xml", "serde"] } 17 | bitvec_helpers = { version = "3.1.6", default-features = false, features = ["bitstream-io"] } 18 | hevc_parser = { version = "0.6.8", features = ["hevc_io"] } 19 | madvr_parse = "1.0.2" 20 | hdr10plus = { version = "2.1.3", features = ["json"] } 21 | 22 | anyhow = "1.0.98" 23 | clap = { version = "4.5.39", features = ["derive", "wrap_help", "deprecated"] } 24 | clap_lex = "*" 25 | indicatif = "0.17.11" 26 | bitvec = "1.0.1" 27 | serde = { version = "1.0.219", features = ["derive"] } 28 | serde_json = { version = "1.0.140", features = ["preserve_order"] } 29 | itertools = "0.14.0" 30 | plotters = { version = "0.3.7", default-features = false, features = ["bitmap_backend", "bitmap_encoder", "all_series"] } 31 | 32 | [dev-dependencies] 33 | assert_cmd = "2.0.17" 34 | assert_fs = "1.1.3" 35 | predicates = "3.1.3" 36 | 37 | [build-dependencies] 38 | anyhow = "1.0.98" 39 | vergen-gitcl = { version = "1.0.8", default-features = false, features = ["build"] } 40 | 41 | [features] 42 | default = ["system-font"] 43 | system-font = ["plotters/ttf"] 44 | internal-font = ["plotters/ab_glyph"] 45 | 46 | [profile.release-deploy] 47 | inherits = "release" 48 | lto = "thin" 49 | strip = "symbols" 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 quietvoid 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/NotoSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/NotoSans-Regular.ttf -------------------------------------------------------------------------------- /assets/av1-rpu/fel_orig.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/av1-rpu/fel_orig.bin -------------------------------------------------------------------------------- /assets/av1-rpu/p5-01-ref.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/av1-rpu/p5-01-ref.bin -------------------------------------------------------------------------------- /assets/av1-rpu/p5-01.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/av1-rpu/p5-01.bin -------------------------------------------------------------------------------- /assets/av1-rpu/p84-01-ref.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/av1-rpu/p84-01-ref.bin -------------------------------------------------------------------------------- /assets/av1-rpu/p84-01.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/av1-rpu/p84-01.bin -------------------------------------------------------------------------------- /assets/editor_examples/active_area.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": 2, 3 | "active_area": { 4 | "crop": true, 5 | "presets": [ 6 | { 7 | "id": 0, 8 | "left": 0, 9 | "right": 0, 10 | "top": 210, 11 | "bottom": 210 12 | } 13 | ], 14 | "edits": { 15 | "0-40": 0 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /assets/editor_examples/active_area_all.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": 0, 3 | "active_area": { 4 | "presets": [ 5 | { 6 | "id": 0, 7 | "left": 0, 8 | "right": 0, 9 | "top": 210, 10 | "bottom": 210 11 | } 12 | ], 13 | "edits": { 14 | "all": 0 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /assets/editor_examples/crop.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": 2, 3 | "active_area": { 4 | "crop": true 5 | } 6 | } -------------------------------------------------------------------------------- /assets/editor_examples/duplicate.json: -------------------------------------------------------------------------------- 1 | { 2 | "duplicate": [ 3 | { 4 | "source": 0, 5 | "offset": 39, 6 | "length": 10 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /assets/editor_examples/fel_to_p81_encode.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": 2, 3 | "remove_mapping": true, 4 | "active_area": { 5 | "crop": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /assets/editor_examples/l9_and_l11.json: -------------------------------------------------------------------------------- 1 | { 2 | "level9": "BT.2020", 3 | "level11": { 4 | "content_type": 1, 5 | "whitepoint": 0, 6 | "reference_mode_flag": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /assets/editor_examples/level6_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "level6": { 3 | "max_display_mastering_luminance": 1000, 4 | "min_display_mastering_luminance": 1, 5 | "max_content_light_level": 1000, 6 | "max_frame_average_light_level": 400 7 | } 8 | } -------------------------------------------------------------------------------- /assets/editor_examples/levels.json: -------------------------------------------------------------------------------- 1 | { 2 | "min_pq": 7, 3 | "max_pq": 3079 4 | } -------------------------------------------------------------------------------- /assets/editor_examples/mode.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": 2 3 | } -------------------------------------------------------------------------------- /assets/editor_examples/p5_to_p81.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": 3 3 | } -------------------------------------------------------------------------------- /assets/editor_examples/remove.json: -------------------------------------------------------------------------------- 1 | { 2 | "remove": [ 3 | "0-39" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /assets/editor_examples/remove_cmv4.json: -------------------------------------------------------------------------------- 1 | { 2 | "remove_cmv4": true 3 | } 4 | -------------------------------------------------------------------------------- /assets/editor_examples/scene_cuts.json: -------------------------------------------------------------------------------- 1 | { 2 | "scene_cuts": { 3 | "all": true, 4 | "0-39": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /assets/editor_examples/source_rpu.json: -------------------------------------------------------------------------------- 1 | { 2 | "source_rpu": "assets/tests/cmv40_full_rpu.bin", 3 | "rpu_levels": [1, 4, 6] 4 | } 5 | -------------------------------------------------------------------------------- /assets/generator_examples/default_cmv29.json: -------------------------------------------------------------------------------- 1 | { 2 | "cm_version": "V29", 3 | "length": 10, 4 | "level6": { 5 | "max_display_mastering_luminance": 1000, 6 | "min_display_mastering_luminance": 1, 7 | "max_content_light_level": 1000, 8 | "max_frame_average_light_level": 400 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /assets/generator_examples/default_cmv40.json: -------------------------------------------------------------------------------- 1 | { 2 | "cm_version": "V40", 3 | "length": 10, 4 | "level6": { 5 | "max_display_mastering_luminance": 1000, 6 | "min_display_mastering_luminance": 1, 7 | "max_content_light_level": 1000, 8 | "max_frame_average_light_level": 400 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /assets/generator_examples/full_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "cm_version": "V40", 3 | "profile": "8.1", 4 | "long_play_mode": false, 5 | "level5": { 6 | "active_area_left_offset": 0, 7 | "active_area_right_offset": 0, 8 | "active_area_top_offset": 40, 9 | "active_area_bottom_offset": 40 10 | }, 11 | "level6": { 12 | "max_display_mastering_luminance": 1000, 13 | "min_display_mastering_luminance": 1, 14 | "max_content_light_level": 1000, 15 | "max_frame_average_light_level": 400 16 | }, 17 | "default_metadata_blocks": [ 18 | { 19 | "Level2": { 20 | "target_max_pq": 3079, 21 | "trim_slope": 2048, 22 | "trim_offset": 2048, 23 | "trim_power": 2048, 24 | "trim_chroma_weight": 2048, 25 | "trim_saturation_gain": 2048, 26 | "ms_weight": 2048 27 | } 28 | }, 29 | { 30 | "Level9": { 31 | "length": 1, 32 | "source_primary_index": 0 33 | } 34 | }, 35 | { 36 | "Level11": { 37 | "content_type": 4, 38 | "whitepoint": 0, 39 | "reference_mode_flag": true 40 | } 41 | } 42 | ], 43 | "shots": [ 44 | { 45 | "start": 0, 46 | "duration": 10, 47 | "metadata_blocks": [ 48 | { 49 | "Level1": { 50 | "min_pq": 2, 51 | "max_pq": 2938, 52 | "avg_pq": 1456 53 | } 54 | }, 55 | { 56 | "Level2": { 57 | "target_max_pq": 2851, 58 | "trim_slope": 2048, 59 | "trim_offset": 2048, 60 | "trim_power": 1800, 61 | "trim_chroma_weight": 2048, 62 | "trim_saturation_gain": 2048, 63 | "ms_weight": 2048 64 | } 65 | } 66 | ], 67 | "frame_edits": [ 68 | { 69 | "edit_offset": 5, 70 | "metadata_blocks": [ 71 | { 72 | "Level1": { 73 | "min_pq": 0, 74 | "max_pq": 3079, 75 | "avg_pq": 1229 76 | } 77 | } 78 | ] 79 | } 80 | ] 81 | } 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /assets/generator_examples/l1_cmv29.json: -------------------------------------------------------------------------------- 1 | { 2 | "cm_version": "V29", 3 | "level6": { 4 | "max_display_mastering_luminance": 1000, 5 | "min_display_mastering_luminance": 1, 6 | "max_content_light_level": 1000, 7 | "max_frame_average_light_level": 400 8 | }, 9 | "shots": [ 10 | { 11 | "start": 0, 12 | "duration": 1, 13 | "metadata_blocks": [ 14 | { 15 | "Level1": { 16 | "min_pq": 0, 17 | "max_pq": 1500, 18 | "avg_pq": 506 19 | } 20 | } 21 | ] 22 | }, 23 | { 24 | "start": 1, 25 | "duration": 1, 26 | "metadata_blocks": [ 27 | { 28 | "Level1": { 29 | "min_pq": 0, 30 | "max_pq": 2604, 31 | "avg_pq": 1340 32 | } 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /assets/generator_examples/l1_cmv29_override_avg_cmv40.json: -------------------------------------------------------------------------------- 1 | { 2 | "cm_version": "V29", 3 | "l1_avg_pq_cm_version": "V40", 4 | "level6": { 5 | "max_display_mastering_luminance": 1000, 6 | "min_display_mastering_luminance": 1, 7 | "max_content_light_level": 1000, 8 | "max_frame_average_light_level": 400 9 | }, 10 | "shots": [ 11 | { 12 | "start": 0, 13 | "duration": 1, 14 | "metadata_blocks": [ 15 | { 16 | "Level1": { 17 | "min_pq": 0, 18 | "max_pq": 1678, 19 | "avg_pq": 948 20 | } 21 | } 22 | ] 23 | }, 24 | { 25 | "start": 1, 26 | "duration": 1, 27 | "metadata_blocks": [ 28 | { 29 | "Level1": { 30 | "min_pq": 0, 31 | "max_pq": 3074, 32 | "avg_pq": 1450 33 | } 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /assets/generator_examples/l1_cmv40.json: -------------------------------------------------------------------------------- 1 | { 2 | "level6": { 3 | "max_display_mastering_luminance": 1000, 4 | "min_display_mastering_luminance": 1, 5 | "max_content_light_level": 1000, 6 | "max_frame_average_light_level": 400 7 | }, 8 | "shots": [ 9 | { 10 | "start": 0, 11 | "duration": 1, 12 | "metadata_blocks": [ 13 | { 14 | "Level1": { 15 | "min_pq": 0, 16 | "max_pq": 1678, 17 | "avg_pq": 948 18 | } 19 | } 20 | ] 21 | }, 22 | { 23 | "start": 1, 24 | "duration": 1, 25 | "metadata_blocks": [ 26 | { 27 | "Level1": { 28 | "min_pq": 0, 29 | "max_pq": 3074, 30 | "avg_pq": 1450 31 | } 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /assets/generator_examples/no_duration.json: -------------------------------------------------------------------------------- 1 | { 2 | "cm_version": "V40", 3 | "level6": { 4 | "max_display_mastering_luminance": 1000, 5 | "min_display_mastering_luminance": 1, 6 | "max_content_light_level": 1000, 7 | "max_frame_average_light_level": 400 8 | }, 9 | "default_metadata_blocks": [ 10 | { 11 | "Level1": { 12 | "min_pq": 0, 13 | "max_pq": 123, 14 | "avg_pq": 39 15 | } 16 | }, 17 | { 18 | "Level9": { 19 | "length": 1, 20 | "source_primary_index": 0 21 | } 22 | } 23 | ], 24 | "shots": [ 25 | { 26 | "start": 0, 27 | "duration": 0, 28 | "metadata_blocks": [ 29 | { 30 | "Level1": { 31 | "min_pq": 0, 32 | "max_pq": 123, 33 | "avg_pq": 39 34 | } 35 | }, 36 | { 37 | "Level2": { 38 | "target_max_pq": 2851, 39 | "trim_slope": 2048, 40 | "trim_offset": 2048, 41 | "trim_power": 1800, 42 | "trim_chroma_weight": 2048, 43 | "trim_saturation_gain": 2048, 44 | "ms_weight": 2048 45 | } 46 | } 47 | ] 48 | }, 49 | { 50 | "start": 0, 51 | "duration": 0, 52 | "metadata_blocks": [ 53 | { 54 | "Level2": { 55 | "target_max_pq": 2851, 56 | "trim_slope": 1400, 57 | "trim_offset": 1234, 58 | "trim_power": 1800, 59 | "trim_chroma_weight": 2048, 60 | "trim_saturation_gain": 2048, 61 | "ms_weight": 2048 62 | } 63 | }, 64 | { 65 | "Level5": { 66 | "active_area_left_offset": 0, 67 | "active_area_right_offset": 0, 68 | "active_area_top_offset": 276, 69 | "active_area_bottom_offset": 276 70 | } 71 | } 72 | ], 73 | "frame_edits": [ 74 | { 75 | "edit_offset": 2, 76 | "metadata_blocks": [ 77 | { 78 | "Level1": { 79 | "min_pq": 0, 80 | "max_pq": 3079, 81 | "avg_pq": 1229 82 | } 83 | }, 84 | { 85 | "Level2": { 86 | "target_max_pq": 2851, 87 | "trim_slope": 1999, 88 | "trim_offset": 1999, 89 | "trim_power": 1999, 90 | "trim_chroma_weight": 2048, 91 | "trim_saturation_gain": 2048, 92 | "ms_weight": 2048 93 | } 94 | }, 95 | { 96 | "Level2": { 97 | "target_max_pq": 3079, 98 | "trim_slope": 2048, 99 | "trim_offset": 2048, 100 | "trim_power": 2048, 101 | "trim_chroma_weight": 2048, 102 | "trim_saturation_gain": 2048, 103 | "ms_weight": 2048 104 | } 105 | } 106 | ] 107 | } 108 | ] 109 | } 110 | ] 111 | } 112 | -------------------------------------------------------------------------------- /assets/hevc_tests/no_aud_bl.hevc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/hevc_tests/no_aud_bl.hevc -------------------------------------------------------------------------------- /assets/hevc_tests/no_aud_injected.hevc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/hevc_tests/no_aud_injected.hevc -------------------------------------------------------------------------------- /assets/hevc_tests/no_aud_muxed.hevc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/hevc_tests/no_aud_muxed.hevc -------------------------------------------------------------------------------- /assets/hevc_tests/regular.hevc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/hevc_tests/regular.hevc -------------------------------------------------------------------------------- /assets/hevc_tests/regular.mkv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/hevc_tests/regular.mkv -------------------------------------------------------------------------------- /assets/hevc_tests/regular_bl_start_code_4.hevc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/hevc_tests/regular_bl_start_code_4.hevc -------------------------------------------------------------------------------- /assets/hevc_tests/regular_bl_start_code_4_shorter.hevc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/hevc_tests/regular_bl_start_code_4_shorter.hevc -------------------------------------------------------------------------------- /assets/hevc_tests/regular_convert_annexb.hevc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/hevc_tests/regular_convert_annexb.hevc -------------------------------------------------------------------------------- /assets/hevc_tests/regular_demux_bl_annexb.hevc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/hevc_tests/regular_demux_bl_annexb.hevc -------------------------------------------------------------------------------- /assets/hevc_tests/regular_inject_annexb.hevc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/hevc_tests/regular_inject_annexb.hevc -------------------------------------------------------------------------------- /assets/hevc_tests/regular_rpu.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/hevc_tests/regular_rpu.bin -------------------------------------------------------------------------------- /assets/hevc_tests/regular_rpu_mel.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/hevc_tests/regular_rpu_mel.bin -------------------------------------------------------------------------------- /assets/hevc_tests/regular_start_code_4.hevc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/hevc_tests/regular_start_code_4.hevc -------------------------------------------------------------------------------- /assets/hevc_tests/regular_start_code_4_muxed_el.hevc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/hevc_tests/regular_start_code_4_muxed_el.hevc -------------------------------------------------------------------------------- /assets/hevc_tests/regular_start_code_4_shorter_muxed_el.hevc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/hevc_tests/regular_start_code_4_shorter_muxed_el.hevc -------------------------------------------------------------------------------- /assets/hevc_tests/sei-double-3byte-case.hevc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/hevc_tests/sei-double-3byte-case.hevc -------------------------------------------------------------------------------- /assets/hevc_tests/sei-double-3byte-start-code-4.hevc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/hevc_tests/sei-double-3byte-start-code-4.hevc -------------------------------------------------------------------------------- /assets/hevc_tests/sei-suffix-muxed-rpu.hevc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/hevc_tests/sei-suffix-muxed-rpu.hevc -------------------------------------------------------------------------------- /assets/hevc_tests/yusesope_regular_muxed.hevc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/hevc_tests/yusesope_regular_muxed.hevc -------------------------------------------------------------------------------- /assets/tests/cmv2_9_xml_with_l5_rpu.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/cmv2_9_xml_with_l5_rpu.bin -------------------------------------------------------------------------------- /assets/tests/cmv40_full_rpu.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/cmv40_full_rpu.bin -------------------------------------------------------------------------------- /assets/tests/cmv4_0_2_custom_displays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.2 4 | 5 | 6 | 2020-02-17T11:29:59Z 7 | Blackmagic Design 8 | DaVinci Resolve Studio 9 | 16.1.2.026 10 | 11 | 12 | 13 | 14 | Black 15 | 118cbebe-317f-4d84-9b10-604289433563 16 | 1 17 | 1.77778 18 | 1.77778 19 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /assets/tests/cmv4_0_2_custom_displays_xml_rpu.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/cmv4_0_2_custom_displays_xml_rpu.bin -------------------------------------------------------------------------------- /assets/tests/cmv4_0_2_xml_rpu.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/cmv4_0_2_xml_rpu.bin -------------------------------------------------------------------------------- /assets/tests/cmv4_0_2_xml_with_l5_rpu.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/cmv4_0_2_xml_with_l5_rpu.bin -------------------------------------------------------------------------------- /assets/tests/cmv4_2_510_xml_rpu.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/cmv4_2_510_xml_rpu.bin -------------------------------------------------------------------------------- /assets/tests/data_before_crc32.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/data_before_crc32.bin -------------------------------------------------------------------------------- /assets/tests/empty_dmv1_blocks.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/empty_dmv1_blocks.bin -------------------------------------------------------------------------------- /assets/tests/eof_rpu.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/eof_rpu.bin -------------------------------------------------------------------------------- /assets/tests/fel_orig.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/fel_orig.bin -------------------------------------------------------------------------------- /assets/tests/fel_rpu.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/fel_rpu.bin -------------------------------------------------------------------------------- /assets/tests/fel_to_81.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/fel_to_81.bin -------------------------------------------------------------------------------- /assets/tests/fel_to_mel.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/fel_to_mel.bin -------------------------------------------------------------------------------- /assets/tests/fix_se_write.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/fix_se_write.bin -------------------------------------------------------------------------------- /assets/tests/mel_orig.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/mel_orig.bin -------------------------------------------------------------------------------- /assets/tests/mel_rpu.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/mel_rpu.bin -------------------------------------------------------------------------------- /assets/tests/mel_to_81.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/mel_to_81.bin -------------------------------------------------------------------------------- /assets/tests/mel_to_mel.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/mel_to_mel.bin -------------------------------------------------------------------------------- /assets/tests/mel_variable_l8_length13.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/mel_variable_l8_length13.bin -------------------------------------------------------------------------------- /assets/tests/p8_001_end_crc32.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/p8_001_end_crc32.bin -------------------------------------------------------------------------------- /assets/tests/poly_coef_int_logic.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/poly_coef_int_logic.bin -------------------------------------------------------------------------------- /assets/tests/profile20_apple.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/profile20_apple.bin -------------------------------------------------------------------------------- /assets/tests/profile4.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/profile4.bin -------------------------------------------------------------------------------- /assets/tests/profile5-02.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/profile5-02.bin -------------------------------------------------------------------------------- /assets/tests/profile5.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/profile5.bin -------------------------------------------------------------------------------- /assets/tests/profile8.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/profile8.bin -------------------------------------------------------------------------------- /assets/tests/profile84.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/profile84.bin -------------------------------------------------------------------------------- /assets/tests/profile8_from_profile5-02.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/profile8_from_profile5-02.bin -------------------------------------------------------------------------------- /assets/tests/source_p5_to_p8_001_end_crc32.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/source_p5_to_p8_001_end_crc32.bin -------------------------------------------------------------------------------- /assets/tests/source_rpu_replaced_fel_orig.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/source_rpu_replaced_fel_orig.bin -------------------------------------------------------------------------------- /assets/tests/st2094_10_level3.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/st2094_10_level3.bin -------------------------------------------------------------------------------- /assets/tests/trailing_bytes_rpu.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/trailing_bytes_rpu.bin -------------------------------------------------------------------------------- /assets/tests/unordered_l8_blocks.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/dovi_tool/4fd2b2235c9f93582dd4a00e65ee34a07800afd7/assets/tests/unordered_l8_blocks.bin -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use vergen_gitcl::{Emitter, GitclBuilder}; 3 | 4 | fn main() -> Result<()> { 5 | let gitcl = GitclBuilder::default() 6 | .describe(true, true, Some("[0-9]*")) 7 | .build()?; 8 | 9 | let gitcl_res = Emitter::default() 10 | .idempotent() 11 | .fail_on_error() 12 | .add_instructions(&gitcl) 13 | .and_then(|emitter| emitter.emit()); 14 | 15 | if let Err(e) = gitcl_res { 16 | eprintln!("error occured while generating instructions: {e:?}"); 17 | Emitter::default().idempotent().fail_on_error().emit()?; 18 | } 19 | 20 | Ok(()) 21 | } 22 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | Documentation related to `dovi_tool` usage 2 | 3 | #### [Editor configuration](editor.md) 4 | #### [Generator configuration](generator.md) 5 | #### [Information about Dolby Vision profiles](profiles.md) 6 | -------------------------------------------------------------------------------- /docs/editor.md: -------------------------------------------------------------------------------- 1 | The RPUs can be edited in two ways: 2 | - Using the **`editor`** subcommand. 3 | - Using the **`--edit-config`** option when using HEVC commands. 4 | 5 | When doing HEVC operations, some capabilities are not supported: 6 | - Editing the active area for specific ranges of frames. Only `"all"` edit is supported. 7 | - Removing or duplicating RPUs. 8 | - Editing scene cuts. 9 | - Replacing metadata from a second RPU file. 10 | 11 |   12 | 13 | The editor expects a JSON config like the example below: 14 | ```json5 15 | { 16 | // Mode to convert the RPU (refer to README) 17 | "mode": int, 18 | 19 | // Removes CM v4.0 from the RPU: 20 | // - L3, L8, L9, L10 and L11 are removed 21 | // - DM v2 metadata is removed, along with L254 22 | "remove_cmv4": boolean, 23 | 24 | // Whether to remove polynomial/MMR mapping coefficients from the metadata 25 | "remove_mapping": boolean, 26 | 27 | // Source min/max PQ values to override 28 | "min_pq": int, 29 | "max_pq": int, 30 | 31 | // Configuration for active area edits 32 | // If no L5 metadata is present in the RPU, L5 metadata is inserted 33 | "active_area": { 34 | // Should be set to true when final video has no letterbox bars 35 | "crop": boolean, 36 | 37 | // Optional, specifies whether to drop some or all L5 metadata. 38 | // This produces spec non conformant RPUs. 39 | // Possible options: "all", "zeroes" 40 | // "zeroes" drops the L5 metadata blocks which have all offsets set to zero. 41 | "drop_l5": string, 42 | 43 | // List of presets to add letterbox bars 44 | "presets": [ 45 | { 46 | "id": int, 47 | "left": int, 48 | "right": int, 49 | "top": int, 50 | "bottom": int 51 | } 52 | ], 53 | 54 | // List of edits 55 | "edits": { 56 | // All or a specific range of frames (inclusive) to use preset for 57 | // Edits before an "all" key can be overriden 58 | "all": presetId, 59 | "0-39": presetId 60 | } 61 | }, 62 | 63 | // List of frames or frame ranges to remove (inclusive) 64 | // Frames are removed before the duplicate passes 65 | "remove": [ 66 | "0-39" 67 | ], 68 | 69 | // List of duplicate operations 70 | "duplicate": [ 71 | { 72 | // Frame to use as metadata source 73 | "source": int, 74 | // Index at which the duplicated frames are added (inclusive) 75 | "offset": int, 76 | // Number of frames to duplicate 77 | "length": int 78 | } 79 | ], 80 | 81 | // Set the scene cut (scene_refresh_flag) flag for specific frame ranges 82 | // Range options: "all" or formatted as "start-end" 83 | "scene_cuts": { 84 | "all": true, 85 | "0-39": false 86 | }, 87 | 88 | // Level 6, ST2086 fallback metadata 89 | // Optional 90 | // Replaces existing L6 metadata values. 91 | // Otherwise, creates the L6 metadata block. 92 | "level6": { 93 | "max_display_mastering_luminance": int, 94 | "min_display_mastering_luminance": int, 95 | "max_content_light_level": int, 96 | "max_frame_average_light_level": int 97 | }, 98 | 99 | 100 | // Level 9 Mastering Display Primaries 101 | // Optional, replaces existing L9. 102 | // The RPU must already be CM v4.0 for this to have any effect 103 | // 104 | // String value, must match enum. 105 | // Default: "DCIP3D65". 106 | "level9": MasteringDisplayPrimaries, 107 | 108 | // Level 11 Content type metadata 109 | // Optional, replaces existing L11 110 | // The RPU must already be CM v4.0 for this to have any effect 111 | "level11": { 112 | // 1 = Cinema, 2 = Games, 3 = Sports, 4 = User generated content 113 | "content_type": int, 114 | 115 | // WP * 375 + 6504 116 | // D65 = 0 117 | "whitepoint": int, 118 | 119 | // Whether to force reference mode or not. 120 | "reference_mode_flag": boolean 121 | }, 122 | 123 | // Level 255 extension block structure 124 | "level255": ExtMetadataBlockLevel255, 125 | 126 | // Source RPU file to use metadata from 127 | // The RPUs must have the same length, after the `remove` pass. 128 | // 129 | // Path must be absolute. 130 | "source_rpu": string, 131 | 132 | // Levels to replace using metadata from `source_rpu` 133 | // List of integers representing block levels 134 | "rpu_levels": int[], 135 | 136 | // EXPERIMENTAL: Allows transferring CM v4.0 levels (L3, L8, L9, L10, L11, L254) to CM v2.9 RPU. 137 | // This implicitly converts the input RPU to CM v4.0 format, with default L254. 138 | // 139 | // WARNING: This is not normally allowed and may lead to playback issues (e.g., if L8 is not transferred). 140 | // Intended for advanced/experimental workflows only. Use at your own risk. 141 | // 142 | // Default: false 143 | "allow_cmv4_transfer": boolean, 144 | } 145 | ``` 146 | -------------------------------------------------------------------------------- /docs/generator.md: -------------------------------------------------------------------------------- 1 | The generator can create a profile 8.1 or 8.4 RPU binary. 2 | 3 | Any extension metadata can be added, but adding blocks is for advanced usage. 4 | Ideally, most custom blocks usage should be scripted, especially when shots are involved. 5 | For the structure expected for deserialization, see [metadata blocks](../dolby_vision/src/rpu/extension_metadata/blocks). 6 | The expected JSON format is the same as output by the `info` subcommand. 7 | 8 | A JSON config example: 9 | 10 | ```json5 11 | { 12 | // CM version, either "V29" or "V40". 13 | // Defaults to "V40". 14 | "cm_version": string, 15 | 16 | // Profile to generate 17 | // - 8.1: HDR10 base layer (default) 18 | // - 8.4: HLG base layer with static reshaping 19 | "profile": string, 20 | 21 | // Number of metadata frames to generate. 22 | // Optional if shots are specified, as well as for HDR10+ and madVR sourced generation. 23 | "length": int, 24 | 25 | // Source min/max PQ values to override, optional. 26 | // If not specified, derived from L6 metadata. 27 | "source_min_pq": int, 28 | "source_max_pq": int, 29 | 30 | // CM version to override the minimum L1 `avg_pq` 31 | "l1_avg_pq_cm_version": string, 32 | 33 | // L5 metadata, optional. 34 | // If not specified, L5 metadata is added with 0 offsets. 35 | "level5": { 36 | "active_area_left_offset": int, 37 | "active_area_right_offset": int, 38 | "active_area_top_offset": int, 39 | "active_area_bottom_offset": int, 40 | }, 41 | 42 | // L6 metadata, required for profile 8.1. 43 | "level6": { 44 | "max_display_mastering_luminance": int, 45 | "min_display_mastering_luminance": int, 46 | "max_content_light_level": int, 47 | "max_frame_average_light_level": int, 48 | }, 49 | 50 | // Metadata blocks that should be present in every RPU of the sequence. 51 | // Does not accept L5, L6 and L254 metadata. 52 | // Disallowed blocks are simply ignored. 53 | // 54 | // For HDR10+ or madVR generation, the default L1 metadata is replaced. 55 | // 56 | // Refer to assets/generator_examples/full_example.json 57 | "default_metadata_blocks": Array, 58 | 59 | // Shots to specify metadata. 60 | // Array of VideoShot objects. 61 | // 62 | // For HDR10+ or madVR generation: 63 | // - The metadata is taken from the shots in the list order. 64 | // This means that both start and duration can be 0. 65 | // - It is expected that the source metadata has the same number of shots as this list. 66 | // Missing or extra shots are ignored. 67 | // 68 | // Refer to generator examples. 69 | "shots": [ 70 | { 71 | // Start frame. 72 | "start": int, 73 | // Shot frame length. 74 | "duration": int, 75 | 76 | // List of metadata blocks to use for this shot. 77 | "metadata_blocks": Array, 78 | 79 | // Metadata to use for specific frames in the shot. 80 | "frame_edits": [ 81 | { 82 | // Frame offset to edit in the shot. 83 | "edit_offset": int, 84 | 85 | // List of metadata blocks to use for the frame. 86 | "metadata_blocks": Array, 87 | } 88 | ] 89 | } 90 | ], 91 | } 92 | ``` 93 | -------------------------------------------------------------------------------- /docs/profiles.md: -------------------------------------------------------------------------------- 1 | Differentiating between Dolby Vision profiles. 2 | ##### Profile 4 3 | Possibly `vdr_bit_depth_minus8` > 4 4 | ##### Profile 5 5 | `vdr_rpu_profile = 0` 6 | `bl_video_full_range_flag = 0` 7 | ##### Profile 7 8 | `vdr_rpu_profile = 1` 9 | `el_spatial_resampling_filter_flag = 1` 10 | `disable_residual_flag = 0` 11 | ##### Profile 8 12 | `vdr_rpu_profile = 1` 13 | `el_spatial_resampling_filter_flag = 0` 14 | -------------------------------------------------------------------------------- /dolby_vision/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | **/*.rs.bk 3 | /*.mkv 4 | /*.hevc 5 | /*.txt 6 | /*.vpy 7 | /*.bin 8 | /*.ffindex 9 | /*.json 10 | /*.xml 11 | -------------------------------------------------------------------------------- /dolby_vision/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dolby_vision" 3 | version = "3.3.2" 4 | authors = ["quietvoid"] 5 | edition = "2024" 6 | rust-version = "1.85.0" 7 | license = "MIT" 8 | description = "Dolby Vision metadata parsing and writing" 9 | repository = "https://github.com/quietvoid/dovi_tool/tree/main/dolby_vision" 10 | 11 | [dependencies] 12 | bitvec_helpers = { version = "3.1.6", default-features = false, features = ["bitstream-io"] } 13 | anyhow = "1.0.98" 14 | bitvec = "1.0.1" 15 | crc = "3.3.0" 16 | roxmltree = { version = "0.20.0", optional = true } 17 | serde = { version = "1.0.219", features = ["derive"], "optional" = true } 18 | serde_json = { version = "1.0.140", features = ["preserve_order"], "optional" = true } 19 | tinyvec = { version = "1.9.0", features = ["rustc_1_55"] } 20 | 21 | libc = { version = "0.2", optional = true } 22 | 23 | [dev-dependencies] 24 | criterion = "0.6.0" 25 | 26 | [features] 27 | xml = ["roxmltree"] 28 | serde = ["dep:serde", "dep:serde_json", "tinyvec/serde"] 29 | capi = ["libc"] 30 | 31 | [package.metadata.docs.rs] 32 | all-features = true 33 | 34 | [package.metadata.capi.header] 35 | subdirectory = "libdovi" 36 | name = "rpu_parser" 37 | 38 | [package.metadata.capi.pkg_config] 39 | strip_include_path_components = 1 40 | subdirectory = false 41 | name = "dovi" 42 | filename = "dovi" 43 | 44 | [package.metadata.capi.library] 45 | rustflags = "-Cpanic=abort" 46 | name = "dovi" 47 | 48 | [lib] 49 | doctest = false 50 | 51 | [[bench]] 52 | name = "bench_main" 53 | harness = false 54 | 55 | [lints.rust] 56 | unexpected_cfgs = { level = "warn", check-cfg = ['cfg(cargo_c)'] } 57 | 58 | [profile.release-deploy] 59 | inherits = "release" 60 | lto = "thin" 61 | strip = "symbols" 62 | -------------------------------------------------------------------------------- /dolby_vision/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 quietvoid 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dolby_vision/README.md: -------------------------------------------------------------------------------- 1 | Library to read & write Dolby Vision metadata. 2 | Comes as a Rust crate and C compatible library. 3 | 4 | See [changelog](CHANGELOG.md) for API changes. 5 | 6 |   7 | 8 | ### Toolchain 9 | 10 | The minimum Rust version to use `dolby_vision` is 1.85.0. 11 | 12 |   13 | 14 | ### `libdovi`, C-API 15 | 16 | Packages 17 | - **Arch Linux**: available on the AUR, `libdovi` or `libdovi-git`. 18 | 19 |   20 | 21 | #### Building the library 22 | 23 | `libdovi` comes as a C compatible library. 24 | To build and install it you can use [cargo-c](https://crates.io/crates/cargo-c): 25 | 26 | ```sh 27 | cargo install cargo-c 28 | cargo cinstall --release 29 | ``` 30 | 31 | #### Running the C-API example 32 | ```sh 33 | cd examples 34 | gcc capi_rpu_file.c -ldovi -o capi_example.o 35 | ./capi_example.o 36 | ``` 37 | -------------------------------------------------------------------------------- /dolby_vision/benches/bench_main.rs: -------------------------------------------------------------------------------- 1 | use criterion::criterion_main; 2 | 3 | mod benchmarks; 4 | 5 | criterion_main! { 6 | benchmarks::parsing::parse_rpus, 7 | benchmarks::rewriting::rewrite_rpus 8 | } 9 | -------------------------------------------------------------------------------- /dolby_vision/benches/benchmarks/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod parsing; 2 | pub mod rewriting; 3 | -------------------------------------------------------------------------------- /dolby_vision/benches/benchmarks/parsing.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::Read, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | use criterion::{Criterion, criterion_group}; 8 | use dolby_vision::rpu::dovi_rpu::DoviRpu; 9 | 10 | const RPU_FILES: &[&str] = &[ 11 | "profile5.bin", 12 | "profile8.bin", 13 | "fel_orig.bin", 14 | "mel_variable_l8_length13.bin", 15 | "cmv40_full_rpu.bin", 16 | "unordered_l8_blocks.bin", 17 | ]; 18 | 19 | fn get_bytes>(path: P) -> Vec { 20 | let mut buf = Vec::with_capacity(500); 21 | File::open(path).unwrap().read_to_end(&mut buf).unwrap(); 22 | 23 | buf 24 | } 25 | 26 | pub fn parse_single_unspec62_nalu(data: &[u8]) { 27 | DoviRpu::parse_unspec62_nalu(data).unwrap(); 28 | } 29 | 30 | fn parse_single_unspec62_nalu_benchmark(c: &mut Criterion) { 31 | let lib_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 32 | let assets_path = lib_path.parent().unwrap().join("assets/tests"); 33 | 34 | let mut group = c.benchmark_group("parse_single_unspec62_nalu"); 35 | 36 | for file in RPU_FILES { 37 | let bytes = get_bytes(assets_path.join(file)); 38 | 39 | group.bench_function(*file, |b| b.iter(|| parse_single_unspec62_nalu(&bytes))); 40 | } 41 | } 42 | 43 | criterion_group!(parse_rpus, parse_single_unspec62_nalu_benchmark); 44 | -------------------------------------------------------------------------------- /dolby_vision/benches/benchmarks/rewriting.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::Read, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | use criterion::{Criterion, criterion_group}; 8 | use dolby_vision::rpu::dovi_rpu::DoviRpu; 9 | 10 | const RPU_FILES: &[&str] = &["fel_orig.bin", "mel_variable_l8_length13.bin"]; 11 | 12 | fn get_bytes>(path: P) -> Vec { 13 | let mut buf = Vec::with_capacity(500); 14 | File::open(path).unwrap().read_to_end(&mut buf).unwrap(); 15 | 16 | buf 17 | } 18 | 19 | pub fn rewrite_single_unspec62_nalu(data: &[u8]) { 20 | let mut rpu = DoviRpu::parse_unspec62_nalu(data).unwrap(); 21 | rpu.convert_with_mode(2).unwrap(); 22 | 23 | rpu.write_hevc_unspec62_nalu().unwrap(); 24 | } 25 | 26 | fn rewrite_single_unspec62_nalu_benchmark(c: &mut Criterion) { 27 | let lib_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 28 | let assets_path = lib_path.parent().unwrap().join("assets/tests"); 29 | 30 | let mut group = c.benchmark_group("rewrite_single_unspec62_nalu"); 31 | 32 | for file in RPU_FILES { 33 | let bytes = get_bytes(assets_path.join(file)); 34 | 35 | group.bench_function(*file, |b| b.iter(|| rewrite_single_unspec62_nalu(&bytes))); 36 | } 37 | } 38 | 39 | criterion_group!(rewrite_rpus, rewrite_single_unspec62_nalu_benchmark); 40 | -------------------------------------------------------------------------------- /dolby_vision/cbindgen.toml: -------------------------------------------------------------------------------- 1 | header = "// SPDX-License-Identifier: MIT" 2 | sys_includes = ["stddef.h", "stdint.h", "stdlib.h", "stdbool.h"] 3 | no_includes = true 4 | include_guard = "DOVI_H" 5 | tab_width = 4 6 | style = "Type" 7 | language = "C" 8 | cpp_compat = true 9 | 10 | [parse] 11 | parse_deps = false 12 | 13 | [export] 14 | item_types = ["constants", "enums", "structs", "unions", "typedefs", "opaque", "functions"] 15 | prefix = "Dovi" 16 | -------------------------------------------------------------------------------- /dolby_vision/examples/capi_rpu_file.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "helpers.h" 6 | 7 | int main(void) { 8 | char *path = "../../assets/hevc_tests/regular_rpu_mel.bin"; 9 | int ret; 10 | 11 | const DoviRpuOpaqueList *rpus = dovi_parse_rpu_bin_file(path); 12 | if (rpus->error) { 13 | printf("%s\n", rpus->error); 14 | 15 | dovi_rpu_list_free(rpus); 16 | return 1; 17 | } 18 | 19 | printf("Parsed RPU file: %d frames\n", rpus->len); 20 | 21 | // All the RPUs are valid at this point 22 | DoviRpuOpaque *rpu = rpus->list[0]; 23 | 24 | const DoviRpuDataHeader *header = dovi_rpu_get_header(rpu); 25 | 26 | // Process the RPU.. 27 | ret = process_rpu_info(rpu, header); 28 | if (ret < 0) { 29 | const char *error = dovi_rpu_get_error(rpu); 30 | printf("%s\n", error); 31 | } 32 | 33 | // Free everything 34 | dovi_rpu_free_header(header); 35 | dovi_rpu_list_free(rpus); 36 | } 37 | -------------------------------------------------------------------------------- /dolby_vision/examples/capi_single_rpu.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "helpers.h" 6 | 7 | int main(void) { 8 | char *path = "../../assets/tests/cmv40_full_rpu.bin"; 9 | int ret; 10 | 11 | size_t length; 12 | const uint8_t *buf = read_rpu_file(path, &length); 13 | 14 | DoviRpuOpaque *rpu = dovi_parse_unspec62_nalu(buf, length); 15 | free((void *) buf); 16 | 17 | // The RPU header is always present 18 | const DoviRpuDataHeader *header = dovi_rpu_get_header(rpu); 19 | if (!header) { 20 | const char *error = dovi_rpu_get_error(rpu); 21 | printf("%s\n", error); 22 | 23 | dovi_rpu_free(rpu); 24 | return 1; 25 | } 26 | 27 | // Process the RPU.. 28 | ret = process_rpu_info(rpu, header); 29 | if (ret < 0) { 30 | const char *error = dovi_rpu_get_error(rpu); 31 | printf("%s\n", error); 32 | } 33 | 34 | // Free everything 35 | dovi_rpu_free_header(header); 36 | dovi_rpu_free(rpu); 37 | } 38 | -------------------------------------------------------------------------------- /dolby_vision/examples/simple_edit.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | extern "C" 8 | { 9 | #include "helpers.h" 10 | } 11 | 12 | int main(void) { 13 | std::ifstream input("../../assets/tests/fel_orig.bin", std::ios::binary); 14 | 15 | const std::vector buf( 16 | (std::istreambuf_iterator(input)), 17 | (std::istreambuf_iterator())); 18 | 19 | input.close(); 20 | 21 | auto start = std::chrono::high_resolution_clock::now(); 22 | 23 | DoviRpuOpaque *rpu = dovi_parse_unspec62_nalu(buf.data(), buf.size()); 24 | const DoviRpuDataHeader *header = dovi_rpu_get_header(rpu); 25 | 26 | // Only converts profile 7 as they are guaranteed to be HDR10 base 27 | if (header && header->guessed_profile == 7) { 28 | // Convert the base to 8.1 compatible 29 | // Also handles removing mapping for FEL 30 | int ret = dovi_convert_rpu_with_mode(rpu, 2); 31 | 32 | // Final video has letterboxing completely cropped 33 | ret = dovi_rpu_set_active_area_offsets(rpu, 0, 0, 0, 0); 34 | 35 | const DoviData *rpu_payload = dovi_write_unspec62_nalu(rpu); 36 | 37 | // Do something with the edited payload 38 | dovi_data_free(rpu_payload); 39 | } 40 | 41 | if (header) 42 | dovi_rpu_free_header(header); 43 | 44 | dovi_rpu_free(rpu); 45 | 46 | auto end = std::chrono::high_resolution_clock::now(); 47 | std::cout << std::chrono::duration_cast(end - start).count() << " μs"; 48 | } 49 | -------------------------------------------------------------------------------- /dolby_vision/src/av1/emdf.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, ensure}; 2 | use bitvec_helpers::{ 3 | bitstream_io_reader::BsIoSliceReader, bitstream_io_writer::BitstreamIoWriter, 4 | }; 5 | 6 | /// Parse the expected EMDF container with fixed values according to spec 7 | /// Returns `emdf_payload_size` 8 | pub(crate) fn parse_emdf_container(reader: &mut BsIoSliceReader) -> Result { 9 | let emdf_version = reader.get_n::(2)?; 10 | ensure!(emdf_version == 0); 11 | 12 | let key_id = reader.get_n::(3)?; 13 | ensure!(key_id == 6); 14 | 15 | let emdf_payload_id = reader.get_n::(5)?; 16 | ensure!(emdf_payload_id == 31); 17 | 18 | let emdf_payload_id_ext = parse_variable_bits(reader, 5)?; 19 | ensure!(emdf_payload_id_ext == 225); 20 | 21 | ensure!(!reader.get()?); // smploffste = 0 22 | ensure!(!reader.get()?); // duratione = 0 23 | ensure!(!reader.get()?); // groupide = 0 24 | ensure!(!reader.get()?); // codecdatae = 0 25 | ensure!(reader.get()?); // discard_unknown_payload = 1 26 | 27 | let emdf_payload_size = parse_variable_bits(reader, 8)? as usize; 28 | Ok(emdf_payload_size) 29 | } 30 | 31 | /// Write the DOVI RPU EMDF container with payload 32 | pub(crate) fn write_emdf_container_with_dovi_rpu_payload( 33 | writer: &mut BitstreamIoWriter, 34 | payload: &[u8], 35 | ) -> Result<()> { 36 | let emdf_payload_size = payload.len() as u32; 37 | 38 | write_dovi_rpu_emdf_header(writer)?; 39 | write_variable_bits(writer, emdf_payload_size, 8)?; 40 | 41 | for b in payload { 42 | writer.write_n(b, 8)?; 43 | } 44 | 45 | // emdf_payload_id and emdf_protection 46 | writer.write_n(&0, 5)?; 47 | writer.write_n(&1, 2)?; 48 | writer.write_n(&0, 2)?; 49 | writer.write_n(&0, 8)?; 50 | 51 | Ok(()) 52 | } 53 | 54 | fn write_dovi_rpu_emdf_header(writer: &mut BitstreamIoWriter) -> Result<()> { 55 | writer.write_n(&0, 2)?; // emdf_version 56 | writer.write_n(&6, 3)?; // key_id 57 | writer.write_n(&31, 5)?; // emdf_payload_id 58 | write_variable_bits(writer, 225, 5)?; // emdf_payload_id_ext 59 | 60 | writer.write_n(&0, 4)?; // smploffste, duratione, groupide, codecdatae 61 | writer.write(true)?; // discard_unknown_payload 62 | 63 | Ok(()) 64 | } 65 | 66 | fn parse_variable_bits(reader: &mut BsIoSliceReader, n: u32) -> Result { 67 | let mut value: u32 = 0; 68 | 69 | loop { 70 | let tmp: u32 = reader.get_n(n)?; 71 | value += tmp; 72 | 73 | // read_more flag 74 | if !reader.get()? { 75 | break; 76 | } 77 | 78 | value <<= n; 79 | value += 1 << n; 80 | } 81 | 82 | Ok(value) 83 | } 84 | 85 | fn write_variable_bits(writer: &mut BitstreamIoWriter, value: u32, n: u32) -> Result<()> { 86 | let max = 1 << n; 87 | 88 | if value > max { 89 | let mut remaining = value; 90 | 91 | loop { 92 | let tmp = remaining >> n; 93 | let clipped = tmp << n; 94 | remaining -= clipped; 95 | 96 | let byte = (clipped - max) >> n; 97 | writer.write_n(&byte, n)?; 98 | writer.write(true)?; // read_more 99 | 100 | // Stop once the remaining can be written in N bits 101 | if remaining <= max { 102 | break; 103 | } 104 | } 105 | 106 | writer.write_n(&remaining, n)?; 107 | } else { 108 | writer.write_n(&value, n)?; 109 | } 110 | 111 | writer.write(false)?; 112 | 113 | Ok(()) 114 | } 115 | -------------------------------------------------------------------------------- /dolby_vision/src/av1/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, bail, ensure}; 2 | use bitvec_helpers::{ 3 | bitstream_io_reader::BsIoSliceReader, bitstream_io_writer::BitstreamIoWriter, 4 | }; 5 | 6 | use crate::{ 7 | av1::emdf::{parse_emdf_container, write_emdf_container_with_dovi_rpu_payload}, 8 | rpu::dovi_rpu::{DoviRpu, FINAL_BYTE}, 9 | }; 10 | 11 | mod emdf; 12 | 13 | pub const ITU_T35_DOVI_RPU_PAYLOAD_HEADER: &[u8] = 14 | &[0x00, 0x3B, 0x00, 0x00, 0x08, 0x00, 0x37, 0xCD, 0x08]; 15 | const ITU_T35_DOVI_RPU_PAYLOAD_HEADER_LEN: usize = ITU_T35_DOVI_RPU_PAYLOAD_HEADER.len(); 16 | 17 | /// Parse AV1 ITU-T T.35 metadata OBU into a `DoviRpu` 18 | /// The payload is extracted out of the EMDF wrapper 19 | #[deprecated( 20 | since = "3.3.0", 21 | note = "Replaced by DoviRpu::parse_itu_t35_dovi_metadata_obu" 22 | )] 23 | pub fn parse_itu_t35_dovi_metadata_obu(data: &[u8]) -> Result { 24 | DoviRpu::parse_itu_t35_dovi_metadata_obu(data) 25 | } 26 | 27 | pub(crate) fn av1_validated_trimmed_data(data: &[u8]) -> Result<&[u8]> { 28 | if data.len() < 34 { 29 | bail!("Invalid RPU length: {}", data.len()); 30 | } 31 | 32 | let data = if data[0] == 0xB5 { 33 | // itu_t_t35_country_code - United States 34 | // Remove from buffer 35 | &data[1..] 36 | } else { 37 | data 38 | }; 39 | 40 | let trimmed_data = match &data[..ITU_T35_DOVI_RPU_PAYLOAD_HEADER_LEN] { 41 | ITU_T35_DOVI_RPU_PAYLOAD_HEADER => data, 42 | _ => bail!( 43 | "Invalid AV1 RPU payload header: {:?}", 44 | &data[..ITU_T35_DOVI_RPU_PAYLOAD_HEADER_LEN] 45 | ), 46 | }; 47 | 48 | Ok(trimmed_data) 49 | } 50 | 51 | /// Returns the EMDF payload bytes representing the RPU buffer 52 | pub(crate) fn convert_av1_rpu_payload_to_regular(data: &[u8]) -> Result> { 53 | let mut reader = BsIoSliceReader::from_slice(data); 54 | 55 | let itu_t_t35_terminal_provider_code = reader.get_n::(16)?; 56 | ensure!(itu_t_t35_terminal_provider_code == 0x3B); 57 | 58 | let itu_t_t35_terminal_provider_oriented_code = reader.get_n::(32)?; 59 | ensure!(itu_t_t35_terminal_provider_oriented_code == 0x800); 60 | 61 | let emdf_payload_size = parse_emdf_container(&mut reader)?; 62 | let mut converted_buf = Vec::with_capacity(emdf_payload_size + 1); 63 | converted_buf.push(0x19); 64 | 65 | for _ in 0..emdf_payload_size { 66 | converted_buf.push(reader.get_n(8)?); 67 | } 68 | 69 | Ok(converted_buf) 70 | } 71 | 72 | /// Wraps a regular RPU into EMDF container with ITU-T T.35 header 73 | /// Buffer must start with 0x19 prefix. 74 | /// 75 | /// Returns payload for AV1 ITU T-T.35 metadata OBU 76 | pub fn convert_regular_rpu_to_av1_payload(data: &[u8]) -> Result> { 77 | ensure!(data[0] == 0x19); 78 | 79 | // The EMDF payload must not include any trailing bytes after 0x80 terminator 80 | let trailing_zeroes = data.iter().rev().take_while(|b| **b == 0).count(); 81 | let rpu_end = data.len() - trailing_zeroes; 82 | let last_byte = data[rpu_end - 1]; 83 | 84 | if last_byte != FINAL_BYTE { 85 | bail!("Invalid RPU last byte: {}", last_byte); 86 | } 87 | 88 | // Exclude 0x19 prefix 89 | let data = &data[1..rpu_end]; 90 | let rpu_size = data.len(); 91 | let capacity = 16 + rpu_size; 92 | 93 | let mut writer = BitstreamIoWriter::with_capacity(capacity * 2); 94 | 95 | writer.write_n(&0x3B, 16)?; // itu_t_t35_terminal_provider_code 96 | writer.write_n(&0x800, 32)?; // itu_t_t35_terminal_provider_oriented_code 97 | 98 | write_emdf_container_with_dovi_rpu_payload(&mut writer, data)?; 99 | 100 | while !writer.is_aligned() { 101 | writer.write(true)?; 102 | } 103 | 104 | Ok(writer.into_inner()) 105 | } 106 | -------------------------------------------------------------------------------- /dolby_vision/src/c_structs/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::rpu::NUM_COMPONENTS; 2 | 3 | mod buffers; 4 | mod extension_metadata; 5 | mod rpu; 6 | mod rpu_data_header; 7 | mod rpu_data_mapping; 8 | mod rpu_data_nlq; 9 | mod vdr_dm_data; 10 | 11 | pub use buffers::*; 12 | pub use extension_metadata::DmData; 13 | pub use rpu::{RpuOpaque, RpuOpaqueList}; 14 | pub use rpu_data_header::RpuDataHeader; 15 | pub use rpu_data_mapping::RpuDataMapping; 16 | pub use rpu_data_nlq::RpuDataNlq; 17 | pub use vdr_dm_data::VdrDmData; 18 | -------------------------------------------------------------------------------- /dolby_vision/src/c_structs/rpu.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CString; 2 | 3 | use libc::{c_char, size_t}; 4 | 5 | use crate::rpu::dovi_rpu::DoviRpu; 6 | 7 | use super::Freeable; 8 | 9 | /// Opaque Dolby Vision RPU. 10 | /// 11 | /// Use dovi_rpu_free to free. 12 | /// It should be freed regardless of whether or not an error occurred. 13 | pub struct RpuOpaque { 14 | /// Optional parsed RPU, present when parsing is successful. 15 | pub rpu: Option, 16 | /// Error String of the parsing, in cases of failure. 17 | pub error: Option, 18 | } 19 | 20 | /// Heap allocated list of valid RPU pointers 21 | #[repr(C)] 22 | pub struct RpuOpaqueList { 23 | pub list: *const *mut RpuOpaque, 24 | pub len: size_t, 25 | 26 | pub error: *const c_char, 27 | } 28 | 29 | impl RpuOpaque { 30 | pub(crate) fn new(rpu: Option, error: Option) -> Self { 31 | Self { rpu, error } 32 | } 33 | } 34 | 35 | impl From> for RpuOpaque { 36 | fn from(res: Result) -> Self { 37 | match res { 38 | Ok(rpu) => Self::new(Some(rpu), None), 39 | Err(e) => Self::new( 40 | None, 41 | Some(CString::new(format!("Failed parsing RPU: {e}")).unwrap()), 42 | ), 43 | } 44 | } 45 | } 46 | 47 | impl Freeable for RpuOpaqueList { 48 | unsafe fn free(&self) { 49 | unsafe { 50 | let list = Vec::from_raw_parts(self.list as *mut *mut RpuOpaque, self.len, self.len); 51 | for ptr in list { 52 | drop(Box::from_raw(ptr)); 53 | } 54 | 55 | if !self.error.is_null() { 56 | drop(CString::from_raw(self.error as *mut c_char)); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /dolby_vision/src/c_structs/rpu_data_header.rs: -------------------------------------------------------------------------------- 1 | use libc::c_char; 2 | use std::ptr::null; 3 | 4 | use crate::rpu::rpu_data_header::RpuDataHeader as RuRpuDataHeader; 5 | 6 | /// C struct for rpu_data_header() 7 | #[repr(C)] 8 | pub struct RpuDataHeader { 9 | /// Profile guessed from the values in the header 10 | guessed_profile: u8, 11 | 12 | /// Enhancement layer type (FEL or MEL) if the RPU is profile 7 13 | /// null pointer if not profile 7 14 | pub(crate) el_type: *const c_char, 15 | 16 | /// Deprecated since 3.2.0 17 | /// The field is not actually part of the RPU header 18 | #[deprecated( 19 | since = "3.2.0", 20 | note = "The field is not actually part of the RPU header" 21 | )] 22 | rpu_nal_prefix: u8, 23 | 24 | rpu_type: u8, 25 | rpu_format: u16, 26 | vdr_rpu_profile: u8, 27 | vdr_rpu_level: u8, 28 | vdr_seq_info_present_flag: bool, 29 | chroma_resampling_explicit_filter_flag: bool, 30 | coefficient_data_type: u8, 31 | coefficient_log2_denom: u64, 32 | vdr_rpu_normalized_idc: u8, 33 | bl_video_full_range_flag: bool, 34 | bl_bit_depth_minus8: u64, 35 | el_bit_depth_minus8: u64, 36 | vdr_bit_depth_minus8: u64, 37 | spatial_resampling_filter_flag: bool, 38 | reserved_zero_3bits: u8, 39 | el_spatial_resampling_filter_flag: bool, 40 | disable_residual_flag: bool, 41 | vdr_dm_metadata_present_flag: bool, 42 | use_prev_vdr_rpu_flag: bool, 43 | prev_vdr_rpu_id: u64, 44 | } 45 | 46 | impl From<&RuRpuDataHeader> for RpuDataHeader { 47 | fn from(header: &RuRpuDataHeader) -> Self { 48 | #[allow(deprecated)] 49 | Self { 50 | guessed_profile: header.get_dovi_profile(), 51 | el_type: null(), 52 | // FIXME: rpu_nal_prefix deprecation 53 | rpu_nal_prefix: header.rpu_nal_prefix, 54 | rpu_type: header.rpu_type, 55 | rpu_format: header.rpu_format, 56 | vdr_rpu_profile: header.vdr_rpu_profile, 57 | vdr_rpu_level: header.vdr_rpu_level, 58 | vdr_seq_info_present_flag: header.vdr_seq_info_present_flag, 59 | chroma_resampling_explicit_filter_flag: header.chroma_resampling_explicit_filter_flag, 60 | coefficient_data_type: header.coefficient_data_type, 61 | coefficient_log2_denom: header.coefficient_log2_denom, 62 | vdr_rpu_normalized_idc: header.vdr_rpu_normalized_idc, 63 | bl_video_full_range_flag: header.bl_video_full_range_flag, 64 | bl_bit_depth_minus8: header.bl_bit_depth_minus8, 65 | el_bit_depth_minus8: header.el_bit_depth_minus8, 66 | vdr_bit_depth_minus8: header.vdr_bit_depth_minus8, 67 | spatial_resampling_filter_flag: header.spatial_resampling_filter_flag, 68 | reserved_zero_3bits: header.reserved_zero_3bits, 69 | el_spatial_resampling_filter_flag: header.el_spatial_resampling_filter_flag, 70 | disable_residual_flag: header.disable_residual_flag, 71 | vdr_dm_metadata_present_flag: header.vdr_dm_metadata_present_flag, 72 | use_prev_vdr_rpu_flag: header.use_prev_vdr_rpu_flag, 73 | prev_vdr_rpu_id: header.prev_vdr_rpu_id, 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /dolby_vision/src/c_structs/rpu_data_nlq.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CStr; 2 | 3 | use crate::rpu::{ 4 | NUM_COMPONENTS, 5 | rpu_data_nlq::{DoviELType, RpuDataNlq as RuRpuDataNlq}, 6 | }; 7 | 8 | const FEL_CSTR: &CStr = c"FEL"; 9 | const MEL_CSTR: &CStr = c"MEL"; 10 | 11 | /// C struct for rpu_data_nlq() 12 | #[repr(C)] 13 | pub struct RpuDataNlq { 14 | nlq_offset: [u16; NUM_COMPONENTS], 15 | vdr_in_max_int: [u64; NUM_COMPONENTS], 16 | vdr_in_max: [u64; NUM_COMPONENTS], 17 | linear_deadzone_slope_int: [u64; NUM_COMPONENTS], 18 | linear_deadzone_slope: [u64; NUM_COMPONENTS], 19 | linear_deadzone_threshold_int: [u64; NUM_COMPONENTS], 20 | linear_deadzone_threshold: [u64; NUM_COMPONENTS], 21 | } 22 | 23 | impl DoviELType { 24 | pub const fn as_cstr(&self) -> &'static CStr { 25 | match self { 26 | DoviELType::MEL => MEL_CSTR, 27 | DoviELType::FEL => FEL_CSTR, 28 | } 29 | } 30 | } 31 | 32 | impl From<&RuRpuDataNlq> for RpuDataNlq { 33 | fn from(data: &RuRpuDataNlq) -> Self { 34 | Self { 35 | nlq_offset: data.nlq_offset, 36 | vdr_in_max_int: data.vdr_in_max_int, 37 | vdr_in_max: data.vdr_in_max, 38 | linear_deadzone_slope_int: data.linear_deadzone_slope_int, 39 | linear_deadzone_slope: data.linear_deadzone_slope, 40 | linear_deadzone_threshold_int: data.linear_deadzone_threshold_int, 41 | linear_deadzone_threshold: data.linear_deadzone_threshold, 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /dolby_vision/src/c_structs/vdr_dm_data.rs: -------------------------------------------------------------------------------- 1 | use crate::rpu::vdr_dm_data::VdrDmData as RuVdrDmData; 2 | 3 | use super::DmData; 4 | 5 | /// C struct for vdr_dm_data() 6 | #[repr(C)] 7 | pub struct VdrDmData { 8 | compressed: bool, 9 | 10 | affected_dm_metadata_id: u64, 11 | current_dm_metadata_id: u64, 12 | scene_refresh_flag: u64, 13 | ycc_to_rgb_coef0: i16, 14 | ycc_to_rgb_coef1: i16, 15 | ycc_to_rgb_coef2: i16, 16 | ycc_to_rgb_coef3: i16, 17 | ycc_to_rgb_coef4: i16, 18 | ycc_to_rgb_coef5: i16, 19 | ycc_to_rgb_coef6: i16, 20 | ycc_to_rgb_coef7: i16, 21 | ycc_to_rgb_coef8: i16, 22 | ycc_to_rgb_offset0: u32, 23 | ycc_to_rgb_offset1: u32, 24 | ycc_to_rgb_offset2: u32, 25 | rgb_to_lms_coef0: i16, 26 | rgb_to_lms_coef1: i16, 27 | rgb_to_lms_coef2: i16, 28 | rgb_to_lms_coef3: i16, 29 | rgb_to_lms_coef4: i16, 30 | rgb_to_lms_coef5: i16, 31 | rgb_to_lms_coef6: i16, 32 | rgb_to_lms_coef7: i16, 33 | rgb_to_lms_coef8: i16, 34 | signal_eotf: u16, 35 | signal_eotf_param0: u16, 36 | signal_eotf_param1: u16, 37 | signal_eotf_param2: u32, 38 | signal_bit_depth: u8, 39 | signal_color_space: u8, 40 | signal_chroma_format: u8, 41 | signal_full_range_flag: u8, 42 | source_min_pq: u16, 43 | source_max_pq: u16, 44 | source_diagonal: u16, 45 | dm_data: DmData, 46 | } 47 | 48 | impl VdrDmData { 49 | /// # Safety 50 | pub unsafe fn free(&self) { 51 | unsafe { 52 | self.dm_data.free(); 53 | } 54 | } 55 | } 56 | 57 | impl From<&RuVdrDmData> for VdrDmData { 58 | fn from(data: &RuVdrDmData) -> Self { 59 | Self { 60 | compressed: data.compressed, 61 | affected_dm_metadata_id: data.affected_dm_metadata_id, 62 | current_dm_metadata_id: data.current_dm_metadata_id, 63 | scene_refresh_flag: data.scene_refresh_flag, 64 | ycc_to_rgb_coef0: data.ycc_to_rgb_coef0, 65 | ycc_to_rgb_coef1: data.ycc_to_rgb_coef1, 66 | ycc_to_rgb_coef2: data.ycc_to_rgb_coef2, 67 | ycc_to_rgb_coef3: data.ycc_to_rgb_coef3, 68 | ycc_to_rgb_coef4: data.ycc_to_rgb_coef4, 69 | ycc_to_rgb_coef5: data.ycc_to_rgb_coef5, 70 | ycc_to_rgb_coef6: data.ycc_to_rgb_coef6, 71 | ycc_to_rgb_coef7: data.ycc_to_rgb_coef7, 72 | ycc_to_rgb_coef8: data.ycc_to_rgb_coef8, 73 | ycc_to_rgb_offset0: data.ycc_to_rgb_offset0, 74 | ycc_to_rgb_offset1: data.ycc_to_rgb_offset1, 75 | ycc_to_rgb_offset2: data.ycc_to_rgb_offset2, 76 | rgb_to_lms_coef0: data.rgb_to_lms_coef0, 77 | rgb_to_lms_coef1: data.rgb_to_lms_coef1, 78 | rgb_to_lms_coef2: data.rgb_to_lms_coef2, 79 | rgb_to_lms_coef3: data.rgb_to_lms_coef3, 80 | rgb_to_lms_coef4: data.rgb_to_lms_coef4, 81 | rgb_to_lms_coef5: data.rgb_to_lms_coef5, 82 | rgb_to_lms_coef6: data.rgb_to_lms_coef6, 83 | rgb_to_lms_coef7: data.rgb_to_lms_coef7, 84 | rgb_to_lms_coef8: data.rgb_to_lms_coef8, 85 | signal_eotf: data.signal_eotf, 86 | signal_eotf_param0: data.signal_eotf_param0, 87 | signal_eotf_param1: data.signal_eotf_param1, 88 | signal_eotf_param2: data.signal_eotf_param2, 89 | signal_bit_depth: data.signal_bit_depth, 90 | signal_color_space: data.signal_color_space, 91 | signal_chroma_format: data.signal_chroma_format, 92 | signal_full_range_flag: data.signal_full_range_flag, 93 | source_min_pq: data.source_min_pq, 94 | source_max_pq: data.source_max_pq, 95 | source_diagonal: data.source_diagonal, 96 | dm_data: DmData::combine_dm_data( 97 | data.cmv29_metadata.as_ref(), 98 | data.cmv40_metadata.as_ref(), 99 | ), 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /dolby_vision/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// Dolby Vision RPU (as found in HEVC type 62 NALUs) module 2 | pub mod rpu; 3 | 4 | /// Dolby Vision RPU (as found in AV1 ITU T.35 metadata OBUs) 5 | pub mod av1; 6 | 7 | /// SMPTE ST2094-10 metadata module 8 | pub mod st2094_10; 9 | 10 | /// Various utils 11 | /// cbindgen:ignore 12 | pub mod utils; 13 | 14 | /// Dolby Vision XML metadata module 15 | #[cfg(feature = "xml")] 16 | pub mod xml; 17 | 18 | /// C API module 19 | #[cfg(any(cargo_c, feature = "capi"))] 20 | mod capi; 21 | 22 | /// Structs used and exposed in the C API 23 | #[cfg(any(cargo_c, feature = "capi"))] 24 | mod c_structs; 25 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/extension_metadata/blocks/level1.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, ensure}; 2 | use bitvec_helpers::{ 3 | bitstream_io_reader::BsIoSliceReader, bitstream_io_writer::BitstreamIoWriter, 4 | }; 5 | 6 | #[cfg(feature = "serde")] 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use crate::rpu::vdr_dm_data::CmVersion; 10 | 11 | use super::{ExtMetadataBlock, ExtMetadataBlockInfo}; 12 | 13 | /// cbindgen:ignore 14 | pub const L1_MIN_PQ_MAX_VALUE: u16 = 12; 15 | /// cbindgen:ignore 16 | pub const L1_MAX_PQ_MIN_VALUE: u16 = 2081; 17 | /// cbindgen:ignore 18 | pub const L1_MAX_PQ_MAX_VALUE: u16 = 4095; 19 | /// cbindgen:ignore 20 | pub const L1_AVG_PQ_MIN_VALUE: u16 = 819; 21 | /// cbindgen:ignore 22 | pub const L1_AVG_PQ_MIN_VALUE_CMV40: u16 = 1229; 23 | 24 | /// Statistical analysis of the frame: min, max, avg brightness. 25 | #[repr(C)] 26 | #[derive(Debug, Default, Clone)] 27 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 28 | pub struct ExtMetadataBlockLevel1 { 29 | pub min_pq: u16, 30 | pub max_pq: u16, 31 | pub avg_pq: u16, 32 | } 33 | 34 | impl ExtMetadataBlockLevel1 { 35 | pub(crate) fn parse(reader: &mut BsIoSliceReader) -> Result { 36 | Ok(ExtMetadataBlock::Level1(Self { 37 | min_pq: reader.get_n(12)?, 38 | max_pq: reader.get_n(12)?, 39 | avg_pq: reader.get_n(12)?, 40 | })) 41 | } 42 | 43 | pub fn write(&self, writer: &mut BitstreamIoWriter) -> Result<()> { 44 | self.validate()?; 45 | 46 | writer.write_n(&self.min_pq, 12)?; 47 | writer.write_n(&self.max_pq, 12)?; 48 | writer.write_n(&self.avg_pq, 12)?; 49 | 50 | Ok(()) 51 | } 52 | 53 | pub fn validate(&self) -> Result<()> { 54 | ensure!(self.min_pq <= L1_MAX_PQ_MAX_VALUE); 55 | ensure!(self.max_pq <= L1_MAX_PQ_MAX_VALUE); 56 | ensure!(self.avg_pq <= L1_MAX_PQ_MAX_VALUE); 57 | 58 | Ok(()) 59 | } 60 | 61 | pub fn new(min_pq: u16, max_pq: u16, avg_pq: u16) -> ExtMetadataBlockLevel1 { 62 | ExtMetadataBlockLevel1 { 63 | min_pq, 64 | max_pq, 65 | avg_pq, 66 | } 67 | } 68 | 69 | fn clamp_values_int(&mut self, cm_version: CmVersion) { 70 | let avg_min_value = match cm_version { 71 | CmVersion::V29 => L1_AVG_PQ_MIN_VALUE, 72 | CmVersion::V40 => L1_AVG_PQ_MIN_VALUE_CMV40, 73 | }; 74 | 75 | self.min_pq = self.min_pq.clamp(0, L1_MIN_PQ_MAX_VALUE); 76 | self.max_pq = self.max_pq.clamp(L1_MAX_PQ_MIN_VALUE, L1_MAX_PQ_MAX_VALUE); 77 | self.avg_pq = self.avg_pq.clamp(avg_min_value, self.max_pq - 1); 78 | } 79 | 80 | // Returns a L1 metadata block clamped to valid values 81 | pub fn from_stats_cm_version( 82 | min_pq: u16, 83 | max_pq: u16, 84 | avg_pq: u16, 85 | cm_version: CmVersion, 86 | ) -> ExtMetadataBlockLevel1 { 87 | let mut block = Self::new(min_pq, max_pq, avg_pq); 88 | block.clamp_values_int(cm_version); 89 | 90 | block 91 | } 92 | 93 | pub fn clamp_values_cm_version(&mut self, cm_version: CmVersion) { 94 | self.clamp_values_int(cm_version); 95 | } 96 | } 97 | 98 | impl ExtMetadataBlockInfo for ExtMetadataBlockLevel1 { 99 | fn level(&self) -> u8 { 100 | 1 101 | } 102 | 103 | fn bytes_size(&self) -> u64 { 104 | 5 105 | } 106 | 107 | fn required_bits(&self) -> u64 { 108 | 36 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/extension_metadata/blocks/level11.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, ensure}; 2 | use bitvec_helpers::{ 3 | bitstream_io_reader::BsIoSliceReader, bitstream_io_writer::BitstreamIoWriter, 4 | }; 5 | 6 | #[cfg(feature = "serde")] 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use super::{ExtMetadataBlock, ExtMetadataBlockInfo}; 10 | 11 | const MAX_WHITEPOINT_VALUE: u8 = 15; 12 | 13 | /// Content type metadata level 14 | #[repr(C)] 15 | #[derive(Debug, Default, Clone)] 16 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 17 | pub struct ExtMetadataBlockLevel11 { 18 | pub content_type: u8, 19 | pub whitepoint: u8, 20 | pub reference_mode_flag: bool, 21 | 22 | #[cfg_attr(feature = "serde", serde(default))] 23 | pub reserved_byte2: u8, 24 | #[cfg_attr(feature = "serde", serde(default))] 25 | pub reserved_byte3: u8, 26 | } 27 | 28 | impl ExtMetadataBlockLevel11 { 29 | pub(crate) fn parse(reader: &mut BsIoSliceReader) -> Result { 30 | let mut l11 = Self { 31 | content_type: reader.get_n(8)?, 32 | whitepoint: reader.get_n(8)?, 33 | reserved_byte2: reader.get_n(8)?, 34 | reserved_byte3: reader.get_n(8)?, 35 | ..Default::default() 36 | }; 37 | 38 | if l11.whitepoint > MAX_WHITEPOINT_VALUE { 39 | l11.reference_mode_flag = true; 40 | l11.whitepoint -= MAX_WHITEPOINT_VALUE + 1; 41 | } 42 | 43 | Ok(ExtMetadataBlock::Level11(l11)) 44 | } 45 | 46 | pub fn write(&self, writer: &mut BitstreamIoWriter) -> Result<()> { 47 | self.validate()?; 48 | 49 | let mut wp = self.whitepoint; 50 | 51 | if self.reference_mode_flag { 52 | wp += MAX_WHITEPOINT_VALUE + 1 53 | } 54 | 55 | writer.write_n(&self.content_type, 8)?; 56 | writer.write_n(&wp, 8)?; 57 | writer.write_n(&self.reserved_byte2, 8)?; 58 | writer.write_n(&self.reserved_byte3, 8)?; 59 | 60 | Ok(()) 61 | } 62 | 63 | pub fn validate(&self) -> Result<()> { 64 | ensure!(self.content_type <= 15); 65 | ensure!(self.whitepoint <= 15); 66 | ensure!(self.reserved_byte2 == 0); 67 | ensure!(self.reserved_byte3 == 0); 68 | 69 | Ok(()) 70 | } 71 | 72 | /// Cinema, reference mode, D65 whitepoint 73 | pub fn default_reference_cinema() -> Self { 74 | Self { 75 | content_type: 1, 76 | whitepoint: 0, 77 | reference_mode_flag: true, 78 | reserved_byte2: 0, 79 | reserved_byte3: 0, 80 | } 81 | } 82 | } 83 | 84 | impl ExtMetadataBlockInfo for ExtMetadataBlockLevel11 { 85 | fn level(&self) -> u8 { 86 | 11 87 | } 88 | 89 | fn bytes_size(&self) -> u64 { 90 | 4 91 | } 92 | 93 | fn required_bits(&self) -> u64 { 94 | 32 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/extension_metadata/blocks/level2.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, ensure}; 2 | use bitvec_helpers::{ 3 | bitstream_io_reader::BsIoSliceReader, bitstream_io_writer::BitstreamIoWriter, 4 | }; 5 | 6 | #[cfg(feature = "serde")] 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use crate::utils::nits_to_pq_12_bit; 10 | 11 | use super::{ExtMetadataBlock, ExtMetadataBlockInfo, MAX_12_BIT_VALUE}; 12 | 13 | /// Creative intent trim passes per target display peak brightness 14 | #[repr(C)] 15 | #[derive(Debug, Clone)] 16 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 17 | #[cfg_attr(feature = "serde", serde(default))] 18 | pub struct ExtMetadataBlockLevel2 { 19 | pub target_max_pq: u16, 20 | 21 | pub trim_slope: u16, 22 | pub trim_offset: u16, 23 | pub trim_power: u16, 24 | pub trim_chroma_weight: u16, 25 | pub trim_saturation_gain: u16, 26 | pub ms_weight: i16, 27 | } 28 | 29 | impl ExtMetadataBlockLevel2 { 30 | pub(crate) fn parse(reader: &mut BsIoSliceReader) -> Result { 31 | let mut level2 = Self { 32 | target_max_pq: reader.get_n(12)?, 33 | trim_slope: reader.get_n(12)?, 34 | trim_offset: reader.get_n(12)?, 35 | trim_power: reader.get_n(12)?, 36 | trim_chroma_weight: reader.get_n(12)?, 37 | trim_saturation_gain: reader.get_n(12)?, 38 | ms_weight: reader.get_n::(13)? as i16, 39 | }; 40 | 41 | if level2.ms_weight > MAX_12_BIT_VALUE as i16 { 42 | level2.ms_weight = level2.ms_weight.wrapping_sub(8192); 43 | } 44 | 45 | Ok(ExtMetadataBlock::Level2(level2)) 46 | } 47 | 48 | pub fn write(&self, writer: &mut BitstreamIoWriter) -> Result<()> { 49 | self.validate()?; 50 | 51 | writer.write_n(&self.target_max_pq, 12)?; 52 | writer.write_n(&self.trim_slope, 12)?; 53 | writer.write_n(&self.trim_offset, 12)?; 54 | writer.write_n(&self.trim_power, 12)?; 55 | writer.write_n(&self.trim_chroma_weight, 12)?; 56 | writer.write_n(&self.trim_saturation_gain, 12)?; 57 | writer.write_signed_n(&self.ms_weight, 13)?; 58 | 59 | Ok(()) 60 | } 61 | 62 | pub fn validate(&self) -> Result<()> { 63 | ensure!(self.target_max_pq <= MAX_12_BIT_VALUE); 64 | ensure!(self.trim_slope <= MAX_12_BIT_VALUE); 65 | ensure!(self.trim_offset <= MAX_12_BIT_VALUE); 66 | ensure!(self.trim_power <= MAX_12_BIT_VALUE); 67 | ensure!(self.trim_chroma_weight <= MAX_12_BIT_VALUE); 68 | ensure!(self.trim_saturation_gain <= MAX_12_BIT_VALUE); 69 | ensure!(self.ms_weight >= -1 && self.ms_weight <= (MAX_12_BIT_VALUE as i16)); 70 | 71 | Ok(()) 72 | } 73 | 74 | pub fn from_nits(target_nits: u16) -> ExtMetadataBlockLevel2 { 75 | ExtMetadataBlockLevel2 { 76 | target_max_pq: nits_to_pq_12_bit(target_nits), 77 | ..Default::default() 78 | } 79 | } 80 | } 81 | 82 | impl ExtMetadataBlockInfo for ExtMetadataBlockLevel2 { 83 | fn level(&self) -> u8 { 84 | 2 85 | } 86 | 87 | fn bytes_size(&self) -> u64 { 88 | 11 89 | } 90 | 91 | fn required_bits(&self) -> u64 { 92 | 85 93 | } 94 | 95 | fn sort_key(&self) -> (u8, u16) { 96 | (self.level(), self.target_max_pq) 97 | } 98 | } 99 | 100 | impl Default for ExtMetadataBlockLevel2 { 101 | fn default() -> Self { 102 | Self { 103 | target_max_pq: 2081, 104 | trim_slope: 2048, 105 | trim_offset: 2048, 106 | trim_power: 2048, 107 | trim_chroma_weight: 2048, 108 | trim_saturation_gain: 2048, 109 | ms_weight: 2048, 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/extension_metadata/blocks/level254.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use bitvec_helpers::{ 3 | bitstream_io_reader::BsIoSliceReader, bitstream_io_writer::BitstreamIoWriter, 4 | }; 5 | 6 | #[cfg(feature = "serde")] 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use super::{ExtMetadataBlock, ExtMetadataBlockInfo}; 10 | 11 | /// Metadata level present in CM v4.0 12 | #[repr(C)] 13 | #[derive(Debug, Default, Clone)] 14 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 15 | #[cfg_attr(feature = "serde", serde(default))] 16 | pub struct ExtMetadataBlockLevel254 { 17 | pub dm_mode: u8, 18 | pub dm_version_index: u8, 19 | } 20 | 21 | impl ExtMetadataBlockLevel254 { 22 | pub(crate) fn parse(reader: &mut BsIoSliceReader) -> Result { 23 | Ok(ExtMetadataBlock::Level254(Self { 24 | dm_mode: reader.get_n(8)?, 25 | dm_version_index: reader.get_n(8)?, 26 | })) 27 | } 28 | 29 | pub fn write(&self, writer: &mut BitstreamIoWriter) -> Result<()> { 30 | writer.write_n(&self.dm_mode, 8)?; 31 | writer.write_n(&self.dm_version_index, 8)?; 32 | 33 | Ok(()) 34 | } 35 | 36 | pub fn cmv402_default() -> ExtMetadataBlockLevel254 { 37 | ExtMetadataBlockLevel254 { 38 | dm_mode: 0, 39 | dm_version_index: 2, 40 | } 41 | } 42 | } 43 | 44 | impl ExtMetadataBlockInfo for ExtMetadataBlockLevel254 { 45 | fn level(&self) -> u8 { 46 | 254 47 | } 48 | 49 | fn bytes_size(&self) -> u64 { 50 | 2 51 | } 52 | 53 | fn required_bits(&self) -> u64 { 54 | 16 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/extension_metadata/blocks/level255.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use bitvec_helpers::{ 3 | bitstream_io_reader::BsIoSliceReader, bitstream_io_writer::BitstreamIoWriter, 4 | }; 5 | 6 | #[cfg(feature = "serde")] 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use super::{ExtMetadataBlock, ExtMetadataBlockInfo}; 10 | 11 | /// Metadata level optionally present in CM v2.9. 12 | /// Different display modes (calibration/verify/bypass), debugging 13 | #[repr(C)] 14 | #[derive(Debug, Default, Clone)] 15 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 16 | #[cfg_attr(feature = "serde", serde(default))] 17 | pub struct ExtMetadataBlockLevel255 { 18 | pub dm_run_mode: u8, 19 | pub dm_run_version: u8, 20 | pub dm_debug0: u8, 21 | pub dm_debug1: u8, 22 | pub dm_debug2: u8, 23 | pub dm_debug3: u8, 24 | } 25 | 26 | impl ExtMetadataBlockLevel255 { 27 | pub(crate) fn parse(reader: &mut BsIoSliceReader) -> Result { 28 | Ok(ExtMetadataBlock::Level255(Self { 29 | dm_run_mode: reader.get_n(8)?, 30 | dm_run_version: reader.get_n(8)?, 31 | dm_debug0: reader.get_n(8)?, 32 | dm_debug1: reader.get_n(8)?, 33 | dm_debug2: reader.get_n(8)?, 34 | dm_debug3: reader.get_n(8)?, 35 | })) 36 | } 37 | 38 | pub fn write(&self, writer: &mut BitstreamIoWriter) -> Result<()> { 39 | writer.write_n(&self.dm_run_mode, 8)?; 40 | writer.write_n(&self.dm_run_version, 8)?; 41 | writer.write_n(&self.dm_debug0, 8)?; 42 | writer.write_n(&self.dm_debug1, 8)?; 43 | writer.write_n(&self.dm_debug2, 8)?; 44 | writer.write_n(&self.dm_debug3, 8)?; 45 | 46 | Ok(()) 47 | } 48 | } 49 | 50 | impl ExtMetadataBlockInfo for ExtMetadataBlockLevel255 { 51 | fn level(&self) -> u8 { 52 | 255 53 | } 54 | 55 | fn bytes_size(&self) -> u64 { 56 | 6 57 | } 58 | 59 | fn required_bits(&self) -> u64 { 60 | 48 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/extension_metadata/blocks/level3.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, ensure}; 2 | use bitvec_helpers::{ 3 | bitstream_io_reader::BsIoSliceReader, bitstream_io_writer::BitstreamIoWriter, 4 | }; 5 | 6 | #[cfg(feature = "serde")] 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use super::{ExtMetadataBlock, ExtMetadataBlockInfo, MAX_12_BIT_VALUE}; 10 | 11 | /// Level 1 offsets. 12 | #[repr(C)] 13 | #[derive(Debug, Clone)] 14 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 15 | #[cfg_attr(feature = "serde", serde(default))] 16 | pub struct ExtMetadataBlockLevel3 { 17 | pub min_pq_offset: u16, 18 | pub max_pq_offset: u16, 19 | pub avg_pq_offset: u16, 20 | } 21 | 22 | impl ExtMetadataBlockLevel3 { 23 | pub(crate) fn parse(reader: &mut BsIoSliceReader) -> Result { 24 | Ok(ExtMetadataBlock::Level3(Self { 25 | min_pq_offset: reader.get_n(12)?, 26 | max_pq_offset: reader.get_n(12)?, 27 | avg_pq_offset: reader.get_n(12)?, 28 | })) 29 | } 30 | 31 | pub fn write(&self, writer: &mut BitstreamIoWriter) -> Result<()> { 32 | self.validate()?; 33 | 34 | writer.write_n(&self.min_pq_offset, 12)?; 35 | writer.write_n(&self.max_pq_offset, 12)?; 36 | writer.write_n(&self.avg_pq_offset, 12)?; 37 | 38 | Ok(()) 39 | } 40 | 41 | pub fn validate(&self) -> Result<()> { 42 | ensure!(self.min_pq_offset <= MAX_12_BIT_VALUE); 43 | ensure!(self.max_pq_offset <= MAX_12_BIT_VALUE); 44 | ensure!(self.avg_pq_offset <= MAX_12_BIT_VALUE); 45 | 46 | Ok(()) 47 | } 48 | } 49 | 50 | impl ExtMetadataBlockInfo for ExtMetadataBlockLevel3 { 51 | fn level(&self) -> u8 { 52 | 3 53 | } 54 | 55 | fn bytes_size(&self) -> u64 { 56 | 5 57 | } 58 | 59 | fn required_bits(&self) -> u64 { 60 | 36 61 | } 62 | } 63 | 64 | impl Default for ExtMetadataBlockLevel3 { 65 | fn default() -> Self { 66 | Self { 67 | min_pq_offset: 2048, 68 | max_pq_offset: 2048, 69 | avg_pq_offset: 2048, 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/extension_metadata/blocks/level4.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, ensure}; 2 | use bitvec_helpers::{ 3 | bitstream_io_reader::BsIoSliceReader, bitstream_io_writer::BitstreamIoWriter, 4 | }; 5 | 6 | #[cfg(feature = "serde")] 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use super::{ExtMetadataBlock, ExtMetadataBlockInfo, MAX_12_BIT_VALUE}; 10 | 11 | /// Something about temporal stability 12 | #[repr(C)] 13 | #[derive(Debug, Default, Clone)] 14 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 15 | pub struct ExtMetadataBlockLevel4 { 16 | pub anchor_pq: u16, 17 | pub anchor_power: u16, 18 | } 19 | 20 | impl ExtMetadataBlockLevel4 { 21 | pub(crate) fn parse(reader: &mut BsIoSliceReader) -> Result { 22 | Ok(ExtMetadataBlock::Level4(Self { 23 | anchor_pq: reader.get_n(12)?, 24 | anchor_power: reader.get_n(12)?, 25 | })) 26 | } 27 | 28 | pub fn write(&self, writer: &mut BitstreamIoWriter) -> Result<()> { 29 | self.validate()?; 30 | 31 | writer.write_n(&self.anchor_pq, 12)?; 32 | writer.write_n(&self.anchor_power, 12)?; 33 | 34 | Ok(()) 35 | } 36 | 37 | pub fn validate(&self) -> Result<()> { 38 | ensure!(self.anchor_pq <= MAX_12_BIT_VALUE); 39 | ensure!(self.anchor_power <= MAX_12_BIT_VALUE); 40 | 41 | Ok(()) 42 | } 43 | } 44 | 45 | impl ExtMetadataBlockInfo for ExtMetadataBlockLevel4 { 46 | fn level(&self) -> u8 { 47 | 4 48 | } 49 | 50 | fn bytes_size(&self) -> u64 { 51 | 3 52 | } 53 | 54 | fn required_bits(&self) -> u64 { 55 | 24 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/extension_metadata/blocks/level5.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, ensure}; 2 | use bitvec_helpers::{ 3 | bitstream_io_reader::BsIoSliceReader, bitstream_io_writer::BitstreamIoWriter, 4 | }; 5 | 6 | #[cfg(feature = "serde")] 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use super::{ExtMetadataBlock, ExtMetadataBlockInfo}; 10 | 11 | const MAX_RESOLUTION_13_BITS: u16 = 8191; 12 | 13 | /// Active area of the picture (letterbox, aspect ratio) 14 | #[repr(C)] 15 | #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] 16 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 17 | pub struct ExtMetadataBlockLevel5 { 18 | pub active_area_left_offset: u16, 19 | pub active_area_right_offset: u16, 20 | pub active_area_top_offset: u16, 21 | pub active_area_bottom_offset: u16, 22 | } 23 | 24 | impl ExtMetadataBlockLevel5 { 25 | pub(crate) fn parse(reader: &mut BsIoSliceReader) -> Result { 26 | Ok(ExtMetadataBlock::Level5(Self { 27 | active_area_left_offset: reader.get_n(13)?, 28 | active_area_right_offset: reader.get_n(13)?, 29 | active_area_top_offset: reader.get_n(13)?, 30 | active_area_bottom_offset: reader.get_n(13)?, 31 | })) 32 | } 33 | 34 | pub fn write(&self, writer: &mut BitstreamIoWriter) -> Result<()> { 35 | self.validate()?; 36 | 37 | writer.write_n(&self.active_area_left_offset, 13)?; 38 | writer.write_n(&self.active_area_right_offset, 13)?; 39 | writer.write_n(&self.active_area_top_offset, 13)?; 40 | writer.write_n(&self.active_area_bottom_offset, 13)?; 41 | 42 | Ok(()) 43 | } 44 | 45 | pub fn validate(&self) -> Result<()> { 46 | ensure!(self.active_area_left_offset <= MAX_RESOLUTION_13_BITS); 47 | ensure!(self.active_area_right_offset <= MAX_RESOLUTION_13_BITS); 48 | ensure!(self.active_area_top_offset <= MAX_RESOLUTION_13_BITS); 49 | ensure!(self.active_area_bottom_offset <= MAX_RESOLUTION_13_BITS); 50 | 51 | Ok(()) 52 | } 53 | 54 | pub fn get_offsets(&self) -> (u16, u16, u16, u16) { 55 | ( 56 | self.active_area_left_offset, 57 | self.active_area_right_offset, 58 | self.active_area_top_offset, 59 | self.active_area_bottom_offset, 60 | ) 61 | } 62 | 63 | pub fn get_offsets_vec(&self) -> Vec { 64 | vec![ 65 | self.active_area_left_offset, 66 | self.active_area_right_offset, 67 | self.active_area_top_offset, 68 | self.active_area_bottom_offset, 69 | ] 70 | } 71 | 72 | pub fn set_offsets(&mut self, left: u16, right: u16, top: u16, bottom: u16) { 73 | self.active_area_left_offset = left; 74 | self.active_area_right_offset = right; 75 | self.active_area_top_offset = top; 76 | self.active_area_bottom_offset = bottom; 77 | } 78 | 79 | pub fn crop(&mut self) { 80 | self.active_area_left_offset = 0; 81 | self.active_area_right_offset = 0; 82 | self.active_area_top_offset = 0; 83 | self.active_area_bottom_offset = 0; 84 | } 85 | 86 | pub fn from_offsets(left: u16, right: u16, top: u16, bottom: u16) -> Self { 87 | ExtMetadataBlockLevel5 { 88 | active_area_left_offset: left, 89 | active_area_right_offset: right, 90 | active_area_top_offset: top, 91 | active_area_bottom_offset: bottom, 92 | } 93 | } 94 | } 95 | 96 | impl ExtMetadataBlockInfo for ExtMetadataBlockLevel5 { 97 | fn level(&self) -> u8 { 98 | 5 99 | } 100 | 101 | fn bytes_size(&self) -> u64 { 102 | 7 103 | } 104 | 105 | fn required_bits(&self) -> u64 { 106 | 52 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/extension_metadata/blocks/level6.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, ensure}; 2 | use bitvec_helpers::{ 3 | bitstream_io_reader::BsIoSliceReader, bitstream_io_writer::BitstreamIoWriter, 4 | }; 5 | 6 | #[cfg(feature = "serde")] 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use super::{ExtMetadataBlock, ExtMetadataBlockInfo}; 10 | 11 | /// cbindgen:ignore 12 | pub const MAX_PQ_LUMINANCE: u16 = 10_000; 13 | 14 | /// ST2086/HDR10 metadata fallback 15 | #[repr(C)] 16 | #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] 17 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 18 | pub struct ExtMetadataBlockLevel6 { 19 | pub max_display_mastering_luminance: u16, 20 | pub min_display_mastering_luminance: u16, 21 | pub max_content_light_level: u16, 22 | pub max_frame_average_light_level: u16, 23 | } 24 | 25 | impl ExtMetadataBlockLevel6 { 26 | pub(crate) fn parse(reader: &mut BsIoSliceReader) -> Result { 27 | Ok(ExtMetadataBlock::Level6(Self { 28 | max_display_mastering_luminance: reader.get_n(16)?, 29 | min_display_mastering_luminance: reader.get_n(16)?, 30 | max_content_light_level: reader.get_n(16)?, 31 | max_frame_average_light_level: reader.get_n(16)?, 32 | })) 33 | } 34 | 35 | pub fn write(&self, writer: &mut BitstreamIoWriter) -> Result<()> { 36 | self.validate()?; 37 | 38 | writer.write_n(&self.max_display_mastering_luminance, 16)?; 39 | writer.write_n(&self.min_display_mastering_luminance, 16)?; 40 | writer.write_n(&self.max_content_light_level, 16)?; 41 | writer.write_n(&self.max_frame_average_light_level, 16)?; 42 | 43 | Ok(()) 44 | } 45 | 46 | pub fn validate(&self) -> Result<()> { 47 | ensure!(self.max_display_mastering_luminance <= MAX_PQ_LUMINANCE); 48 | ensure!(self.min_display_mastering_luminance <= MAX_PQ_LUMINANCE); 49 | ensure!(self.max_content_light_level <= MAX_PQ_LUMINANCE); 50 | ensure!(self.max_frame_average_light_level <= MAX_PQ_LUMINANCE); 51 | 52 | Ok(()) 53 | } 54 | 55 | pub fn source_meta_from_l6(&self) -> (u16, u16) { 56 | let mdl_min = self.min_display_mastering_luminance; 57 | let mdl_max = self.max_display_mastering_luminance; 58 | 59 | let source_min_pq = if mdl_min <= 10 { 60 | 7 61 | } else if mdl_min == 50 { 62 | 62 63 | } else { 64 | 0 65 | }; 66 | 67 | let source_max_pq = match mdl_max { 68 | 1000 => 3079, 69 | 2000 => 3388, 70 | 4000 => 3696, 71 | 10000 => 4095, 72 | _ => 3079, 73 | }; 74 | 75 | (source_min_pq, source_max_pq) 76 | } 77 | } 78 | 79 | impl ExtMetadataBlockInfo for ExtMetadataBlockLevel6 { 80 | fn level(&self) -> u8 { 81 | 6 82 | } 83 | 84 | fn bytes_size(&self) -> u64 { 85 | 8 86 | } 87 | 88 | fn required_bits(&self) -> u64 { 89 | 64 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/extension_metadata/blocks/reserved.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, bail}; 2 | 3 | use bitvec::{order::Msb0, prelude::BitVec}; 4 | use bitvec_helpers::{ 5 | bitstream_io_reader::BsIoSliceReader, bitstream_io_writer::BitstreamIoWriter, 6 | }; 7 | 8 | #[cfg(feature = "serde")] 9 | use serde::{Deserialize, Serialize}; 10 | 11 | use super::{ExtMetadataBlock, ExtMetadataBlockInfo}; 12 | 13 | #[derive(Debug, Default, Clone)] 14 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 15 | pub struct ReservedExtMetadataBlock { 16 | pub ext_block_length: u64, 17 | pub ext_block_level: u8, 18 | 19 | #[cfg_attr( 20 | feature = "serde", 21 | serde(serialize_with = "crate::utils::bitvec_ser_bits", skip_deserializing) 22 | )] 23 | pub data: BitVec, 24 | } 25 | 26 | impl ReservedExtMetadataBlock { 27 | pub(crate) fn parse( 28 | ext_block_length: u64, 29 | ext_block_level: u8, 30 | reader: &mut BsIoSliceReader, 31 | ) -> Result { 32 | let bits = 8 * ext_block_length; 33 | let mut data = BitVec::new(); 34 | 35 | for _ in 0..bits { 36 | data.push(reader.get()?); 37 | } 38 | 39 | Ok(ExtMetadataBlock::Reserved(Self { 40 | ext_block_length, 41 | ext_block_level, 42 | data, 43 | })) 44 | } 45 | 46 | pub fn write(&self, _writer: &mut BitstreamIoWriter) -> Result<()> { 47 | bail!("Cannot write reserved block"); 48 | // self.data.iter().for_each(|b| writer.write(*b))?; 49 | } 50 | } 51 | 52 | impl ExtMetadataBlockInfo for ReservedExtMetadataBlock { 53 | // TODO: Level 255 is actually definded for DM debugging purposes, we may add it. 54 | fn level(&self) -> u8 { 55 | 0 56 | } 57 | 58 | fn bytes_size(&self) -> u64 { 59 | self.ext_block_length 60 | } 61 | 62 | fn required_bits(&self) -> u64 { 63 | self.data.len() as u64 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/extension_metadata/cmv29.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, bail, ensure}; 2 | use bitvec_helpers::bitstream_io_reader::BsIoSliceReader; 3 | 4 | #[cfg(feature = "serde")] 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use super::WithExtMetadataBlocks; 8 | use crate::rpu::extension_metadata::blocks::*; 9 | 10 | #[derive(Debug, Default, Clone)] 11 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 12 | pub struct CmV29DmData { 13 | num_ext_blocks: u64, 14 | ext_metadata_blocks: Vec, 15 | } 16 | 17 | impl WithExtMetadataBlocks for CmV29DmData { 18 | const VERSION: &'static str = "CM v2.9"; 19 | const ALLOWED_BLOCK_LEVELS: &'static [u8] = &[1, 2, 4, 5, 6, 255]; 20 | 21 | fn with_blocks_allocation(num_ext_blocks: u64) -> Self { 22 | Self { 23 | ext_metadata_blocks: Vec::with_capacity(num_ext_blocks as usize), 24 | ..Default::default() 25 | } 26 | } 27 | 28 | fn set_num_ext_blocks(&mut self, num_ext_blocks: u64) { 29 | self.num_ext_blocks = num_ext_blocks; 30 | } 31 | 32 | fn num_ext_blocks(&self) -> u64 { 33 | self.num_ext_blocks 34 | } 35 | 36 | fn blocks_ref(&self) -> &Vec { 37 | self.ext_metadata_blocks.as_ref() 38 | } 39 | 40 | fn blocks_mut(&mut self) -> &mut Vec { 41 | self.ext_metadata_blocks.as_mut() 42 | } 43 | 44 | fn parse_block(&mut self, reader: &mut BsIoSliceReader) -> Result<()> { 45 | let ext_block_length = reader.get_ue()?; 46 | let ext_block_level = reader.get_n(8)?; 47 | 48 | let ext_metadata_block = match ext_block_level { 49 | 1 => level1::ExtMetadataBlockLevel1::parse(reader)?, 50 | 2 => level2::ExtMetadataBlockLevel2::parse(reader)?, 51 | 4 => level4::ExtMetadataBlockLevel4::parse(reader)?, 52 | 5 => level5::ExtMetadataBlockLevel5::parse(reader)?, 53 | 6 => level6::ExtMetadataBlockLevel6::parse(reader)?, 54 | 255 => level255::ExtMetadataBlockLevel255::parse(reader)?, 55 | 3 | 8 | 9 | 10 | 11 | 254 => bail!( 56 | "Invalid block level {} for {} RPU", 57 | ext_block_level, 58 | Self::VERSION, 59 | ), 60 | _ => { 61 | ensure!( 62 | false, 63 | format!( 64 | "{} - Unknown metadata block found: Level {}, length {}, please open an issue.", 65 | Self::VERSION, 66 | ext_block_level, 67 | ext_block_length 68 | ) 69 | ); 70 | 71 | reserved::ReservedExtMetadataBlock::parse( 72 | ext_block_length, 73 | ext_block_level, 74 | reader, 75 | )? 76 | } 77 | }; 78 | 79 | ext_metadata_block.validate_and_read_remaining::(reader, ext_block_length)?; 80 | 81 | self.ext_metadata_blocks.push(ext_metadata_block); 82 | 83 | Ok(()) 84 | } 85 | } 86 | 87 | impl CmV29DmData { 88 | pub fn replace_level2_block(&mut self, block: &ExtMetadataBlockLevel2) { 89 | let blocks = self.blocks_mut(); 90 | 91 | let existing_idx = blocks.iter().position(|b| match b { 92 | ExtMetadataBlock::Level2(b) => b.target_max_pq == block.target_max_pq, 93 | _ => false, 94 | }); 95 | 96 | // Replace or add level 2 block 97 | if let Some(i) = existing_idx { 98 | blocks[i] = ExtMetadataBlock::Level2(block.clone()); 99 | } else { 100 | blocks.push(ExtMetadataBlock::Level2(block.clone())); 101 | } 102 | 103 | self.update_extension_block_info(); 104 | } 105 | 106 | /// Validates different level block counts. 107 | /// The specification requires one block of L1, L4, L5, L6 and L255. 108 | /// However they are not really required, so YMMV. 109 | pub fn validate(&self) -> Result<()> { 110 | let blocks = self.blocks_ref(); 111 | 112 | let invalid_blocks_count = blocks 113 | .iter() 114 | .filter(|b| !Self::ALLOWED_BLOCK_LEVELS.contains(&b.level())) 115 | .count(); 116 | 117 | let level1_count = blocks.iter().filter(|b| b.level() == 1).count(); 118 | 119 | let level2_count = blocks.iter().filter(|b| b.level() == 2).count(); 120 | 121 | let level255_count = blocks.iter().filter(|b| b.level() == 255).count(); 122 | 123 | let level4_count = blocks.iter().filter(|b| b.level() == 4).count(); 124 | 125 | let level5_count = blocks.iter().filter(|b| b.level() == 5).count(); 126 | 127 | let level6_count = blocks.iter().filter(|b| b.level() == 6).count(); 128 | 129 | ensure!( 130 | invalid_blocks_count == 0, 131 | format!( 132 | "{}: Only allowed blocks level 1, 2, 4, 5, 6, and 255", 133 | Self::VERSION 134 | ) 135 | ); 136 | 137 | ensure!( 138 | level1_count <= 1, 139 | format!( 140 | "{}: There must be at most one L1 metadata block", 141 | Self::VERSION 142 | ) 143 | ); 144 | ensure!( 145 | level2_count <= 8, 146 | format!( 147 | "{}: There must be at most 8 L2 metadata blocks", 148 | Self::VERSION 149 | ) 150 | ); 151 | ensure!( 152 | level255_count <= 1, 153 | format!( 154 | "{}: There must be at most one L255 metadata block", 155 | Self::VERSION 156 | ) 157 | ); 158 | ensure!( 159 | level4_count <= 1, 160 | format!( 161 | "{}: There must be at most one L4 metadata block", 162 | Self::VERSION 163 | ) 164 | ); 165 | ensure!( 166 | level5_count <= 1, 167 | format!( 168 | "{}: There must be at most one L5 metadata block", 169 | Self::VERSION 170 | ) 171 | ); 172 | ensure!( 173 | level6_count <= 1, 174 | format!( 175 | "{}: There must be at most one L6 metadata block", 176 | Self::VERSION 177 | ) 178 | ); 179 | 180 | Ok(()) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/extension_metadata/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, ensure}; 2 | use bitvec_helpers::{ 3 | bitstream_io_reader::BsIoSliceReader, bitstream_io_writer::BitstreamIoWriter, 4 | }; 5 | 6 | #[cfg(feature = "serde")] 7 | use serde::{Deserialize, Serialize}; 8 | 9 | pub mod blocks; 10 | pub mod cmv29; 11 | pub mod cmv40; 12 | 13 | pub mod primaries; 14 | pub use primaries::*; 15 | 16 | pub use cmv29::CmV29DmData; 17 | pub use cmv40::CmV40DmData; 18 | 19 | use blocks::ExtMetadataBlock; 20 | 21 | #[derive(Debug, Clone)] 22 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 23 | #[cfg_attr(feature = "serde", serde(untagged))] 24 | pub enum DmData { 25 | V29(CmV29DmData), 26 | V40(CmV40DmData), 27 | } 28 | 29 | pub trait ExtMetadata { 30 | fn parse(&mut self, reader: &mut BsIoSliceReader) -> Result<()>; 31 | fn write(&self, writer: &mut BitstreamIoWriter); 32 | } 33 | 34 | pub trait WithExtMetadataBlocks { 35 | const VERSION: &'static str; 36 | const ALLOWED_BLOCK_LEVELS: &'static [u8]; 37 | 38 | fn with_blocks_allocation(num_ext_blocks: u64) -> Self; 39 | 40 | fn set_num_ext_blocks(&mut self, num_ext_blocks: u64); 41 | fn num_ext_blocks(&self) -> u64; 42 | 43 | fn parse_block(&mut self, reader: &mut BsIoSliceReader) -> Result<()>; 44 | fn blocks_ref(&self) -> &Vec; 45 | fn blocks_mut(&mut self) -> &mut Vec; 46 | 47 | fn sort_blocks(&mut self) { 48 | let blocks = self.blocks_mut(); 49 | blocks.sort_by_key(|ext| ext.sort_key()); 50 | } 51 | 52 | fn update_extension_block_info(&mut self) { 53 | self.set_num_ext_blocks(self.blocks_ref().len() as u64); 54 | self.sort_blocks(); 55 | } 56 | 57 | fn add_block(&mut self, meta: ExtMetadataBlock) -> Result<()> { 58 | let level = meta.level(); 59 | 60 | ensure!( 61 | Self::ALLOWED_BLOCK_LEVELS.contains(&level), 62 | "Metadata block level {} is invalid for {}", 63 | &level, 64 | Self::VERSION 65 | ); 66 | 67 | let blocks = self.blocks_mut(); 68 | blocks.push(meta); 69 | 70 | self.update_extension_block_info(); 71 | 72 | Ok(()) 73 | } 74 | 75 | fn remove_level(&mut self, level: u8) { 76 | let blocks = self.blocks_mut(); 77 | blocks.retain(|b| b.level() != level); 78 | 79 | self.update_extension_block_info(); 80 | } 81 | 82 | fn write(&self, writer: &mut BitstreamIoWriter) -> Result<()> { 83 | let num_ext_blocks = self.num_ext_blocks(); 84 | 85 | writer.write_ue(&num_ext_blocks)?; 86 | 87 | // dm_alignment_zero_bit 88 | writer.byte_align()?; 89 | 90 | let ext_metadata_blocks = self.blocks_ref(); 91 | 92 | for ext_metadata_block in ext_metadata_blocks { 93 | let remaining_bits = 94 | ext_metadata_block.length_bits() - ext_metadata_block.required_bits(); 95 | 96 | writer.write_ue(&ext_metadata_block.length_bytes())?; 97 | writer.write_n(&ext_metadata_block.level(), 8)?; 98 | 99 | ext_metadata_block.write(writer)?; 100 | 101 | // ext_dm_alignment_zero_bit 102 | for _ in 0..remaining_bits { 103 | writer.write(false)?; 104 | } 105 | } 106 | 107 | Ok(()) 108 | } 109 | } 110 | 111 | impl DmData { 112 | pub(crate) fn parse( 113 | reader: &mut BsIoSliceReader, 114 | ) -> Result> { 115 | let num_ext_blocks = reader.get_ue()?; 116 | let mut meta = T::with_blocks_allocation(num_ext_blocks); 117 | 118 | meta.set_num_ext_blocks(num_ext_blocks); 119 | 120 | while !reader.is_aligned() { 121 | ensure!( 122 | !reader.get()?, 123 | format!("{}: dm_alignment_zero_bit != 0", T::VERSION) 124 | ); 125 | } 126 | 127 | for _ in 0..num_ext_blocks { 128 | meta.parse_block(reader)?; 129 | } 130 | 131 | Ok(Some(meta)) 132 | } 133 | 134 | pub fn write(&self, writer: &mut BitstreamIoWriter) -> Result<()> { 135 | match self { 136 | DmData::V29(m) => m.write(writer), 137 | DmData::V40(m) => m.write(writer), 138 | } 139 | } 140 | 141 | pub fn validate(&self) -> Result<()> { 142 | match self { 143 | DmData::V29(m) => m.validate(), 144 | DmData::V40(m) => m.validate(), 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/extension_metadata/primaries.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | #[cfg(feature = "serde")] 4 | use serde::{Deserialize, Serialize}; 5 | 6 | pub const PREDEFINED_COLORSPACE_PRIMARIES: &[[f64; 8]] = &[ 7 | [0.68, 0.32, 0.265, 0.69, 0.15, 0.06, 0.3127, 0.329], // 0, DCI-P3 D65 8 | [0.64, 0.33, 0.30, 0.60, 0.15, 0.06, 0.3127, 0.329], // 1, BT.709 9 | [0.708, 0.292, 0.170, 0.797, 0.131, 0.046, 0.3127, 0.329], // 2, BT.2020 10 | [0.63, 0.34, 0.31, 0.595, 0.155, 0.07, 0.3127, 0.329], // 3, BT.601 NTSC / SMPTE-C 11 | [0.64, 0.33, 0.29, 0.60, 0.15, 0.06, 0.3127, 0.329], // 4, BT.601 PAL / BT.470 BG 12 | [0.68, 0.32, 0.265, 0.69, 0.15, 0.06, 0.314, 0.351], // 5, DCI-P3 13 | [0.7347, 0.2653, 0.0, 1.0, 0.0001, -0.077, 0.32168, 0.33767], // 6, ACES 14 | [0.73, 0.28, 0.14, 0.855, 0.10, -0.05, 0.3127, 0.329], // 7, S-Gamut 15 | [0.766, 0.275, 0.225, 0.80, 0.089, -0.087, 0.3127, 0.329], // 8, S-Gamut-3.Cine 16 | ]; 17 | 18 | #[derive(Debug, Default, Clone, Copy)] 19 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 20 | pub struct ColorPrimaries { 21 | pub red_x: u16, 22 | pub red_y: u16, 23 | pub green_x: u16, 24 | pub green_y: u16, 25 | pub blue_x: u16, 26 | pub blue_y: u16, 27 | pub white_x: u16, 28 | pub white_y: u16, 29 | } 30 | 31 | #[derive(Debug, Clone, Copy)] 32 | #[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] 33 | pub enum MasteringDisplayPrimaries { 34 | #[cfg_attr(feature = "serde", serde(alias = "DCI-P3 D65"))] 35 | DCIP3D65 = 0, 36 | #[cfg_attr(feature = "serde", serde(alias = "BT.709"))] 37 | BT709, 38 | #[cfg_attr(feature = "serde", serde(alias = "BT.2020"))] 39 | BT2020, 40 | #[cfg_attr(feature = "serde", serde(alias = "SMPTE-C"))] 41 | SMPTEC, 42 | #[cfg_attr(feature = "serde", serde(alias = "BT.601"))] 43 | BT601, 44 | #[cfg_attr(feature = "serde", serde(alias = "DCI-P3"))] 45 | DCIP3, 46 | ACES, 47 | #[cfg_attr(feature = "serde", serde(alias = "S-Gamut"))] 48 | SGamut, 49 | #[cfg_attr(feature = "serde", serde(alias = "S-Gamut-3.Cine"))] 50 | SGamut3Cine, 51 | } 52 | 53 | impl From for MasteringDisplayPrimaries { 54 | fn from(value: u8) -> Self { 55 | match value { 56 | 0 => Self::DCIP3D65, 57 | 1 => Self::BT709, 58 | 2 => Self::BT2020, 59 | 3 => Self::SMPTEC, 60 | 4 => Self::BT601, 61 | 5 => Self::DCIP3, 62 | 6 => Self::ACES, 63 | 7 => Self::SGamut, 64 | 8 => Self::SGamut3Cine, 65 | _ => Self::DCIP3D65, 66 | } 67 | } 68 | } 69 | 70 | impl std::fmt::Display for MasteringDisplayPrimaries { 71 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 72 | let alias = match self { 73 | Self::DCIP3D65 => "DCI-P3 D65", 74 | Self::BT709 => "BT.709", 75 | Self::BT2020 => "BT.2020", 76 | Self::SMPTEC => "SMPTE-C", 77 | Self::BT601 => "BT.601", 78 | Self::DCIP3 => "DCI-P3", 79 | Self::ACES => "ACES", 80 | Self::SGamut => "S-Gamut", 81 | Self::SGamut3Cine => "S-Gamut-3.Cine", 82 | }; 83 | write!(f, "{}", alias) 84 | } 85 | } 86 | 87 | impl ColorPrimaries { 88 | pub fn from_array_int(primaries: &[u16; 8]) -> ColorPrimaries { 89 | Self { 90 | red_x: primaries[0], 91 | red_y: primaries[1], 92 | green_x: primaries[2], 93 | green_y: primaries[3], 94 | blue_x: primaries[4], 95 | blue_y: primaries[5], 96 | white_x: primaries[6], 97 | white_y: primaries[7], 98 | } 99 | } 100 | 101 | pub fn from_array_float(primaries: &[f64; 8]) -> ColorPrimaries { 102 | // Float to integer primaries 103 | let primaries_int = f64_to_integer_primaries(primaries); 104 | 105 | Self::from_array_int(&primaries_int) 106 | } 107 | 108 | pub fn from_enum(primary: MasteringDisplayPrimaries) -> ColorPrimaries { 109 | Self::from_array_float(&PREDEFINED_COLORSPACE_PRIMARIES[primary as usize]) 110 | } 111 | } 112 | 113 | /// Assumes a list of size 8, otherwise panics 114 | pub fn f64_to_integer_primaries(primaries: &[f64]) -> [u16; 8] { 115 | const SCALE: f64 = 1.0 / 32767.0; 116 | 117 | primaries 118 | .iter() 119 | .map(|v| (v / SCALE).round() as u16) 120 | .collect::>() 121 | .try_into() 122 | .unwrap() 123 | } 124 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/mod.rs: -------------------------------------------------------------------------------- 1 | use crc::{CRC_32_MPEG_2, Crc, Table}; 2 | 3 | pub mod dovi_rpu; 4 | pub mod extension_metadata; 5 | pub mod generate; 6 | pub mod profiles; 7 | pub mod rpu_data_header; 8 | pub mod rpu_data_mapping; 9 | pub mod rpu_data_nlq; 10 | pub mod vdr_dm_data; 11 | 12 | pub mod utils; 13 | 14 | static CRC32_INSTANCE: Crc> = Crc::>::new(&CRC_32_MPEG_2); 15 | 16 | pub const NUM_COMPONENTS: usize = 3; 17 | 18 | pub(crate) const MMR_MAX_COEFFS: usize = 7; 19 | pub(crate) const NLQ_NUM_PIVOTS: usize = 2; 20 | 21 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 22 | pub enum ConversionMode { 23 | Lossless = 0, 24 | ToMel, 25 | To81, 26 | To84, 27 | To81MappingPreserved, 28 | } 29 | 30 | #[inline(always)] 31 | fn compute_crc32(data: &[u8]) -> u32 { 32 | CRC32_INSTANCE.checksum(data) 33 | } 34 | 35 | impl From for ConversionMode { 36 | fn from(mode: u8) -> ConversionMode { 37 | match mode { 38 | 0 => ConversionMode::Lossless, 39 | 1 => ConversionMode::ToMel, 40 | 2 | 3 => ConversionMode::To81, 41 | 4 => ConversionMode::To84, 42 | _ => ConversionMode::Lossless, 43 | } 44 | } 45 | } 46 | 47 | impl std::fmt::Display for ConversionMode { 48 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 49 | match self { 50 | ConversionMode::Lossless => write!(f, "Lossless"), 51 | ConversionMode::ToMel => write!(f, "To MEL"), 52 | ConversionMode::To81 => write!(f, "To 8.1"), 53 | ConversionMode::To84 => write!(f, "To 8.4"), 54 | ConversionMode::To81MappingPreserved => { 55 | write!(f, "To 8.1, preserving the mapping metadata") 56 | } 57 | } 58 | } 59 | } 60 | 61 | impl Default for ConversionMode { 62 | fn default() -> Self { 63 | Self::Lossless 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/profiles/mod.rs: -------------------------------------------------------------------------------- 1 | use super::vdr_dm_data::VdrDmData; 2 | 3 | pub mod profile4; 4 | pub mod profile5; 5 | pub mod profile7; 6 | pub mod profile81; 7 | pub mod profile84; 8 | 9 | pub trait DoviProfile { 10 | fn dm_data() -> VdrDmData { 11 | VdrDmData::default_pq() 12 | } 13 | 14 | fn backwards_compatible() -> bool { 15 | true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/profiles/profile4.rs: -------------------------------------------------------------------------------- 1 | use super::{DoviProfile, VdrDmData}; 2 | 3 | pub struct Profile4 {} 4 | 5 | impl DoviProfile for Profile4 { 6 | fn dm_data() -> VdrDmData { 7 | VdrDmData { 8 | ycc_to_rgb_coef0: 9575, 9 | ycc_to_rgb_coef1: 0, 10 | ycc_to_rgb_coef2: 14742, 11 | ycc_to_rgb_coef3: 9575, 12 | ycc_to_rgb_coef4: -1754, 13 | ycc_to_rgb_coef5: -4383, 14 | ycc_to_rgb_coef6: 9575, 15 | ycc_to_rgb_coef7: 17372, 16 | ycc_to_rgb_coef8: 0, 17 | ycc_to_rgb_offset0: 67108864, 18 | ycc_to_rgb_offset1: 536870912, 19 | ycc_to_rgb_offset2: 536870912, 20 | rgb_to_lms_coef0: 5845, 21 | rgb_to_lms_coef1: 9702, 22 | rgb_to_lms_coef2: 837, 23 | rgb_to_lms_coef3: 2568, 24 | rgb_to_lms_coef4: 12256, 25 | rgb_to_lms_coef5: 1561, 26 | rgb_to_lms_coef6: 0, 27 | rgb_to_lms_coef7: 679, 28 | rgb_to_lms_coef8: 15705, 29 | signal_eotf: 39322, 30 | signal_eotf_param0: 15867, 31 | signal_eotf_param1: 228, 32 | signal_eotf_param2: 1383604, 33 | signal_bit_depth: 14, 34 | signal_full_range_flag: 1, 35 | source_diagonal: 42, 36 | ..Default::default() 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/profiles/profile5.rs: -------------------------------------------------------------------------------- 1 | use super::{DoviProfile, VdrDmData}; 2 | 3 | pub struct Profile5 {} 4 | 5 | impl DoviProfile for Profile5 { 6 | fn dm_data() -> VdrDmData { 7 | VdrDmData { 8 | ycc_to_rgb_coef0: 8192, 9 | ycc_to_rgb_coef1: 799, 10 | ycc_to_rgb_coef2: 1681, 11 | ycc_to_rgb_coef3: 8192, 12 | ycc_to_rgb_coef4: -933, 13 | ycc_to_rgb_coef5: 1091, 14 | ycc_to_rgb_coef6: 8192, 15 | ycc_to_rgb_coef7: 267, 16 | ycc_to_rgb_coef8: -5545, 17 | ycc_to_rgb_offset0: 0, 18 | ycc_to_rgb_offset1: 134217728, 19 | ycc_to_rgb_offset2: 134217728, 20 | rgb_to_lms_coef0: 17081, 21 | rgb_to_lms_coef1: -349, 22 | rgb_to_lms_coef2: -349, 23 | rgb_to_lms_coef3: -349, 24 | rgb_to_lms_coef4: 17081, 25 | rgb_to_lms_coef5: -349, 26 | rgb_to_lms_coef6: -349, 27 | rgb_to_lms_coef7: -349, 28 | rgb_to_lms_coef8: 17081, 29 | signal_color_space: 2, 30 | ..VdrDmData::default_pq() 31 | } 32 | } 33 | 34 | fn backwards_compatible() -> bool { 35 | false 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/profiles/profile7.rs: -------------------------------------------------------------------------------- 1 | use super::{DoviProfile, VdrDmData, profile81::Profile81}; 2 | 3 | pub struct Profile7 {} 4 | 5 | impl DoviProfile for Profile7 { 6 | fn dm_data() -> VdrDmData { 7 | Profile81::dm_data() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/profiles/profile81.rs: -------------------------------------------------------------------------------- 1 | use crate::rpu::{ 2 | NUM_COMPONENTS, 3 | rpu_data_mapping::{ 4 | DoviMappingMethod, DoviPolynomialCurve, DoviReshapingCurve, RpuDataMapping, 5 | }, 6 | }; 7 | 8 | use super::{DoviProfile, VdrDmData}; 9 | 10 | pub struct Profile81 {} 11 | 12 | impl DoviProfile for Profile81 { 13 | fn dm_data() -> VdrDmData { 14 | VdrDmData { 15 | ycc_to_rgb_coef0: 9574, 16 | ycc_to_rgb_coef1: 0, 17 | ycc_to_rgb_coef2: 13802, 18 | ycc_to_rgb_coef3: 9574, 19 | ycc_to_rgb_coef4: -1540, 20 | ycc_to_rgb_coef5: -5348, 21 | ycc_to_rgb_coef6: 9574, 22 | ycc_to_rgb_coef7: 17610, 23 | ycc_to_rgb_coef8: 0, 24 | ycc_to_rgb_offset0: 16777216, 25 | ycc_to_rgb_offset1: 134217728, 26 | ycc_to_rgb_offset2: 134217728, 27 | rgb_to_lms_coef0: 7222, 28 | rgb_to_lms_coef1: 8771, 29 | rgb_to_lms_coef2: 390, 30 | rgb_to_lms_coef3: 2654, 31 | rgb_to_lms_coef4: 12430, 32 | rgb_to_lms_coef5: 1300, 33 | rgb_to_lms_coef6: 0, 34 | rgb_to_lms_coef7: 422, 35 | rgb_to_lms_coef8: 15962, 36 | ..VdrDmData::default_pq() 37 | } 38 | } 39 | } 40 | 41 | impl Profile81 { 42 | pub fn rpu_data_mapping() -> RpuDataMapping { 43 | let curves: [DoviReshapingCurve; NUM_COMPONENTS] = [ 44 | Self::dovi_reshaping_curve(), 45 | Self::dovi_reshaping_curve(), 46 | Self::dovi_reshaping_curve(), 47 | ]; 48 | 49 | RpuDataMapping { 50 | vdr_rpu_id: 0, 51 | mapping_color_space: 0, 52 | mapping_chroma_format_idc: 0, 53 | nlq_method_idc: None, 54 | nlq_num_pivots_minus2: None, 55 | nlq_pred_pivot_value: None, 56 | num_x_partitions_minus1: 0, 57 | num_y_partitions_minus1: 0, 58 | curves, 59 | nlq: None, 60 | } 61 | } 62 | 63 | pub fn dovi_reshaping_curve() -> DoviReshapingCurve { 64 | DoviReshapingCurve { 65 | num_pivots_minus2: 0, 66 | pivots: vec![0, 1023], 67 | mapping_idc: DoviMappingMethod::Polynomial, 68 | polynomial: Some(DoviPolynomialCurve::p81_default()), 69 | mmr: None, 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/profiles/profile84.rs: -------------------------------------------------------------------------------- 1 | use tinyvec::array_vec; 2 | 3 | use crate::rpu::{ 4 | NUM_COMPONENTS, 5 | rpu_data_mapping::{ 6 | DoviMMRCurve, DoviMappingMethod, DoviPolynomialCurve, DoviReshapingCurve, RpuDataMapping, 7 | }, 8 | }; 9 | 10 | use super::{DoviProfile, VdrDmData, profile81::Profile81}; 11 | 12 | pub struct Profile84 {} 13 | 14 | impl DoviProfile for Profile84 { 15 | fn dm_data() -> VdrDmData { 16 | VdrDmData { 17 | source_min_pq: 62, 18 | source_max_pq: 3079, 19 | ..Profile81::dm_data() 20 | } 21 | } 22 | } 23 | 24 | // Based on iPhone 13 polynomials and MMR 25 | impl Profile84 { 26 | pub fn rpu_data_mapping() -> RpuDataMapping { 27 | // Luma component 28 | let poly_coef_int = vec![ 29 | array_vec!(-1, 1, -3), 30 | array_vec!(-1, 1, -2), 31 | array_vec!(0, 0, -1), 32 | array_vec!(0, 0, 0), 33 | array_vec!(0, -2, 1), 34 | array_vec!(6, -14, 8), 35 | array_vec!(13, -30, 16), 36 | array_vec!(28, -62, 34), 37 | ]; 38 | 39 | let poly_coef = vec![ 40 | array_vec!(7978928, 8332855, 4889184), 41 | array_vec!(8269552, 5186604, 3909327), 42 | array_vec!(1317527, 5338528, 7440486), 43 | array_vec!(2119979, 2065496, 2288524), 44 | array_vec!(7982780, 5409990, 1585336), 45 | array_vec!(3460436, 3197328, 615464), 46 | array_vec!(3921968, 6820672, 5546752), 47 | array_vec!(1947392, 1244640, 6094272), 48 | ]; 49 | 50 | let poly_curve = DoviPolynomialCurve { 51 | poly_order_minus1: vec![1; 8], 52 | linear_interp_flag: vec![], 53 | poly_coef_int, 54 | poly_coef, 55 | }; 56 | let luma_reshaping_curve = DoviReshapingCurve { 57 | num_pivots_minus2: 7, 58 | pivots: vec![63, 69, 230, 256, 256, 37, 16, 8, 7], 59 | mapping_idc: DoviMappingMethod::Polynomial, 60 | polynomial: Some(poly_curve), 61 | mmr: None, 62 | }; 63 | 64 | // Chroma component 1 65 | let mmr_coef_int_cmp1 = vec![array_vec!( 66 | array_vec!(-1, -2, -5, 2, 5, 9, -12), 67 | array_vec!(-1, -1, 3, -1, -5, -12, 18), 68 | array_vec!(-1, 0, -2, 0, 2, 7, -19) 69 | )]; 70 | let mmr_coef_cmp1 = vec![array_vec!( 71 | array_vec!(87355, 6228986, 642500, 1023296, 6569512, 5128216, 4317296), 72 | array_vec!( 73 | 8299905, 5819931, 2324124, 7273546, 1562484, 3679480, 6357360 74 | ), 75 | array_vec!(8172981, 3261951, 5970055, 927142, 3525840, 5110348, 6236848) 76 | )]; 77 | let mmr_curve1 = DoviMMRCurve { 78 | mmr_order_minus1: vec![2], 79 | mmr_constant_int: vec![1], 80 | mmr_constant: vec![1150183], 81 | mmr_coef_int: mmr_coef_int_cmp1, 82 | mmr_coef: mmr_coef_cmp1, 83 | }; 84 | let chroma_reshaping_curve1 = DoviReshapingCurve { 85 | num_pivots_minus2: 0, 86 | pivots: vec![0, 1023], 87 | mapping_idc: DoviMappingMethod::MMR, 88 | polynomial: None, 89 | mmr: Some(mmr_curve1), 90 | }; 91 | 92 | // Chroma component 2 93 | let mmr_coef_int_cmp2 = vec![array_vec!( 94 | array_vec!(4, 0, 5, -2, -8, -1, 1), 95 | array_vec!(-4, -1, -6, 1, 12, 0, -4), 96 | array_vec!(1, 0, 2, -1, -8, -1, 4) 97 | )]; 98 | let mmr_coef_cmp2 = vec![array_vec!( 99 | array_vec!(193104, 5369128, 2553116, 8009648, 2772020, 3122453, 2961581), 100 | array_vec!(6769788, 2565605, 7864496, 4777288, 649616, 7036536, 1666406), 101 | array_vec!(406265, 2901521, 2680224, 146340, 1008052, 4366810, 5080852) 102 | )]; 103 | let mmr_curve2 = DoviMMRCurve { 104 | mmr_order_minus1: vec![2], 105 | mmr_constant_int: vec![-2], 106 | mmr_constant: vec![6266112], 107 | mmr_coef_int: mmr_coef_int_cmp2, 108 | mmr_coef: mmr_coef_cmp2, 109 | }; 110 | let chroma_reshaping_curve2 = DoviReshapingCurve { 111 | num_pivots_minus2: 0, 112 | pivots: vec![0, 1023], 113 | mapping_idc: DoviMappingMethod::MMR, 114 | polynomial: None, 115 | mmr: Some(mmr_curve2), 116 | }; 117 | 118 | let curves: [DoviReshapingCurve; NUM_COMPONENTS] = [ 119 | luma_reshaping_curve, 120 | chroma_reshaping_curve1, 121 | chroma_reshaping_curve2, 122 | ]; 123 | 124 | RpuDataMapping { 125 | vdr_rpu_id: 0, 126 | mapping_color_space: 0, 127 | mapping_chroma_format_idc: 0, 128 | nlq_method_idc: None, 129 | nlq_num_pivots_minus2: None, 130 | nlq_pred_pivot_value: None, 131 | num_x_partitions_minus1: 0, 132 | num_y_partitions_minus1: 0, 133 | curves, 134 | nlq: None, 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /dolby_vision/src/rpu/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::{BufReader, Read}, 4 | path::Path, 5 | }; 6 | 7 | use anyhow::{Result, bail}; 8 | 9 | use super::dovi_rpu::DoviRpu; 10 | 11 | pub fn parse_rpu_file>(input: P) -> Result> { 12 | let rpu_file = File::open(input)?; 13 | let metadata = rpu_file.metadata()?; 14 | let file_size_bytes = metadata.len() as usize; 15 | 16 | let mut reader = BufReader::new(rpu_file); 17 | 18 | let chunk_size = 100_000; 19 | let mut main_buf = vec![0; chunk_size]; 20 | let mut chunk = Vec::with_capacity(chunk_size); 21 | let mut end = Vec::with_capacity(chunk_size); 22 | 23 | let mut offsets_count = 0; 24 | // Estimate RPU count from file size 25 | let mut rpus: Vec = Vec::with_capacity(chunk_size / 400); 26 | let mut warning_error = None; 27 | 28 | while let Ok(n) = reader.read(&mut main_buf) { 29 | let read_bytes = n; 30 | if read_bytes == 0 && end.is_empty() && chunk.is_empty() { 31 | break; 32 | } 33 | 34 | if read_bytes < chunk_size { 35 | chunk.extend_from_slice(&main_buf[..read_bytes]); 36 | } else { 37 | chunk.extend_from_slice(&main_buf); 38 | } 39 | 40 | let mut offsets: Vec = chunk 41 | .windows(4) 42 | .enumerate() 43 | .filter_map(|(i, chunk)| { 44 | if matches!(chunk, &[0, 0, 0, 1]) { 45 | Some(i) 46 | } else { 47 | None 48 | } 49 | }) 50 | .collect(); 51 | 52 | if offsets.is_empty() { 53 | bail!("No NALU start codes found in chunk. Maybe not a valid RPU?"); 54 | } 55 | 56 | let last = if read_bytes < chunk_size { 57 | *offsets.last().unwrap() 58 | } else { 59 | let last = offsets.pop().unwrap(); 60 | 61 | end.clear(); 62 | end.extend_from_slice(&chunk[last..]); 63 | 64 | last 65 | }; 66 | 67 | let count = offsets.len(); 68 | let parsed_rpus_iter = offsets 69 | .iter() 70 | .enumerate() 71 | .map(|(index, offset)| { 72 | let size = if offset == &last { 73 | chunk.len() - offset 74 | } else { 75 | let size = if index == count - 1 { 76 | last - offset 77 | } else { 78 | offsets[index + 1] - offset 79 | }; 80 | 81 | match &chunk[offset + size - 1..offset + size + 3] { 82 | [0, 0, 0, 1] => size - 1, 83 | _ => size, 84 | } 85 | }; 86 | 87 | let start = *offset; 88 | let end = start + size; 89 | 90 | DoviRpu::parse_unspec62_nalu(&chunk[start..end]) 91 | }) 92 | .enumerate() 93 | .filter_map(|(i, res)| { 94 | if let Err(e) = &res { 95 | if warning_error.is_none() { 96 | warning_error = Some(format!("Found invalid RPU: Index {i}, error: {e}")) 97 | } 98 | } 99 | 100 | res.ok() 101 | }); 102 | rpus.extend(parsed_rpus_iter); 103 | 104 | if warning_error.is_some() { 105 | offsets_count += count; 106 | break; 107 | } else if rpus.is_empty() { 108 | bail!("No valid RPUs parsed for chunk, assuming invalid RPU file."); 109 | } 110 | 111 | if offsets_count == 0 && file_size_bytes > chunk_size { 112 | rpus.reserve((metadata.len() as usize - chunk_size) / 400); 113 | } 114 | offsets_count += count; 115 | 116 | chunk.clear(); 117 | 118 | if !end.is_empty() { 119 | chunk.extend_from_slice(&end); 120 | end.clear() 121 | } 122 | } 123 | 124 | if offsets_count > 0 && rpus.len() == offsets_count { 125 | Ok(rpus) 126 | } else if offsets_count == 0 { 127 | bail!("No RPU found"); 128 | } else if let Some(error) = warning_error { 129 | bail!("{}", error); 130 | } else { 131 | bail!( 132 | "Number of valid RPUs different from total: expected {} got {}", 133 | offsets_count, 134 | rpus.len() 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /dolby_vision/src/st2094_10/itu_t35/dm_data.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use bitvec_helpers::bitstream_io_reader::BsIoSliceReader; 3 | 4 | use crate::rpu::extension_metadata::{CmV29DmData, DmData}; 5 | 6 | use super::UserDataTypeStruct; 7 | 8 | #[derive(Default, Debug)] 9 | pub struct ST2094_10DmData { 10 | pub app_identifier: u64, 11 | pub app_version: u64, 12 | pub metadata_refresh_flag: bool, 13 | pub dm_data: Option, 14 | } 15 | 16 | impl ST2094_10DmData { 17 | pub(crate) fn parse(reader: &mut BsIoSliceReader) -> Result { 18 | let mut meta = ST2094_10DmData { 19 | app_identifier: reader.get_ue()?, 20 | app_version: reader.get_ue()?, 21 | metadata_refresh_flag: reader.get()?, 22 | ..Default::default() 23 | }; 24 | 25 | if meta.metadata_refresh_flag { 26 | meta.dm_data = DmData::parse::(reader)?; 27 | } 28 | 29 | Ok(UserDataTypeStruct::DMData(meta)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /dolby_vision/src/st2094_10/itu_t35/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, bail, ensure}; 2 | use bitvec_helpers::bitstream_io_reader::BsIoSliceReader; 3 | 4 | use crate::utils::clear_start_code_emulation_prevention_3_byte; 5 | 6 | mod cm_data; 7 | mod dm_data; 8 | 9 | use cm_data::ST2094_10CmData; 10 | use dm_data::ST2094_10DmData; 11 | 12 | /// ITU T.35 SEI version of ST2094-10 metadata 13 | #[derive(Debug)] 14 | pub struct ST2094_10ItuT35 { 15 | pub user_data_type_struct: UserDataTypeStruct, 16 | } 17 | 18 | #[derive(Debug)] 19 | pub enum UserDataTypeStruct { 20 | DMData(ST2094_10DmData), 21 | CMData(Box), 22 | } 23 | 24 | impl ST2094_10ItuT35 { 25 | /// Implementation of https://dashif-documents.azurewebsites.net/DASH-IF-IOP/master/DASH-IF-IOP.html#codecs-dolbyvision 26 | pub fn parse_itu_t35_dashif(data: &[u8]) -> Result { 27 | let trimmed_data = Self::validated_trimmed_data(data)?; 28 | let bytes = clear_start_code_emulation_prevention_3_byte(trimmed_data); 29 | 30 | let mut reader = BsIoSliceReader::from_slice(&bytes); 31 | 32 | let itu_t_t35_country_code: u8 = reader.get_n(8)?; 33 | let itu_t_t35_provider_code: u16 = reader.get_n(16)?; 34 | 35 | ensure!(itu_t_t35_country_code == 0xB5); 36 | ensure!(itu_t_t35_provider_code == 0x31); 37 | 38 | let user_identifier: u32 = reader.get_n(32)?; 39 | ensure!( 40 | user_identifier == 0x47413934, 41 | "invalid user_identifier: {}", 42 | user_identifier 43 | ); 44 | 45 | let user_data_type_code: u8 = reader.get_n(8)?; 46 | 47 | let meta = match user_data_type_code { 48 | 0x08 => ST2094_10CmData::parse(&mut reader)?, 49 | 0x09 => ST2094_10DmData::parse(&mut reader)?, 50 | _ => bail!("Invalid user_data_type_code: {}", user_data_type_code), 51 | }; 52 | 53 | Ok(ST2094_10ItuT35 { 54 | user_data_type_struct: meta, 55 | }) 56 | } 57 | 58 | pub fn validated_trimmed_data(data: &[u8]) -> Result<&[u8]> { 59 | let trimmed_data = match &data[..7] { 60 | [0x4E, 0x01, 0x04, _, 0xB5, 0x00, 0x31] => &data[4..], 61 | [0xB5, 0x00, 0x31, 0x47, 0x41, 0x39, 0x34] => data, 62 | _ => bail!("Invalid St2094-10 T-T35 SEI start bytes\n{:?}", &data[..7]), 63 | }; 64 | 65 | Ok(trimmed_data) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /dolby_vision/src/st2094_10/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod itu_t35; 2 | -------------------------------------------------------------------------------- /dolby_vision/src/utils.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "serde")] 2 | use { 3 | bitvec::prelude::*, 4 | serde::{Serialize, ser::Serializer}, 5 | }; 6 | 7 | pub const ST2084_Y_MAX: f64 = 10000.0; 8 | pub const ST2084_M1: f64 = 2610.0 / 16384.0; 9 | pub const ST2084_M2: f64 = (2523.0 / 4096.0) * 128.0; 10 | pub const ST2084_C1: f64 = 3424.0 / 4096.0; 11 | pub const ST2084_C2: f64 = (2413.0 / 4096.0) * 32.0; 12 | pub const ST2084_C3: f64 = (2392.0 / 4096.0) * 32.0; 13 | 14 | #[inline(always)] 15 | pub fn pq_to_nits(x: f64) -> f64 { 16 | if x > 0.0 { 17 | let xpow = x.powf(1.0 / ST2084_M2); 18 | let num = (xpow - ST2084_C1).max(0.0); 19 | let den = (ST2084_C2 - ST2084_C3 * xpow).max(f64::NEG_INFINITY); 20 | 21 | (num / den).powf(1.0 / ST2084_M1) * ST2084_Y_MAX 22 | } else { 23 | 0.0 24 | } 25 | } 26 | 27 | /// Helper function to calculate PQ codes from nits (cd/m2) values 28 | #[inline(always)] 29 | pub fn nits_to_pq(nits: f64) -> f64 { 30 | let y = nits / ST2084_Y_MAX; 31 | 32 | ((ST2084_C1 + ST2084_C2 * y.powf(ST2084_M1)) / (1.0 + ST2084_C3 * y.powf(ST2084_M1))) 33 | .powf(ST2084_M2) 34 | } 35 | 36 | /// Helper function to calculate PQ codes from nits (cd/m2) values, as 12 bit integer 37 | #[inline(always)] 38 | pub fn nits_to_pq_12_bit>(nits: T) -> u16 { 39 | (nits_to_pq(nits.into()) * 4095.0).round() as u16 40 | } 41 | 42 | /// Copied from hevc_parser for convenience, and to avoid a dependency 43 | /// Unescapes a byte slice from annexb. 44 | /// Allocates a new Vec. 45 | pub fn clear_start_code_emulation_prevention_3_byte(data: &[u8]) -> Vec { 46 | let len = data.len(); 47 | 48 | if len > 2 { 49 | let mut unescaped_bytes: Vec = Vec::with_capacity(len); 50 | unescaped_bytes.push(data[0]); 51 | unescaped_bytes.push(data[1]); 52 | 53 | for i in 2..len { 54 | if !(data[i - 2] == 0 && data[i - 1] == 0 && data[i] == 3) { 55 | unescaped_bytes.push(data[i]); 56 | } 57 | } 58 | 59 | unescaped_bytes 60 | } else { 61 | data.to_owned() 62 | } 63 | } 64 | 65 | /// Escapes the vec to annexb to avoid emulating a start code by accident 66 | pub fn add_start_code_emulation_prevention_3_byte(data: &mut Vec) { 67 | let mut count = data.len(); 68 | let mut i = 0; 69 | 70 | while i < count { 71 | if i > 2 && data[i - 2] == 0 && data[i - 1] == 0 && data[i] <= 3 { 72 | data.insert(i, 3); 73 | count += 1; 74 | } 75 | 76 | i += 1; 77 | } 78 | } 79 | 80 | /// Serializing a bitvec as a vec of bits 81 | #[cfg(feature = "serde")] 82 | pub(crate) fn bitvec_ser_bits( 83 | bitvec: &BitVec, 84 | s: S, 85 | ) -> Result { 86 | let bits: Vec = bitvec.iter().map(|b| *b as u8).collect(); 87 | bits.serialize(s) 88 | } 89 | 90 | /// Serializing an optional bitvec as a vec of bits 91 | #[cfg(feature = "serde")] 92 | pub(crate) fn opt_bitvec_ser_bits( 93 | bitvec: &Option>, 94 | s: S, 95 | ) -> Result { 96 | let bits: Vec = if let Some(vec) = bitvec { 97 | vec.iter().map(|b| *b as u8).collect() 98 | } else { 99 | Vec::new() 100 | }; 101 | bits.serialize(s) 102 | } 103 | -------------------------------------------------------------------------------- /dolby_vision/src/xml/mod.rs: -------------------------------------------------------------------------------- 1 | /// XML metadata parser 2 | mod parser; 3 | 4 | #[cfg(test)] 5 | mod tests; 6 | 7 | pub use parser::{CmXmlParser, XmlParserOpts}; 8 | -------------------------------------------------------------------------------- /src/commands/convert.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args, ValueHint}; 2 | use std::path::PathBuf; 3 | 4 | #[derive(Args, Debug)] 5 | pub struct ConvertArgs { 6 | #[arg( 7 | id = "input", 8 | help = "Sets the input HEVC file to use, or piped with -", 9 | long, 10 | short = 'i', 11 | conflicts_with = "input_pos", 12 | required_unless_present = "input_pos", 13 | value_hint = ValueHint::FilePath, 14 | )] 15 | pub input: Option, 16 | 17 | #[arg( 18 | id = "input_pos", 19 | help = "Sets the input HEVC file to use, or piped with - (positional)", 20 | conflicts_with = "input", 21 | required_unless_present = "input", 22 | value_hint = ValueHint::FilePath 23 | )] 24 | pub input_pos: Option, 25 | 26 | #[arg( 27 | long, 28 | short = 'o', 29 | help = "Converted single layer output file location", 30 | value_hint = ValueHint::FilePath 31 | )] 32 | pub output: Option, 33 | 34 | #[arg(short = 'd', long, help = "Discard the EL stream")] 35 | pub discard: bool, 36 | } 37 | -------------------------------------------------------------------------------- /src/commands/demux.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args, ValueHint}; 2 | use std::path::PathBuf; 3 | 4 | #[derive(Args, Debug)] 5 | pub struct DemuxArgs { 6 | #[arg( 7 | id = "input", 8 | help = "Sets the input HEVC file to use, or piped with -", 9 | long, 10 | short = 'i', 11 | conflicts_with = "input_pos", 12 | required_unless_present = "input_pos", 13 | value_hint = ValueHint::FilePath, 14 | )] 15 | pub input: Option, 16 | 17 | #[arg( 18 | id = "input_pos", 19 | help = "Sets the input HEVC file to use, or piped with - (positional)", 20 | conflicts_with = "input", 21 | required_unless_present = "input", 22 | value_hint = ValueHint::FilePath 23 | )] 24 | pub input_pos: Option, 25 | 26 | #[arg( 27 | long, 28 | short = 'b', 29 | help = "BL output file location", 30 | value_hint = ValueHint::FilePath 31 | )] 32 | pub bl_out: Option, 33 | 34 | #[arg( 35 | long, 36 | short = 'e', 37 | help = "EL output file location", 38 | value_hint = ValueHint::FilePath 39 | )] 40 | pub el_out: Option, 41 | 42 | #[arg(long, help = "Output the EL file only")] 43 | pub el_only: bool, 44 | } 45 | -------------------------------------------------------------------------------- /src/commands/editor.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args, ValueHint}; 2 | use std::path::PathBuf; 3 | 4 | #[derive(Args, Debug)] 5 | pub struct EditorArgs { 6 | #[arg( 7 | id = "input", 8 | help = "Sets the input RPU file to use", 9 | long, 10 | short = 'i', 11 | conflicts_with = "input_pos", 12 | required_unless_present = "input_pos", 13 | value_hint = ValueHint::FilePath, 14 | )] 15 | pub input: Option, 16 | 17 | #[arg( 18 | id = "input_pos", 19 | help = "Sets the input RPU file to use (positional)", 20 | conflicts_with = "input", 21 | required_unless_present = "input", 22 | value_hint = ValueHint::FilePath 23 | )] 24 | pub input_pos: Option, 25 | 26 | #[arg( 27 | id = "json", 28 | long, 29 | short = 'j', 30 | help = "Sets the edit JSON file to use", 31 | value_hint = ValueHint::FilePath 32 | )] 33 | pub json_file: PathBuf, 34 | 35 | #[arg( 36 | long, 37 | short = 'o', 38 | help = "Modified RPU output file location", 39 | value_hint = ValueHint::FilePath 40 | )] 41 | pub rpu_out: Option, 42 | } 43 | -------------------------------------------------------------------------------- /src/commands/export.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::{ 4 | Args, ValueEnum, ValueHint, 5 | builder::{EnumValueParser, PossibleValue, TypedValueParser}, 6 | }; 7 | use clap_lex::OsStrExt as _; 8 | 9 | #[derive(Args, Debug)] 10 | pub struct ExportArgs { 11 | #[arg( 12 | id = "input", 13 | help = "Sets the input RPU file to use", 14 | long, 15 | short = 'i', 16 | conflicts_with = "input_pos", 17 | required_unless_present = "input_pos", 18 | value_hint = ValueHint::FilePath, 19 | )] 20 | pub input: Option, 21 | 22 | #[arg( 23 | id = "input_pos", 24 | help = "Sets the input RPU file to use (positional)", 25 | conflicts_with = "input", 26 | required_unless_present = "input", 27 | value_hint = ValueHint::FilePath 28 | )] 29 | pub input_pos: Option, 30 | 31 | #[arg( 32 | id = "data", 33 | help = "List of key-value export parameters formatted as `key=output`, where `output` is an output file path.\nSupports multiple occurences prefixed by --data or delimited by ','", 34 | long, 35 | short = 'd', 36 | conflicts_with = "output", 37 | value_parser = ExportOptionParser, 38 | value_delimiter = ',' 39 | )] 40 | pub data: Vec<(ExportData, Option)>, 41 | 42 | // FIXME: export single output deprecation 43 | #[arg( 44 | id = "output", 45 | help = "Output JSON file name. Deprecated, replaced by `--data all=output`", 46 | long, 47 | short = 'o', 48 | conflicts_with = "data", 49 | hide = true, 50 | value_hint = ValueHint::FilePath 51 | )] 52 | pub output: Option, 53 | } 54 | 55 | #[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)] 56 | pub enum ExportData { 57 | /// Exports the list of RPUs as a JSON file 58 | All, 59 | /// Exports the frame indices at which `scene_refresh_flag` is set to 1 60 | Scenes, 61 | /// Exports the video's L5 metadata in the form of an `editor` config JSON 62 | Level5, 63 | } 64 | 65 | impl ExportData { 66 | pub fn default_output_file(&self) -> &'static str { 67 | match self { 68 | ExportData::All => "RPU_export.json", 69 | ExportData::Scenes => "RPU_scenes.txt", 70 | ExportData::Level5 => "RPU_L5_edit_config.json", 71 | } 72 | } 73 | } 74 | 75 | #[derive(Clone)] 76 | struct ExportOptionParser; 77 | impl TypedValueParser for ExportOptionParser { 78 | type Value = (ExportData, Option); 79 | 80 | fn parse_ref( 81 | &self, 82 | cmd: &clap::Command, 83 | arg: Option<&clap::Arg>, 84 | value: &std::ffi::OsStr, 85 | ) -> Result { 86 | let data_parser = EnumValueParser::::new(); 87 | 88 | if let Some((data_str, output_str)) = value.split_once("=") { 89 | Ok(( 90 | data_parser.parse_ref(cmd, arg, data_str)?, 91 | output_str.to_str().map(str::parse).and_then(Result::ok), 92 | )) 93 | } else { 94 | Ok((data_parser.parse_ref(cmd, arg, value)?, None)) 95 | } 96 | } 97 | 98 | fn possible_values(&self) -> Option + '_>> { 99 | Some(Box::new( 100 | ExportData::value_variants() 101 | .iter() 102 | .filter_map(|v| v.to_possible_value()), 103 | )) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/commands/extract_rpu.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args, ValueHint}; 2 | use std::path::PathBuf; 3 | 4 | #[derive(Args, Debug)] 5 | pub struct ExtractRpuArgs { 6 | #[arg( 7 | id = "input", 8 | help = "Sets the input HEVC file to use, or piped with -", 9 | long, 10 | short = 'i', 11 | conflicts_with = "input_pos", 12 | required_unless_present = "input_pos", 13 | value_hint = ValueHint::FilePath, 14 | )] 15 | pub input: Option, 16 | 17 | #[arg( 18 | id = "input_pos", 19 | help = "Sets the input HEVC file to use, or piped with - (positional)", 20 | conflicts_with = "input", 21 | required_unless_present = "input", 22 | value_hint = ValueHint::FilePath 23 | )] 24 | pub input_pos: Option, 25 | 26 | #[arg( 27 | long, 28 | short = 'o', 29 | help = "RPU output file location", 30 | value_hint = ValueHint::FilePath 31 | )] 32 | pub rpu_out: Option, 33 | 34 | #[arg( 35 | id = "limit", 36 | long, 37 | short = 'l', 38 | help = "Stop processing input after N frames" 39 | )] 40 | pub limit: Option, 41 | } 42 | -------------------------------------------------------------------------------- /src/commands/generate.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args, ValueHint}; 2 | use hdr10plus::metadata::PeakBrightnessSource; 3 | use std::path::PathBuf; 4 | 5 | use crate::dovi::generator::GeneratorProfile; 6 | 7 | #[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)] 8 | pub enum ArgHdr10PlusPeakBrightnessSource { 9 | /// The max value from the histogram measurements 10 | Histogram, 11 | /// The last percentile in the histogram, usually 99.98% brightness percentile 12 | Histogram99, 13 | /// The max value in `maxscl` 14 | MaxScl, 15 | /// The luminance calculated from the `maxscl` components 16 | /// Assumed BT.2020 primaries 17 | MaxSclLuminance, 18 | } 19 | 20 | #[derive(Args, Debug, Default)] 21 | pub struct GenerateArgs { 22 | #[arg( 23 | id = "json", 24 | long, 25 | short = 'j', 26 | help = "Sets the generator config JSON file to use", 27 | conflicts_with = "xml", 28 | required_unless_present = "xml", 29 | value_hint = ValueHint::FilePath 30 | )] 31 | pub json_file: Option, 32 | 33 | #[arg( 34 | long, 35 | short = 'o', 36 | help = "Generated RPU output file location", 37 | value_hint = ValueHint::FilePath 38 | )] 39 | pub rpu_out: Option, 40 | 41 | #[arg( 42 | id = "hdr10plus-json", 43 | long, 44 | help = "HDR10+ JSON file to generate from", 45 | conflicts_with = "madvr-file", 46 | value_hint = ValueHint::FilePath, 47 | )] 48 | pub hdr10plus_json: Option, 49 | 50 | #[arg( 51 | value_enum, 52 | long, 53 | help = "HDR10+: How to extract the peak brightness for the metadata", 54 | default_value = "histogram" 55 | )] 56 | pub hdr10plus_peak_source: Option, 57 | 58 | #[arg( 59 | short = 'x', 60 | long, 61 | help = "XML metadata file to generate from", 62 | conflicts_with_all = &["json", "hdr10plus-json", "madvr-file"], 63 | required_unless_present = "json", 64 | value_hint = ValueHint::FilePath 65 | )] 66 | pub xml: Option, 67 | 68 | #[arg(long, help = "Canvas width for L5 metadata generation")] 69 | pub canvas_width: Option, 70 | 71 | #[arg(long, help = "Canvas height for L5 metadata generation")] 72 | pub canvas_height: Option, 73 | 74 | #[arg( 75 | id = "madvr-file", 76 | long, 77 | help = "madVR measurement file to generate from", 78 | value_hint = ValueHint::FilePath 79 | )] 80 | pub madvr_file: Option, 81 | 82 | #[arg( 83 | long, 84 | help = "madVR source: use custom per-frame target nits if available" 85 | )] 86 | pub use_custom_targets: bool, 87 | 88 | #[arg( 89 | value_enum, 90 | short = 'p', 91 | long, 92 | help = "Dolby Vision profile to generate" 93 | )] 94 | pub profile: Option, 95 | 96 | #[arg(long, help = "Set scene cut flag for every frame")] 97 | pub long_play_mode: Option, 98 | } 99 | 100 | impl From for PeakBrightnessSource { 101 | fn from(e: ArgHdr10PlusPeakBrightnessSource) -> Self { 102 | match e { 103 | ArgHdr10PlusPeakBrightnessSource::Histogram => Self::Histogram, 104 | ArgHdr10PlusPeakBrightnessSource::Histogram99 => Self::Histogram99, 105 | ArgHdr10PlusPeakBrightnessSource::MaxScl => Self::MaxScl, 106 | ArgHdr10PlusPeakBrightnessSource::MaxSclLuminance => Self::MaxSclLuminance, 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/commands/info.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args, ValueHint}; 2 | use std::path::PathBuf; 3 | 4 | #[derive(Args, Debug)] 5 | pub struct InfoArgs { 6 | #[arg( 7 | id = "input", 8 | help = "Sets the input RPU file to use", 9 | long, 10 | short = 'i', 11 | conflicts_with = "input_pos", 12 | required_unless_present = "input_pos", 13 | value_hint = ValueHint::FilePath, 14 | )] 15 | pub input: Option, 16 | 17 | #[arg( 18 | id = "input_pos", 19 | help = "Sets the input RPU file to use (positional)", 20 | conflicts_with = "input", 21 | required_unless_present = "input", 22 | value_hint = ValueHint::FilePath 23 | )] 24 | pub input_pos: Option, 25 | 26 | #[arg( 27 | id = "frame", 28 | long, 29 | short = 'f', 30 | help = "Frame number to show info for" 31 | )] 32 | pub frame: Option, 33 | 34 | #[arg(id = "summary", long, short = 's', help = "Show the RPU summary")] 35 | pub summary: bool, 36 | } 37 | -------------------------------------------------------------------------------- /src/commands/inject_rpu.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args, ValueHint}; 2 | use std::path::PathBuf; 3 | 4 | #[derive(Args, Debug)] 5 | pub struct InjectRpuArgs { 6 | #[arg( 7 | id = "input", 8 | help = "Sets the input HEVC file to use", 9 | long, 10 | short = 'i', 11 | conflicts_with = "input_pos", 12 | required_unless_present = "input_pos", 13 | value_hint = ValueHint::FilePath, 14 | )] 15 | pub input: Option, 16 | 17 | #[arg( 18 | id = "input_pos", 19 | help = "Sets the input HEVC file to use (positional)", 20 | conflicts_with = "input", 21 | required_unless_present = "input", 22 | value_hint = ValueHint::FilePath 23 | )] 24 | pub input_pos: Option, 25 | 26 | #[arg(long, short = 'r', help = "Sets the input RPU file to use", value_hint = ValueHint::FilePath)] 27 | pub rpu_in: PathBuf, 28 | 29 | #[arg( 30 | long, 31 | short = 'o', 32 | help = "Output HEVC file location", 33 | value_hint = ValueHint::FilePath 34 | )] 35 | pub output: Option, 36 | 37 | #[arg(long, num_args = 0, help = "Disable adding AUD NALUs between frames")] 38 | pub no_add_aud: bool, 39 | } 40 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | use dolby_vision::rpu::ConversionMode; 4 | 5 | mod convert; 6 | mod demux; 7 | mod editor; 8 | mod export; 9 | mod extract_rpu; 10 | pub(crate) mod generate; 11 | mod info; 12 | mod inject_rpu; 13 | mod mux; 14 | mod plot; 15 | mod remove; 16 | 17 | pub use convert::ConvertArgs; 18 | pub use demux::DemuxArgs; 19 | pub use editor::EditorArgs; 20 | pub use export::{ExportArgs, ExportData}; 21 | pub use extract_rpu::ExtractRpuArgs; 22 | pub use generate::GenerateArgs; 23 | pub use info::InfoArgs; 24 | pub use inject_rpu::InjectRpuArgs; 25 | pub use mux::MuxArgs; 26 | pub use plot::PlotArgs; 27 | pub use remove::RemoveArgs; 28 | 29 | #[derive(Parser, Debug)] 30 | pub enum Commands { 31 | #[command(about = "Converts RPU within a single layer HEVC file")] 32 | Convert(ConvertArgs), 33 | 34 | #[command( 35 | about = "Demuxes single track dual layer Dolby Vision into Base layer and Enhancement layer files" 36 | )] 37 | Demux(DemuxArgs), 38 | 39 | #[command(about = "Edits a binary RPU according to a JSON config")] 40 | Editor(EditorArgs), 41 | 42 | #[command(about = "Exports a binary RPU file to JSON for simpler analysis")] 43 | Export(ExportArgs), 44 | 45 | #[command(about = "Extracts Dolby Vision RPU from an HEVC file")] 46 | ExtractRpu(ExtractRpuArgs), 47 | 48 | #[command(about = "Interleaves RPU NAL units between slices in an HEVC encoded bitstream")] 49 | InjectRpu(InjectRpuArgs), 50 | 51 | #[command(about = "Generates a binary RPU from different sources")] 52 | Generate(GenerateArgs), 53 | 54 | #[command(about = "Prints the parsed RPU data as JSON for a specific frame")] 55 | Info(InfoArgs), 56 | 57 | #[command(about = "Interleaves the enhancement layer into a base layer HEVC bitstream")] 58 | Mux(MuxArgs), 59 | 60 | #[command(about = "Plot the L1/L2/L8 metadata")] 61 | Plot(PlotArgs), 62 | 63 | #[command(about = "Removes the enhancement layer and RPU data from the video")] 64 | Remove(RemoveArgs), 65 | } 66 | 67 | #[derive(clap::ValueEnum, Debug, Copy, Clone)] 68 | pub enum ConversionModeCli { 69 | #[value(name = "0")] 70 | Lossless = 0, 71 | #[value(name = "1")] 72 | ToMel, 73 | #[value(name = "2")] 74 | To81, 75 | #[value(name = "3")] 76 | Profile5To81, 77 | #[value(name = "4")] 78 | To84, 79 | #[value(name = "5")] 80 | To81MappingPreserved, 81 | } 82 | 83 | impl From for ConversionMode { 84 | fn from(mode: ConversionModeCli) -> ConversionMode { 85 | match mode { 86 | ConversionModeCli::Lossless => ConversionMode::Lossless, 87 | ConversionModeCli::ToMel => ConversionMode::ToMel, 88 | ConversionModeCli::To81 | ConversionModeCli::Profile5To81 => ConversionMode::To81, 89 | ConversionModeCli::To84 => ConversionMode::To84, 90 | ConversionModeCli::To81MappingPreserved => ConversionMode::To81MappingPreserved, 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/commands/mux.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args, ValueHint}; 2 | use std::path::PathBuf; 3 | 4 | #[derive(Args, Debug)] 5 | pub struct MuxArgs { 6 | #[arg( 7 | id = "bl", 8 | long, 9 | short = 'b', 10 | help = "Sets the base layer HEVC file to use", 11 | value_hint = ValueHint::FilePath 12 | )] 13 | pub bl: PathBuf, 14 | 15 | #[arg( 16 | id = "el", 17 | long, 18 | short = 'e', 19 | help = "Sets the input enhancement layer HEVC file to use", 20 | value_hint = ValueHint::FilePath 21 | )] 22 | pub el: PathBuf, 23 | 24 | #[arg( 25 | long, 26 | short = 'o', 27 | help = "Output BL+EL+RPU HEVC file location", 28 | value_hint = ValueHint::FilePath 29 | )] 30 | pub output: Option, 31 | 32 | #[arg(long, num_args = 0, help = "Disable adding AUD NALUs between frames")] 33 | pub no_add_aud: bool, 34 | 35 | #[arg( 36 | long, 37 | help = "Write the EOS/EOB NALUs before the EL. Defaults to false. See --help for more info", 38 | long_help = "Write the EOS/EOB NALUs before the EL. Defaults to false.\n\ 39 | In the case of the last frame containing EOS/EOB NALUs, they are written after the EL by default.\n\n\ 40 | This behaviour is different from yusesope and MakeMKV's mux, but conforms to the HEVC spec.\n\ 41 | To match their behaviour, enable the --eos-before-el flag." 42 | )] 43 | pub eos_before_el: bool, 44 | 45 | #[arg( 46 | short = 'd', 47 | long, 48 | help = "Discard the EL video NALUs, keeping only the RPU" 49 | )] 50 | pub discard: bool, 51 | } 52 | -------------------------------------------------------------------------------- /src/commands/plot.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args, ValueHint}; 2 | use std::path::PathBuf; 3 | 4 | use crate::dovi::plotter::{PlotType, TrimParameter}; 5 | 6 | #[derive(Args, Debug)] 7 | pub struct PlotArgs { 8 | #[arg( 9 | id = "input", 10 | help = "Sets the input RPU file to use", 11 | long, 12 | short = 'i', 13 | conflicts_with = "input_pos", 14 | required_unless_present = "input_pos", 15 | value_hint = ValueHint::FilePath, 16 | )] 17 | pub input: Option, 18 | 19 | #[arg( 20 | id = "input_pos", 21 | help = "Sets the input RPU file to use (positional)", 22 | conflicts_with = "input", 23 | required_unless_present = "input", 24 | value_hint = ValueHint::FilePath 25 | )] 26 | pub input_pos: Option, 27 | 28 | #[arg( 29 | long, 30 | short = 'o', 31 | help = "Output PNG image file location", 32 | value_hint = ValueHint::FilePath 33 | )] 34 | pub output: Option, 35 | 36 | #[arg(long, short = 't', help = "Title to use at the top")] 37 | pub title: Option, 38 | 39 | #[arg(long, short = 's', help = "Set frame range start")] 40 | pub start: Option, 41 | 42 | #[arg(long, short = 'e', help = "Set frame range end (inclusive)")] 43 | pub end: Option, 44 | 45 | #[arg( 46 | long, 47 | short = 'p', 48 | help = "Sets the DV metadata level to plot", 49 | value_enum, 50 | default_value = "l1" 51 | )] 52 | pub plot_type: PlotType, 53 | 54 | #[arg( 55 | long = "target-nits", 56 | help = "Target brightness in nits for L2/L8 plots", 57 | default_value = "100", 58 | value_parser = ["100", "300", "600", "1000", "2000", "4000"] 59 | )] 60 | pub target_nits_str: String, 61 | 62 | #[arg( 63 | long, 64 | help = "Trim parameters to include in L2/L8 trims plots. By default all are included.\nSupports multiple occurrences prefixed by --trims or delimited by ','", 65 | value_enum, 66 | value_delimiter = ',' 67 | )] 68 | pub trims: Option>, 69 | } 70 | -------------------------------------------------------------------------------- /src/commands/remove.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args, ValueHint}; 2 | use std::path::PathBuf; 3 | 4 | #[derive(Args, Debug)] 5 | pub struct RemoveArgs { 6 | #[arg( 7 | id = "input", 8 | help = "Sets the input HEVC file to use, or piped with -", 9 | long, 10 | short = 'i', 11 | conflicts_with = "input_pos", 12 | required_unless_present = "input_pos", 13 | value_hint = ValueHint::FilePath, 14 | )] 15 | pub input: Option, 16 | 17 | #[arg( 18 | id = "input_pos", 19 | help = "Sets the input HEVC file to use, or piped with - (positional)", 20 | conflicts_with = "input", 21 | required_unless_present = "input", 22 | value_hint = ValueHint::FilePath 23 | )] 24 | pub input_pos: Option, 25 | 26 | #[arg( 27 | long, 28 | short = 'o', 29 | help = "Base layer output file location", 30 | value_hint = ValueHint::FilePath 31 | )] 32 | pub output: Option, 33 | } 34 | -------------------------------------------------------------------------------- /src/dovi/converter.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, bail}; 2 | use indicatif::ProgressBar; 3 | use std::path::PathBuf; 4 | 5 | use crate::commands::ConvertArgs; 6 | 7 | use super::{CliOptions, IoFormat, general_read_write, input_from_either}; 8 | 9 | use general_read_write::{DoviProcessor, DoviWriter}; 10 | 11 | pub struct Converter { 12 | format: IoFormat, 13 | input: PathBuf, 14 | output: PathBuf, 15 | } 16 | 17 | impl Converter { 18 | pub fn from_args(args: ConvertArgs, options: &mut CliOptions) -> Result { 19 | let ConvertArgs { 20 | input, 21 | input_pos, 22 | output, 23 | discard, 24 | } = args; 25 | 26 | options.discard_el = discard; 27 | 28 | let input = input_from_either("convert", input, input_pos)?; 29 | let format = hevc_parser::io::format_from_path(&input)?; 30 | 31 | let output = match output { 32 | Some(path) => path, 33 | None => match options.discard_el { 34 | true => PathBuf::from("BL_RPU.hevc"), 35 | false => PathBuf::from("BL_EL_RPU.hevc"), 36 | }, 37 | }; 38 | 39 | Ok(Self { 40 | format, 41 | input, 42 | output, 43 | }) 44 | } 45 | 46 | pub fn convert(args: ConvertArgs, mut options: CliOptions) -> Result<()> { 47 | let converter = Converter::from_args(args, &mut options)?; 48 | converter.process_input(options) 49 | } 50 | 51 | fn process_input(&self, options: CliOptions) -> Result<()> { 52 | let pb = super::initialize_progress_bar(&self.format, &self.input)?; 53 | 54 | match self.format { 55 | IoFormat::Matroska => bail!("Converter: Matroska input is unsupported"), 56 | _ => self.convert_raw_hevc(pb, options), 57 | } 58 | } 59 | 60 | fn convert_raw_hevc(&self, pb: ProgressBar, options: CliOptions) -> Result<()> { 61 | let dovi_writer = DoviWriter::new(None, None, None, Some(&self.output)); 62 | let mut dovi_processor = DoviProcessor::new( 63 | options, 64 | self.input.clone(), 65 | dovi_writer, 66 | pb, 67 | Default::default(), 68 | ); 69 | 70 | dovi_processor.read_write_from_io(&self.format) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/dovi/demuxer.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, bail}; 2 | use indicatif::ProgressBar; 3 | use std::path::PathBuf; 4 | 5 | use crate::commands::DemuxArgs; 6 | 7 | use super::{CliOptions, IoFormat, general_read_write, input_from_either}; 8 | 9 | use general_read_write::{DoviProcessor, DoviWriter}; 10 | 11 | pub struct Demuxer { 12 | format: IoFormat, 13 | input: PathBuf, 14 | bl_out: PathBuf, 15 | el_out: PathBuf, 16 | el_only: bool, 17 | } 18 | 19 | impl Demuxer { 20 | pub fn from_args(args: DemuxArgs) -> Result { 21 | let DemuxArgs { 22 | input, 23 | input_pos, 24 | bl_out, 25 | el_out, 26 | el_only, 27 | } = args; 28 | 29 | let input = input_from_either("demux", input, input_pos)?; 30 | let format = hevc_parser::io::format_from_path(&input)?; 31 | 32 | let bl_out = match bl_out { 33 | Some(path) => path, 34 | None => PathBuf::from("BL.hevc"), 35 | }; 36 | 37 | let el_out = match el_out { 38 | Some(path) => path, 39 | None => PathBuf::from("EL.hevc"), 40 | }; 41 | 42 | Ok(Self { 43 | format, 44 | input, 45 | bl_out, 46 | el_out, 47 | el_only, 48 | }) 49 | } 50 | 51 | pub fn demux(args: DemuxArgs, options: CliOptions) -> Result<()> { 52 | let demuxer = Demuxer::from_args(args)?; 53 | demuxer.process_input(options) 54 | } 55 | 56 | fn process_input(&self, options: CliOptions) -> Result<()> { 57 | let pb = super::initialize_progress_bar(&self.format, &self.input)?; 58 | 59 | match self.format { 60 | IoFormat::Matroska => bail!("Demuxer: Matroska input is unsupported"), 61 | _ => self.demux_raw_hevc(pb, options), 62 | } 63 | } 64 | 65 | fn demux_raw_hevc(&self, pb: ProgressBar, options: CliOptions) -> Result<()> { 66 | let bl_out = if self.el_only { 67 | None 68 | } else { 69 | Some(self.bl_out.as_path()) 70 | }; 71 | 72 | let dovi_writer = DoviWriter::new(bl_out, Some(self.el_out.as_path()), None, None); 73 | let mut dovi_processor = DoviProcessor::new( 74 | options, 75 | self.input.clone(), 76 | dovi_writer, 77 | pb, 78 | Default::default(), 79 | ); 80 | 81 | dovi_processor.read_write_from_io(&self.format) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/dovi/exporter.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::fs::File; 3 | use std::io::{BufWriter, Write, stdout}; 4 | use std::ops::Range; 5 | use std::path::PathBuf; 6 | 7 | use anyhow::Result; 8 | use dolby_vision::rpu::extension_metadata::blocks::{ExtMetadataBlock, ExtMetadataBlockLevel5}; 9 | use itertools::Itertools; 10 | use serde::Serializer; 11 | use serde::ser::SerializeSeq; 12 | 13 | use dolby_vision::rpu::utils::parse_rpu_file; 14 | use serde_json::json; 15 | 16 | use crate::commands::{ExportArgs, ExportData}; 17 | use crate::dovi::input_from_either; 18 | 19 | use super::DoviRpu; 20 | 21 | pub struct Exporter { 22 | input: PathBuf, 23 | data: Vec<(ExportData, Option)>, 24 | } 25 | 26 | impl Exporter { 27 | pub fn export(args: ExportArgs) -> Result<()> { 28 | let ExportArgs { 29 | input, 30 | input_pos, 31 | data, 32 | output, 33 | } = args; 34 | 35 | let input = input_from_either("editor", input, input_pos)?; 36 | let mut exporter = Exporter { input, data }; 37 | 38 | if exporter.data.is_empty() { 39 | exporter.data.push((ExportData::All, output)); 40 | } 41 | 42 | exporter.data.dedup_by_key(|(k, _)| *k); 43 | 44 | println!("Parsing RPU file..."); 45 | stdout().flush().ok(); 46 | 47 | let rpus = parse_rpu_file(&exporter.input)?; 48 | exporter.execute(&rpus)?; 49 | 50 | println!("Done."); 51 | 52 | Ok(()) 53 | } 54 | 55 | fn execute(&self, rpus: &[DoviRpu]) -> Result<()> { 56 | for (data, maybe_output) in &self.data { 57 | let out_path = if let Some(out_path) = maybe_output { 58 | Cow::Borrowed(out_path) 59 | } else { 60 | Cow::Owned(PathBuf::from(data.default_output_file())) 61 | }; 62 | 63 | let writer_buf_len = if matches!(data, ExportData::All) { 64 | 100_000 65 | } else { 66 | 1000 67 | }; 68 | let mut writer = BufWriter::with_capacity( 69 | writer_buf_len, 70 | File::create(out_path.as_path()).expect("Can't create file"), 71 | ); 72 | 73 | match data { 74 | ExportData::All => { 75 | println!("Exporting serialized RPU list..."); 76 | 77 | let mut ser = serde_json::Serializer::new(&mut writer); 78 | let mut seq = ser.serialize_seq(Some(rpus.len()))?; 79 | 80 | for rpu in rpus { 81 | seq.serialize_element(&rpu)?; 82 | } 83 | seq.end()?; 84 | } 85 | ExportData::Scenes => { 86 | println!("Exporting scenes list..."); 87 | 88 | let scene_refresh_indices = rpus 89 | .iter() 90 | .enumerate() 91 | .filter(|(_, rpu)| { 92 | rpu.vdr_dm_data 93 | .as_ref() 94 | .is_some_and(|vdr| vdr.scene_refresh_flag == 1) 95 | }) 96 | .map(|e| e.0); 97 | for i in scene_refresh_indices { 98 | writeln!(&mut writer, "{i}")?; 99 | } 100 | } 101 | ExportData::Level5 => { 102 | self.export_level5_config(rpus, &mut writer)?; 103 | } 104 | } 105 | 106 | writer.flush()?; 107 | } 108 | 109 | Ok(()) 110 | } 111 | 112 | fn export_level5_config(&self, rpus: &[DoviRpu], writer: &mut W) -> Result<()> { 113 | println!("Exporting L5 metadata config..."); 114 | 115 | let default_l5 = ExtMetadataBlockLevel5::default(); 116 | 117 | let l5_groups = rpus.iter().enumerate().chunk_by(|(_, rpu)| { 118 | rpu.vdr_dm_data 119 | .as_ref() 120 | .and_then(|vdr| { 121 | vdr.get_block(5).and_then(|b| match b { 122 | ExtMetadataBlock::Level5(b) => Some(b), 123 | _ => None, 124 | }) 125 | }) 126 | .unwrap_or(&default_l5) 127 | }); 128 | let l5_indices = l5_groups 129 | .into_iter() 130 | .map(|(k, group)| (k, group.take(1).map(|(i, _)| i).next().unwrap())); 131 | 132 | let mut l5_presets = 133 | Vec::<&ExtMetadataBlockLevel5>::with_capacity(l5_indices.size_hint().0); 134 | let mut l5_edits = Vec::<(Range, usize)>::new(); 135 | 136 | for (k, start_index) in l5_indices { 137 | if !l5_presets.contains(&k) { 138 | l5_presets.push(k); 139 | } 140 | 141 | if let Some(last_edit) = l5_edits.last_mut() { 142 | last_edit.0.end = start_index - 1; 143 | } 144 | 145 | let preset_idx = l5_presets.iter().position(|l5| *l5 == k).unwrap(); 146 | l5_edits.push((start_index..start_index, preset_idx)); 147 | } 148 | 149 | // Set last edit end index 150 | if let Some(last_edit) = l5_edits.last_mut() { 151 | last_edit.0.end = rpus.len() - 1; 152 | } 153 | 154 | let l5_presets = l5_presets 155 | .iter() 156 | .enumerate() 157 | .map(|(id, l5)| { 158 | json!({ 159 | "id": id, 160 | "left": l5.active_area_left_offset, 161 | "right": l5.active_area_right_offset, 162 | "top": l5.active_area_top_offset, 163 | "bottom": l5.active_area_bottom_offset 164 | }) 165 | }) 166 | .collect::>(); 167 | let l5_edits = l5_edits.iter().map(|(edit_range, id)| { 168 | ( 169 | format!("{}-{}", edit_range.start, edit_range.end), 170 | json!(id), 171 | ) 172 | }); 173 | let l5_edits = serde_json::Value::Object(l5_edits.collect()); 174 | 175 | let edit_config = json!({ 176 | "crop": true, 177 | "presets": l5_presets, 178 | "edits": l5_edits, 179 | }); 180 | serde_json::to_writer_pretty(writer, &edit_config)?; 181 | 182 | Ok(()) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/dovi/hdr10plus_utils.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use bitvec_helpers::bitstream_io_reader::BsIoSliceReader; 4 | use hevc_parser::hevc::{NAL_SEI_PREFIX, NALUnit, SeiMessage, USER_DATA_REGISTERED_ITU_T_35}; 5 | use hevc_parser::utils::{ 6 | add_start_code_emulation_prevention_3_byte, clear_start_code_emulation_prevention_3_byte, 7 | }; 8 | 9 | /// The bytes must have start_code_emulation_prevention_3_byte removed 10 | pub fn st2094_40_sei_msg(sei_payload: &[u8]) -> Result> { 11 | let res = if sei_payload.len() >= 4 { 12 | let sei = SeiMessage::parse_sei_rbsp(sei_payload)?; 13 | 14 | sei.into_iter().find(|msg| { 15 | if msg.payload_type == USER_DATA_REGISTERED_ITU_T_35 && msg.payload_size >= 7 { 16 | let start = msg.payload_offset; 17 | let end = start + msg.payload_size; 18 | 19 | let bytes = &sei_payload[start..end]; 20 | let mut reader = BsIoSliceReader::from_slice(bytes); 21 | 22 | let itu_t_t35_country_code = reader.get_n::(8).unwrap(); 23 | let itu_t_t35_terminal_provider_code = reader.get_n::(16).unwrap(); 24 | let itu_t_t35_terminal_provider_oriented_code = reader.get_n::(16).unwrap(); 25 | 26 | if itu_t_t35_country_code == 0xB5 27 | && itu_t_t35_terminal_provider_code == 0x003C 28 | && itu_t_t35_terminal_provider_oriented_code == 0x0001 29 | { 30 | let application_identifier = reader.get_n::(8).unwrap(); 31 | let application_version = reader.get_n::(8).unwrap(); 32 | 33 | if application_identifier == 4 && application_version == 1 { 34 | return true; 35 | } 36 | } 37 | } 38 | 39 | false 40 | }) 41 | } else { 42 | None 43 | }; 44 | 45 | Ok(res) 46 | } 47 | 48 | // Returns Some when the SEI needs to be written 49 | // Otherwise, the NALU only contains one SEI message, and can be dropped 50 | pub fn prefix_sei_removed_hdr10plus_nalu( 51 | chunk: &[u8], 52 | nal: &NALUnit, 53 | ) -> Result<(bool, Option>)> { 54 | let (st2094_40_msg, payload) = if nal.nal_type == NAL_SEI_PREFIX { 55 | let sei_payload = clear_start_code_emulation_prevention_3_byte(&chunk[nal.start..nal.end]); 56 | let msg = st2094_40_sei_msg(&sei_payload)?; 57 | 58 | (msg, Some(sei_payload)) 59 | } else { 60 | (None, None) 61 | }; 62 | 63 | let has_st2094_40 = st2094_40_msg.is_some(); 64 | 65 | if let (Some(msg), Some(mut payload)) = (st2094_40_msg, payload) { 66 | let messages = SeiMessage::parse_sei_rbsp(&payload)?; 67 | 68 | // Only remove ST2094-40 message if there are others 69 | if messages.len() > 1 { 70 | let start = msg.msg_offset; 71 | let end = msg.payload_offset + msg.payload_size; 72 | 73 | payload.drain(start..end); 74 | add_start_code_emulation_prevention_3_byte(&mut payload); 75 | 76 | return Ok((true, Some(payload))); 77 | } 78 | } 79 | 80 | Ok((has_st2094_40, None)) 81 | } 82 | -------------------------------------------------------------------------------- /src/dovi/mod.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | use std::path::PathBuf; 3 | use std::{fs::File, io::BufWriter, path::Path}; 4 | 5 | use anyhow::{Result, bail}; 6 | use indicatif::{ProgressBar, ProgressStyle}; 7 | 8 | use dolby_vision::rpu::dovi_rpu::DoviRpu; 9 | 10 | use hevc_parser::hevc::{NAL_UNSPEC62, NALUnit}; 11 | use hevc_parser::io::{IoFormat, StartCodePreset}; 12 | 13 | use self::editor::EditConfig; 14 | use super::commands::ConversionModeCli; 15 | 16 | pub mod converter; 17 | pub mod demuxer; 18 | pub mod editor; 19 | pub mod exporter; 20 | pub mod generator; 21 | pub mod muxer; 22 | pub mod plotter; 23 | pub mod remover; 24 | pub mod rpu_extractor; 25 | pub mod rpu_info; 26 | pub mod rpu_injector; 27 | 28 | mod general_read_write; 29 | mod hdr10plus_utils; 30 | 31 | #[derive(Debug, Clone)] 32 | pub struct CliOptions { 33 | pub mode: Option, 34 | pub crop: bool, 35 | pub discard_el: bool, 36 | pub drop_hdr10plus: bool, 37 | pub edit_config: Option, 38 | pub start_code: StartCodePreset, 39 | } 40 | 41 | #[derive(clap::ValueEnum, Debug, Clone, Copy, PartialEq, Eq)] 42 | pub enum WriteStartCodePreset { 43 | Four, 44 | AnnexB, 45 | } 46 | 47 | pub fn initialize_progress_bar>(format: &IoFormat, input: P) -> Result { 48 | let pb: ProgressBar; 49 | let bytes_count; 50 | 51 | if let IoFormat::RawStdin = format { 52 | pb = ProgressBar::hidden(); 53 | } else { 54 | let file = File::open(input).expect("No file found"); 55 | 56 | //Info for indicatif ProgressBar 57 | let file_meta = file.metadata()?; 58 | bytes_count = file_meta.len() / 100_000_000; 59 | 60 | pb = ProgressBar::new(bytes_count); 61 | pb.set_style( 62 | ProgressStyle::default_bar() 63 | .template("[{elapsed_precise}] {bar:60.cyan} {percent}%")?, 64 | ); 65 | } 66 | 67 | Ok(pb) 68 | } 69 | 70 | pub fn write_rpu_file>(output_path: P, data: Vec>) -> Result<()> { 71 | println!("Writing RPU file..."); 72 | let mut writer = BufWriter::with_capacity( 73 | 100_000, 74 | File::create(output_path).expect("Can't create file"), 75 | ); 76 | 77 | for encoded_rpu in data { 78 | // Remove 0x7C01 79 | NALUnit::write_with_preset( 80 | &mut writer, 81 | &encoded_rpu[2..], 82 | StartCodePreset::Four, 83 | NAL_UNSPEC62, 84 | true, 85 | )?; 86 | } 87 | 88 | writer.flush()?; 89 | 90 | Ok(()) 91 | } 92 | 93 | pub fn convert_encoded_from_opts(opts: &CliOptions, data: &[u8]) -> Result> { 94 | let mut dovi_rpu = DoviRpu::parse_unspec62_nalu(data)?; 95 | 96 | // Config overrides manual arguments 97 | if let Some(edit_config) = &opts.edit_config { 98 | edit_config.execute_single_rpu(&mut dovi_rpu)?; 99 | } else { 100 | if let Some(mode) = opts.mode { 101 | dovi_rpu.convert_with_mode(mode)?; 102 | } 103 | 104 | if opts.crop { 105 | dovi_rpu.crop()?; 106 | } 107 | } 108 | 109 | dovi_rpu.write_hevc_unspec62_nalu() 110 | } 111 | 112 | pub fn input_from_either(cmd: &str, in1: Option, in2: Option) -> Result { 113 | match in1 { 114 | Some(in1) => Ok(in1), 115 | None => match in2 { 116 | Some(in2) => Ok(in2), 117 | None => bail!("No input file provided. See `dovi_tool {} --help`", cmd), 118 | }, 119 | } 120 | } 121 | 122 | impl From for StartCodePreset { 123 | fn from(p: WriteStartCodePreset) -> Self { 124 | match p { 125 | WriteStartCodePreset::Four => StartCodePreset::Four, 126 | WriteStartCodePreset::AnnexB => StartCodePreset::AnnexB, 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/dovi/remover.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, bail}; 2 | use indicatif::ProgressBar; 3 | use std::path::PathBuf; 4 | 5 | use crate::commands::RemoveArgs; 6 | 7 | use super::{CliOptions, IoFormat, general_read_write, input_from_either}; 8 | 9 | use general_read_write::{DoviProcessor, DoviWriter}; 10 | 11 | pub struct Remover { 12 | format: IoFormat, 13 | input: PathBuf, 14 | output: PathBuf, 15 | } 16 | 17 | impl Remover { 18 | pub fn from_args(args: RemoveArgs) -> Result { 19 | let RemoveArgs { 20 | input, 21 | input_pos, 22 | output, 23 | } = args; 24 | 25 | let input = input_from_either("remove", input, input_pos)?; 26 | let format = hevc_parser::io::format_from_path(&input)?; 27 | 28 | let output = output.unwrap_or(PathBuf::from("BL.hevc")); 29 | 30 | Ok(Self { 31 | format, 32 | input, 33 | output, 34 | }) 35 | } 36 | 37 | pub fn remove(args: RemoveArgs, options: CliOptions) -> Result<()> { 38 | let remover = Remover::from_args(args)?; 39 | remover.process_input(options) 40 | } 41 | 42 | fn process_input(&self, options: CliOptions) -> Result<()> { 43 | let pb = super::initialize_progress_bar(&self.format, &self.input)?; 44 | 45 | match self.format { 46 | IoFormat::Matroska => bail!("Remover: Matroska input is unsupported"), 47 | _ => self.remove_from_raw_hevc(pb, options), 48 | } 49 | } 50 | 51 | fn remove_from_raw_hevc(&self, pb: ProgressBar, options: CliOptions) -> Result<()> { 52 | let bl_out = Some(self.output.as_path()); 53 | 54 | let dovi_writer = DoviWriter::new(bl_out, None, None, None); 55 | let mut dovi_processor = DoviProcessor::new( 56 | options, 57 | self.input.clone(), 58 | dovi_writer, 59 | pb, 60 | Default::default(), 61 | ); 62 | 63 | dovi_processor.read_write_from_io(&self.format) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/dovi/rpu_extractor.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use indicatif::ProgressBar; 3 | use std::path::PathBuf; 4 | 5 | use crate::commands::ExtractRpuArgs; 6 | 7 | use super::{ 8 | CliOptions, IoFormat, 9 | general_read_write::{self, DoviProcessorOptions}, 10 | input_from_either, 11 | }; 12 | use general_read_write::{DoviProcessor, DoviWriter}; 13 | 14 | pub struct RpuExtractor { 15 | format: IoFormat, 16 | input: PathBuf, 17 | rpu_out: PathBuf, 18 | limit: Option, 19 | } 20 | 21 | impl RpuExtractor { 22 | pub fn from_args(args: ExtractRpuArgs) -> Result { 23 | let ExtractRpuArgs { 24 | input, 25 | input_pos, 26 | rpu_out, 27 | limit, 28 | } = args; 29 | 30 | let input = input_from_either("extract-rpu", input, input_pos)?; 31 | let format = hevc_parser::io::format_from_path(&input)?; 32 | 33 | let rpu_out = match rpu_out { 34 | Some(path) => path, 35 | None => PathBuf::from("RPU.bin"), 36 | }; 37 | 38 | Ok(Self { 39 | format, 40 | input, 41 | rpu_out, 42 | limit, 43 | }) 44 | } 45 | 46 | pub fn extract_rpu(args: ExtractRpuArgs, options: CliOptions) -> Result<()> { 47 | let rpu_extractor = RpuExtractor::from_args(args)?; 48 | rpu_extractor.process_input(options) 49 | } 50 | 51 | fn process_input(&self, options: CliOptions) -> Result<()> { 52 | let pb = super::initialize_progress_bar(&self.format, &self.input)?; 53 | self.extract_rpu_from_el(pb, options) 54 | } 55 | 56 | fn extract_rpu_from_el(&self, pb: ProgressBar, options: CliOptions) -> Result<()> { 57 | let dovi_writer = DoviWriter::new(None, None, Some(&self.rpu_out), None); 58 | let mut dovi_processor = DoviProcessor::new( 59 | options, 60 | self.input.clone(), 61 | dovi_writer, 62 | pb, 63 | DoviProcessorOptions { limit: self.limit }, 64 | ); 65 | 66 | dovi_processor.read_write_from_io(&self.format) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use anyhow::Result; 4 | use clap::{Parser, ValueHint}; 5 | 6 | #[cfg(test)] 7 | mod tests; 8 | 9 | mod commands; 10 | use commands::{Commands, ConversionModeCli}; 11 | 12 | mod dovi; 13 | use dovi::{ 14 | CliOptions, WriteStartCodePreset, 15 | converter::Converter, 16 | demuxer::Demuxer, 17 | editor::{EditConfig, Editor}, 18 | exporter::Exporter, 19 | generator::Generator, 20 | muxer::Muxer, 21 | plotter::Plotter, 22 | remover::Remover, 23 | rpu_extractor::RpuExtractor, 24 | rpu_info::RpuInfo, 25 | rpu_injector::RpuInjector, 26 | }; 27 | 28 | #[derive(Parser, Debug)] 29 | #[command( 30 | name = env!("CARGO_PKG_NAME"), 31 | about = "CLI tool combining multiple utilities for working with Dolby Vision", 32 | author = "quietvoid", 33 | version = option_env!("VERGEN_GIT_DESCRIBE").unwrap_or(env!("CARGO_PKG_VERSION")) 34 | )] 35 | struct Opt { 36 | #[arg( 37 | id = "mode", 38 | short = 'm', 39 | long, 40 | help = "Sets the mode for RPU processing. See --help for more info", 41 | long_help = "Sets the mode for RPU processing.\n \ 42 | Mode 0: Parses the RPU, rewrites it untouched\n \ 43 | Mode 1: Converts the RPU to be MEL compatible\n \ 44 | Mode 2: Converts the RPU to be profile 8.1 compatible. Removes mapping\n \ 45 | Mode 3: Converts profile 5 to 8.1\n \ 46 | Mode 4: Converts to profile 8.4\n \ 47 | Mode 5: Converts to profile 8.1, preserving luma/chroma mapping", 48 | value_enum 49 | )] 50 | mode: Option, 51 | 52 | #[arg( 53 | long, 54 | short = 'c', 55 | help = "Set active area offsets to 0 (meaning no letterbox bars)" 56 | )] 57 | crop: bool, 58 | 59 | #[arg(long, help = "Ignore HDR10+ metadata when writing the output HEVC.")] 60 | drop_hdr10plus: bool, 61 | 62 | #[arg( 63 | long, 64 | help = "Sets the edit JSON config file to use", 65 | value_hint = ValueHint::FilePath 66 | )] 67 | edit_config: Option, 68 | 69 | #[arg( 70 | value_enum, 71 | long, 72 | help = "Start code to use when writing HEVC", 73 | default_value = "four" 74 | )] 75 | start_code: WriteStartCodePreset, 76 | 77 | #[command(subcommand)] 78 | cmd: Commands, 79 | } 80 | 81 | fn main() -> Result<()> { 82 | let opt = Opt::parse(); 83 | 84 | let edit_config = opt 85 | .edit_config 86 | .as_ref() 87 | .map(EditConfig::from_path) 88 | .and_then(Result::ok); 89 | 90 | let mut cli_options = CliOptions { 91 | mode: opt.mode, 92 | crop: opt.crop, 93 | discard_el: false, 94 | drop_hdr10plus: opt.drop_hdr10plus, 95 | edit_config, 96 | start_code: opt.start_code.into(), 97 | }; 98 | 99 | // Set mode 0 by default if cropping, otherwise it has no effect 100 | if cli_options.mode.is_none() && cli_options.crop { 101 | cli_options.mode = Some(ConversionModeCli::Lossless); 102 | } 103 | 104 | match opt.cmd { 105 | Commands::Demux(args) => Demuxer::demux(args, cli_options), 106 | Commands::Editor(args) => Editor::edit(args), 107 | Commands::Convert(args) => Converter::convert(args, cli_options), 108 | Commands::ExtractRpu(args) => RpuExtractor::extract_rpu(args, cli_options), 109 | Commands::InjectRpu(args) => RpuInjector::inject_rpu(args, cli_options), 110 | Commands::Info(args) => RpuInfo::info(args), 111 | Commands::Generate(args) => Generator::generate(args), 112 | Commands::Export(args) => Exporter::export(args), 113 | Commands::Mux(args) => Muxer::mux_el(args, cli_options), 114 | Commands::Plot(args) => Plotter::plot(args), 115 | Commands::Remove(args) => Remover::remove(args, cli_options), 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/tests/av1_rpu.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::{io::Read, path::PathBuf}; 3 | 4 | use anyhow::Result; 5 | 6 | use dolby_vision::rpu::dovi_rpu::DoviRpu; 7 | 8 | pub fn _parse_file(input: PathBuf, hevc: bool) -> Result<(Vec, DoviRpu)> { 9 | let mut f = File::open(input)?; 10 | let metadata = f.metadata()?; 11 | 12 | let mut original_data = vec![0; metadata.len() as usize]; 13 | f.read_exact(&mut original_data)?; 14 | 15 | let mut cloned_data = original_data.clone(); 16 | let dovi_rpu = if hevc { 17 | DoviRpu::parse_unspec62_nalu(&original_data)? 18 | } else { 19 | DoviRpu::parse_itu_t35_dovi_metadata_obu(cloned_data.as_mut_slice())? 20 | }; 21 | 22 | Ok((original_data, dovi_rpu)) 23 | } 24 | 25 | #[test] 26 | fn profile5_dolby_sample() -> Result<()> { 27 | let mut f = File::open("./assets/av1-rpu/p5-01-ref.bin")?; 28 | let metadata = f.metadata()?; 29 | 30 | let mut ref_data = vec![0; metadata.len() as usize]; 31 | f.read_exact(&mut ref_data)?; 32 | 33 | let (orig_payload, dovi_rpu) = _parse_file(PathBuf::from("./assets/av1-rpu/p5-01.bin"), false)?; 34 | 35 | let rewritten_payload = dovi_rpu.write_av1_rpu_metadata_obu_t35_payload()?; 36 | assert_eq!(rewritten_payload, orig_payload); 37 | 38 | assert_eq!(dovi_rpu.dovi_profile, 5); 39 | let parsed_data = dovi_rpu.write_hevc_unspec62_nalu()?; 40 | 41 | assert_eq!(&ref_data[4..], &parsed_data[2..]); 42 | 43 | Ok(()) 44 | } 45 | 46 | #[test] 47 | fn profile_84_dolby_sample() -> Result<()> { 48 | let mut f = File::open("./assets/av1-rpu/p84-01-ref.bin")?; 49 | let metadata = f.metadata()?; 50 | 51 | let mut ref_data = vec![0; metadata.len() as usize]; 52 | f.read_exact(&mut ref_data)?; 53 | 54 | let (orig_payload, dovi_rpu) = 55 | _parse_file(PathBuf::from("./assets/av1-rpu/p84-01.bin"), false)?; 56 | 57 | let rewritten_payload = dovi_rpu.write_av1_rpu_metadata_obu_t35_payload()?; 58 | assert_eq!(rewritten_payload, orig_payload); 59 | 60 | assert_eq!(dovi_rpu.dovi_profile, 8); 61 | let parsed_data = dovi_rpu.write_hevc_unspec62_nalu()?; 62 | 63 | assert_eq!(&ref_data[4..], &parsed_data[2..]); 64 | 65 | Ok(()) 66 | } 67 | 68 | #[test] 69 | fn av1_fel_orig() -> Result<()> { 70 | let mut f = File::open("./assets/tests/fel_orig.bin")?; 71 | let metadata = f.metadata()?; 72 | 73 | let mut ref_data = vec![0; metadata.len() as usize]; 74 | f.read_exact(&mut ref_data)?; 75 | 76 | let (orig_payload, dovi_rpu) = 77 | _parse_file(PathBuf::from("./assets/av1-rpu/fel_orig.bin"), false)?; 78 | 79 | let rewritten_payload = dovi_rpu.write_av1_rpu_metadata_obu_t35_payload()?; 80 | assert_eq!(rewritten_payload, orig_payload); 81 | 82 | assert_eq!(dovi_rpu.dovi_profile, 7); 83 | let parsed_data = dovi_rpu.write_hevc_unspec62_nalu()?; 84 | 85 | assert_eq!(&ref_data[4..], &parsed_data[2..]); 86 | 87 | Ok(()) 88 | } 89 | 90 | #[test] 91 | fn trailing_bytes_rpu() -> Result<()> { 92 | let (original_data, dovi_rpu) = 93 | _parse_file(PathBuf::from("./assets/tests/trailing_bytes_rpu.bin"), true)?; 94 | let original_without_trailing = &original_data[..original_data.len() - 2]; 95 | assert_eq!(original_without_trailing.last().copied().unwrap(), 0x80); 96 | 97 | let av1_payload = dovi_rpu.write_av1_rpu_metadata_obu_t35_payload()?; 98 | let parsed_rpu = DoviRpu::parse_itu_t35_dovi_metadata_obu(&av1_payload)?; 99 | 100 | let rewritten_data = parsed_rpu.write_hevc_unspec62_nalu()?; 101 | assert_eq!(&original_without_trailing[4..], &rewritten_data[2..]); 102 | 103 | Ok(()) 104 | } 105 | -------------------------------------------------------------------------------- /src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod av1_rpu; 2 | mod rpu; 3 | -------------------------------------------------------------------------------- /tests/hevc/extract_rpu.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::Result; 4 | use assert_cmd::Command; 5 | use assert_fs::prelude::*; 6 | use predicates::prelude::*; 7 | 8 | use dolby_vision::rpu::extension_metadata::blocks::ExtMetadataBlock; 9 | 10 | const SUBCOMMAND: &str = "extract-rpu"; 11 | 12 | #[test] 13 | fn help() -> Result<()> { 14 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; 15 | let assert = cmd.arg(SUBCOMMAND).arg("--help").assert(); 16 | 17 | assert 18 | .success() 19 | .stderr(predicate::str::is_empty()) 20 | .stdout(predicate::str::contains( 21 | "dovi_tool extract-rpu [OPTIONS] [input_pos]", 22 | )); 23 | Ok(()) 24 | } 25 | 26 | #[test] 27 | fn extract_rpu() -> Result<()> { 28 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; 29 | let temp = assert_fs::TempDir::new().unwrap(); 30 | 31 | let input_file = Path::new("assets/hevc_tests/regular.hevc"); 32 | let expected_rpu = Path::new("assets/hevc_tests/regular_rpu.bin"); 33 | 34 | let output_rpu = temp.child("RPU.bin"); 35 | 36 | let assert = cmd 37 | .arg(SUBCOMMAND) 38 | .arg(input_file) 39 | .arg("--rpu-out") 40 | .arg(output_rpu.as_ref()) 41 | .assert(); 42 | 43 | assert.success().stderr(predicate::str::is_empty()); 44 | 45 | output_rpu 46 | .assert(predicate::path::is_file()) 47 | .assert(predicate::path::eq_file(expected_rpu)); 48 | 49 | Ok(()) 50 | } 51 | 52 | #[test] 53 | fn mode_mel() -> Result<()> { 54 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; 55 | let temp = assert_fs::TempDir::new().unwrap(); 56 | 57 | let input_file = Path::new("assets/hevc_tests/regular.hevc"); 58 | let expected_rpu = Path::new("assets/hevc_tests/regular_rpu_mel.bin"); 59 | 60 | let output_rpu = temp.child("RPU.bin"); 61 | 62 | let assert = cmd 63 | .arg("--mode") 64 | .arg("1") 65 | .arg(SUBCOMMAND) 66 | .arg(input_file) 67 | .arg("--rpu-out") 68 | .arg(output_rpu.as_ref()) 69 | .assert(); 70 | 71 | assert.success().stderr(predicate::str::is_empty()); 72 | 73 | output_rpu 74 | .assert(predicate::path::is_file()) 75 | .assert(predicate::path::eq_file(expected_rpu)); 76 | 77 | Ok(()) 78 | } 79 | 80 | /// Edit config with specific active area 81 | #[test] 82 | fn edit_config() -> Result<()> { 83 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; 84 | let temp = assert_fs::TempDir::new().unwrap(); 85 | 86 | let input_file = Path::new("assets/hevc_tests/regular.hevc"); 87 | let edit_config = Path::new("assets/editor_examples/active_area_all.json"); 88 | 89 | let output_rpu = temp.child("RPU.bin"); 90 | 91 | let assert = cmd 92 | .arg("--edit-config") 93 | .arg(edit_config) 94 | .arg(SUBCOMMAND) 95 | .arg(input_file) 96 | .arg("--rpu-out") 97 | .arg(output_rpu.as_ref()) 98 | .assert(); 99 | 100 | assert.success().stderr(predicate::str::is_empty()); 101 | 102 | output_rpu.assert(predicate::path::is_file()); 103 | 104 | let rpus = dolby_vision::rpu::utils::parse_rpu_file(output_rpu.as_ref())?; 105 | assert_eq!(rpus.len(), 259); 106 | 107 | rpus.iter().for_each(|rpu| { 108 | let block = rpu.vdr_dm_data.as_ref().unwrap().get_block(5).unwrap(); 109 | if let ExtMetadataBlock::Level5(b) = block { 110 | assert_eq!(vec![0, 0, 210, 210], b.get_offsets_vec()); 111 | } 112 | }); 113 | 114 | Ok(()) 115 | } 116 | 117 | #[test] 118 | fn extract_rpu_mkv() -> Result<()> { 119 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; 120 | let temp = assert_fs::TempDir::new().unwrap(); 121 | 122 | let input_file = Path::new("assets/hevc_tests/regular.mkv"); 123 | let expected_rpu = Path::new("assets/hevc_tests/regular_rpu.bin"); 124 | 125 | let output_rpu = temp.child("RPU.bin"); 126 | 127 | let assert = cmd 128 | .arg(SUBCOMMAND) 129 | .arg(input_file) 130 | .arg("--rpu-out") 131 | .arg(output_rpu.as_ref()) 132 | .assert(); 133 | 134 | assert.success().stderr(predicate::str::is_empty()); 135 | 136 | output_rpu 137 | .assert(predicate::path::is_file()) 138 | .assert(predicate::path::eq_file(expected_rpu)); 139 | 140 | Ok(()) 141 | } 142 | -------------------------------------------------------------------------------- /tests/hevc/mod.rs: -------------------------------------------------------------------------------- 1 | mod convert; 2 | mod demux; 3 | mod extract_rpu; 4 | mod inject_rpu; 5 | mod mux; 6 | mod remove; 7 | -------------------------------------------------------------------------------- /tests/hevc/remove.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::Result; 4 | use assert_cmd::Command; 5 | use assert_fs::prelude::*; 6 | use predicates::prelude::*; 7 | 8 | const SUBCOMMAND: &str = "remove"; 9 | 10 | #[test] 11 | fn help() -> Result<()> { 12 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; 13 | let assert = cmd.arg(SUBCOMMAND).arg("--help").assert(); 14 | 15 | assert 16 | .success() 17 | .stderr(predicate::str::is_empty()) 18 | .stdout(predicate::str::contains( 19 | "dovi_tool remove [OPTIONS] [input_pos]", 20 | )); 21 | Ok(()) 22 | } 23 | 24 | #[test] 25 | fn remove() -> Result<()> { 26 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; 27 | let temp = assert_fs::TempDir::new().unwrap(); 28 | 29 | let input_file = Path::new("assets/hevc_tests/regular_start_code_4_muxed_el.hevc"); 30 | let expected_bl = Path::new("assets/hevc_tests/regular_bl_start_code_4.hevc"); 31 | 32 | let output_bl = temp.child("BL.hevc"); 33 | 34 | let assert = cmd 35 | .arg(SUBCOMMAND) 36 | .arg(input_file) 37 | .arg("--output") 38 | .arg(output_bl.as_ref()) 39 | .assert(); 40 | 41 | assert.success().stderr(predicate::str::is_empty()); 42 | 43 | output_bl 44 | .assert(predicate::path::is_file()) 45 | .assert(predicate::path::eq_file(expected_bl)); 46 | 47 | Ok(()) 48 | } 49 | 50 | #[test] 51 | fn annexb() -> Result<()> { 52 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; 53 | let temp = assert_fs::TempDir::new().unwrap(); 54 | 55 | let input_file = Path::new("assets/hevc_tests/regular_start_code_4_muxed_el.hevc"); 56 | let expected_bl = Path::new("assets/hevc_tests/regular_demux_bl_annexb.hevc"); 57 | 58 | let output_bl = temp.child("BL.hevc"); 59 | 60 | let assert = cmd 61 | .arg("--start-code") 62 | .arg("annex-b") 63 | .arg(SUBCOMMAND) 64 | .arg(input_file) 65 | .arg("--output") 66 | .arg(output_bl.as_ref()) 67 | .assert(); 68 | 69 | assert.success().stderr(predicate::str::is_empty()); 70 | 71 | output_bl 72 | .assert(predicate::path::is_file()) 73 | .assert(predicate::path::eq_file(expected_bl)); 74 | 75 | Ok(()) 76 | } 77 | -------------------------------------------------------------------------------- /tests/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use assert_cmd::Command; 3 | use predicates::prelude::*; 4 | 5 | mod hevc; 6 | mod rpu; 7 | 8 | #[test] 9 | fn help() -> Result<()> { 10 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; 11 | let assert = cmd.arg("--help").assert(); 12 | 13 | assert 14 | .success() 15 | .stderr(predicate::str::is_empty()) 16 | .stdout(predicate::str::contains("dovi_tool [OPTIONS] ")); 17 | Ok(()) 18 | } 19 | 20 | #[test] 21 | fn version() -> Result<()> { 22 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; 23 | let assert = cmd.arg("--version").assert(); 24 | 25 | assert.success().stderr(predicate::str::is_empty()); 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /tests/rpu/export.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::Result; 4 | use assert_cmd::Command; 5 | use assert_fs::prelude::*; 6 | use predicates::prelude::*; 7 | 8 | const SUBCOMMAND: &str = "export"; 9 | 10 | #[test] 11 | fn help() -> Result<()> { 12 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; 13 | let assert = cmd.arg(SUBCOMMAND).arg("--help").assert(); 14 | 15 | assert 16 | .success() 17 | .stderr(predicate::str::is_empty()) 18 | .stdout(predicate::str::contains( 19 | "dovi_tool export [OPTIONS] [input_pos]", 20 | )); 21 | Ok(()) 22 | } 23 | 24 | #[test] 25 | fn exports_json() -> Result<()> { 26 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; 27 | let temp = assert_fs::TempDir::new().unwrap(); 28 | 29 | let input_rpu = Path::new("assets/tests/fel_orig.bin"); 30 | let output_json = temp.child("RPU_export.json"); 31 | 32 | let assert = cmd 33 | .arg(SUBCOMMAND) 34 | .arg(input_rpu) 35 | .arg("--output") 36 | .arg(output_json.as_ref()) 37 | .assert(); 38 | 39 | assert 40 | .success() 41 | .stderr(predicate::str::is_empty()) 42 | .stdout(predicate::str::contains("Exporting serialized RPU list...")); 43 | 44 | output_json.assert(predicate::path::is_file()); 45 | 46 | Ok(()) 47 | } 48 | 49 | #[test] 50 | fn export_all_and_scenes() -> Result<()> { 51 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; 52 | let temp = assert_fs::TempDir::new().unwrap(); 53 | 54 | let root_path = Path::new(env!("CARGO_MANIFEST_DIR")); 55 | let input_rpu = root_path.join("assets/tests/fel_orig.bin"); 56 | let all_json = temp.child("RPU_export.json"); 57 | let scenes_file = temp.child("RPU_scenes_test.txt"); 58 | 59 | let assert = cmd 60 | .current_dir(temp.canonicalize().unwrap()) 61 | .arg(SUBCOMMAND) 62 | .arg(input_rpu) 63 | .arg("--data") 64 | .arg(format!("all,scenes={}", scenes_file.to_str().unwrap())) 65 | .assert(); 66 | 67 | assert.success().stderr(predicate::str::is_empty()).stdout( 68 | predicate::str::contains("Exporting serialized RPU list...") 69 | .and(predicate::str::contains("Exporting scenes list...")), 70 | ); 71 | 72 | all_json.assert(predicate::path::is_file()); 73 | scenes_file.assert(predicate::path::is_file()); 74 | 75 | Ok(()) 76 | } 77 | -------------------------------------------------------------------------------- /tests/rpu/info.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::Result; 4 | use assert_cmd::Command; 5 | use predicates::prelude::*; 6 | 7 | const SUBCOMMAND: &str = "info"; 8 | 9 | #[test] 10 | fn help() -> Result<()> { 11 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; 12 | let assert = cmd.arg(SUBCOMMAND).arg("--help").assert(); 13 | 14 | assert 15 | .success() 16 | .stderr(predicate::str::is_empty()) 17 | .stdout(predicate::str::contains( 18 | "dovi_tool info [OPTIONS] [input_pos]", 19 | )); 20 | Ok(()) 21 | } 22 | 23 | #[test] 24 | fn summary_p7_mel() -> Result<()> { 25 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; 26 | 27 | let input_rpu = Path::new("assets/hevc_tests/regular_rpu_mel.bin"); 28 | 29 | let assert = cmd.arg(SUBCOMMAND).arg(input_rpu).arg("--summary").assert(); 30 | 31 | assert.success().stderr(predicate::str::is_empty()).stdout( 32 | predicate::str::contains("Summary:") 33 | .and(predicate::str::contains(" Frames: 259")) 34 | .and(predicate::str::contains(" Profile: 7 (MEL)")) 35 | .and(predicate::str::contains(" DM version: 2 (CM v4.0)")) 36 | .and(predicate::str::contains(" Scene/shot count: 3")), 37 | ); 38 | 39 | Ok(()) 40 | } 41 | 42 | #[test] 43 | fn summary_p7_fel() -> Result<()> { 44 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; 45 | 46 | let input_rpu = Path::new("assets/tests/fel_orig.bin"); 47 | 48 | let assert = cmd.arg(SUBCOMMAND).arg(input_rpu).arg("--summary").assert(); 49 | 50 | assert.success().stderr(predicate::str::is_empty()).stdout( 51 | predicate::str::contains("Summary:") 52 | .and(predicate::str::contains(" Frames: 1")) 53 | .and(predicate::str::contains(" Profile: 7 (FEL)")) 54 | .and(predicate::str::contains(" DM version: 1 (CM v2.9)")) 55 | .and(predicate::str::contains(" Scene/shot count: 0")) 56 | .and(predicate::str::contains(" RPU mastering display: 0.0001/1000 nits")) 57 | .and(predicate::str::contains(" RPU content light level (L1): MaxCLL: 630.04 nits, MaxFALL: 5.83 nits")) 58 | .and(predicate::str::contains(" L6 metadata: Mastering display: 0.0001/1000 nits. MaxCLL: 1712 nits, MaxFALL: 175 nits")) 59 | .and(predicate::str::contains(" L2 trims: 100 nits")) 60 | ); 61 | 62 | Ok(()) 63 | } 64 | 65 | #[test] 66 | fn summary_p8() -> Result<()> { 67 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; 68 | 69 | let input_rpu = Path::new("assets/hevc_tests/regular_rpu.bin"); 70 | 71 | let assert = cmd.arg(SUBCOMMAND).arg(input_rpu).arg("--summary").assert(); 72 | 73 | assert.success().stderr(predicate::str::is_empty()).stdout( 74 | predicate::str::contains("Summary:") 75 | .and(predicate::str::contains(" Frames: 259")) 76 | .and(predicate::str::contains(" Profile: 8")) 77 | .and(predicate::str::contains(" DM version: 2 (CM v4.0)")) 78 | .and(predicate::str::contains(" Scene/shot count: 3")) 79 | .and(predicate::str::contains(" RPU mastering display: 0.0001/1000 nits")) 80 | .and(predicate::str::contains(" RPU content light level (L1): MaxCLL: 632.88 nits, MaxFALL: 10.05 nits")) 81 | .and(predicate::str::contains(" L6 metadata: Mastering display: 0.0001/1000 nits. MaxCLL: 3948 nits, MaxFALL: 120 nits")) 82 | .and(predicate::str::contains(" L2 trims: 100 nits, 600 nits, 1000 nits")) 83 | ); 84 | 85 | Ok(()) 86 | } 87 | 88 | #[test] 89 | fn invalid_l3_error() -> Result<()> { 90 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; 91 | 92 | let input_rpu = Path::new("assets/tests/st2094_10_level3.bin"); 93 | 94 | let assert = cmd.arg(SUBCOMMAND).arg(input_rpu).arg("-s").assert(); 95 | 96 | assert.failure().stderr(predicate::str::contains( 97 | "Error: Found invalid RPU: Index 0, error: Invalid block level 3 for CM v2.9 RPU", 98 | )); 99 | 100 | Ok(()) 101 | } 102 | -------------------------------------------------------------------------------- /tests/rpu/mod.rs: -------------------------------------------------------------------------------- 1 | mod editor; 2 | mod export; 3 | mod generate; 4 | mod info; 5 | mod plot; 6 | -------------------------------------------------------------------------------- /tests/rpu/plot.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use anyhow::Result; 4 | use assert_cmd::Command; 5 | use assert_fs::prelude::*; 6 | use predicates::prelude::*; 7 | 8 | const SUBCOMMAND: &str = "plot"; 9 | 10 | #[test] 11 | fn help() -> Result<()> { 12 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; 13 | let assert = cmd.arg(SUBCOMMAND).arg("--help").assert(); 14 | 15 | assert 16 | .success() 17 | .stderr(predicate::str::is_empty()) 18 | .stdout(predicate::str::contains( 19 | "dovi_tool plot [OPTIONS] [input_pos]", 20 | )); 21 | Ok(()) 22 | } 23 | 24 | #[test] 25 | fn plot_p7() -> Result<()> { 26 | let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))?; 27 | let temp = assert_fs::TempDir::new().unwrap(); 28 | 29 | let input_rpu = Path::new("assets/hevc_tests/regular_rpu.bin"); 30 | let output_file = temp.child("L1_plot.png"); 31 | 32 | let assert = cmd 33 | .arg(SUBCOMMAND) 34 | .arg(input_rpu) 35 | .arg("--output") 36 | .arg(output_file.as_ref()) 37 | .assert(); 38 | 39 | assert.success().stderr(predicate::str::is_empty()); 40 | 41 | output_file.assert(predicate::path::is_file()); 42 | 43 | Ok(()) 44 | } 45 | --------------------------------------------------------------------------------