From 85f734b3f04706bcca80378bae7c7da58ae72a07 Mon Sep 17 00:00:00 2001 From: quietvoid <39477805+quietvoid@users.noreply.github.com> Date: Wed, 24 Aug 2022 21:59:12 -0400 Subject: [PATCH] Implement support for encoding HDR10+ from JSON metadata file --- Cargo.toml | 1 + src/api/config/encoder.rs | 8 +++++ src/api/internal.rs | 76 +++++++++++++++++++++++++++++++++++++++ src/api/test.rs | 4 +++ src/bin/common.rs | 43 ++++++++++++++++++++++ 5 files changed, 132 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 676f688fe7..046048501a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,6 +105,7 @@ new_debug_unreachable = "1.0.4" once_cell = "1.13.0" av1-grain = { version = "0.1.1", features = ["serialize"] } serde-big-array = { version = "0.4.1", optional = true } +hdr10plus = { version = "1.1.1", features = ["json"] } [dependencies.image] version = "0.23" diff --git a/src/api/config/encoder.rs b/src/api/config/encoder.rs index 55c2181c6b..1677cde37d 100644 --- a/src/api/config/encoder.rs +++ b/src/api/config/encoder.rs @@ -15,6 +15,9 @@ use crate::api::{Rational, SpeedSettings}; use crate::encoder::Tune; use crate::serialize::{Deserialize, Serialize}; +#[cfg(feature = "unstable")] +use std::collections::BTreeMap; + use std::fmt; // We add 1 to rdo_lookahead_frames in a bunch of places. @@ -85,6 +88,9 @@ pub struct EncoderConfig { pub tune: Tune, /// Parameters for grain synthesis. pub film_grain_params: Option>, + /// HDR10+ T.35 metadata payload map, by frame index. + #[cfg(feature = "unstable")] + pub hdr10plus_payloads: Option>>, /// Number of tiles horizontally. Must be a power of two. /// /// Overridden by [`tiles`], if present. @@ -162,6 +168,8 @@ impl EncoderConfig { bitrate: 0, tune: Tune::default(), film_grain_params: None, + #[cfg(feature = "unstable")] + hdr10plus_payloads: None, tile_cols: 0, tile_rows: 0, tiles: 0, diff --git a/src/api/internal.rs b/src/api/internal.rs index 169a9c96ed..059eae7fd0 100644 --- a/src/api/internal.rs +++ b/src/api/internal.rs @@ -349,6 +349,13 @@ impl ContextInner { } self.frame_q.insert(input_frameno, frame); + #[cfg(feature = "unstable")] + // Update T.35 metadata from encoder config + let maybe_updated_t35_metadata = self.get_maybe_updated_t35_metadata( + input_frameno, + params.as_ref().map(|params| params.t35_metadata.as_ref()), + ); + if let Some(params) = params { if params.frame_type_override == FrameTypeOverride::Key { self.keyframes_forced.insert(input_frameno); @@ -356,7 +363,21 @@ impl ContextInner { if let Some(op) = params.opaque { self.opaque_q.insert(input_frameno, op); } + + #[cfg(not(feature = "unstable"))] self.t35_q.insert(input_frameno, params.t35_metadata); + + #[cfg(feature = "unstable")] + if let Some(new_t35_metadata) = maybe_updated_t35_metadata { + self.t35_q.insert(input_frameno, new_t35_metadata.into_boxed_slice()); + } else { + self.t35_q.insert(input_frameno, params.t35_metadata); + } + } else { + #[cfg(feature = "unstable")] + if let Some(new_t35_metadata) = maybe_updated_t35_metadata { + self.t35_q.insert(input_frameno, new_t35_metadata.into_boxed_slice()); + } } if !self.needs_more_frame_q_lookahead(self.next_lookahead_frame) { @@ -1688,4 +1709,59 @@ impl ContextInner { (prev_keyframe_nframes, prev_keyframe_ntus) } } + + #[cfg(feature = "unstable")] + /// Updates the T.35 metadata to be added to the frame. + /// The existing T.35 array may come from `FrameParameters`. + /// New metadata is added from the encoder config: + /// - HDR10+, ST2094-40 in `EncoderConfig.hdr10plus_payloads` + /// + /// Returns an `Option`, where `None` means the T.35 metadata is unchanged. + /// Otherwise, the updated T.35 metadata array is returned. + fn get_maybe_updated_t35_metadata( + &self, input_frameno: u64, maybe_existing_t35_metadata: Option<&[T35]>, + ) -> Option> { + let hdr10plus_payload = self + .config + .hdr10plus_payloads + .as_ref() + .and_then(|list| list.get(&input_frameno)); + + let update_t35_metadata = hdr10plus_payload.is_some(); + + let mut new_t35_metadata = if update_t35_metadata { + Some( + maybe_existing_t35_metadata.map_or_else(Vec::new, |t35| t35.to_vec()), + ) + } else { + None + }; + + if let Some(list) = new_t35_metadata.as_mut() { + // HDR10+, ST2094-40 + if let Some(payload) = hdr10plus_payload { + // FIXME: Make const + let st2094_40_needle = &[ + 0x00, 0x03C, // Samsung Electronics America + 0x00, 0x01, // ST-2094-40 + 0x04, // application_identifier = 4 + 0x01, // application_mode =1 + ]; + + let has_existing_hdr10plus_meta = list.iter().any(|t35| { + t35.country_code == 0xB5 && t35.data.starts_with(st2094_40_needle) + }); + + if !has_existing_hdr10plus_meta { + list.push(T35 { + country_code: 0xB5, + country_code_extension_byte: 0x00, + data: payload.clone().into_boxed_slice(), + }); + } + } + } + + new_t35_metadata + } } diff --git a/src/api/test.rs b/src/api/test.rs index 5b7a3ae7e6..efb92b9fad 100644 --- a/src/api/test.rs +++ b/src/api/test.rs @@ -2128,6 +2128,8 @@ fn log_q_exp_overflow() { bitrate: 1, tune: Tune::Psychovisual, film_grain_params: None, + #[cfg(feature = "unstable")] + hdr10plus_payloads: None, tile_cols: 0, tile_rows: 0, tiles: 0, @@ -2205,6 +2207,8 @@ fn guess_frame_subtypes_assert() { bitrate: 16384, tune: Tune::Psychovisual, film_grain_params: None, + #[cfg(feature = "unstable")] + hdr10plus_payloads: None, tile_cols: 0, tile_rows: 0, tiles: 0, diff --git a/src/bin/common.rs b/src/bin/common.rs index d8db6ea181..80111a4d5b 100644 --- a/src/bin/common.rs +++ b/src/bin/common.rs @@ -17,6 +17,9 @@ use once_cell::sync::Lazy; use rav1e::prelude::*; use scan_fmt::scan_fmt; +#[cfg(feature = "unstable")] +use std::collections::BTreeMap; + use std::fs::File; use std::io; use std::io::prelude::*; @@ -203,6 +206,15 @@ pub struct CliOptions { help_heading = "ENCODE SETTINGS" )] pub film_grain_table: Option, + /// Uses a HDR10+ metadata JSON file to add as T.35 metadata to the encode. + #[cfg(feature = "unstable")] + #[clap( + long, + alias = "dhdr10-info", + value_parser, + help_heading = "ENCODE SETTINGS" + )] + pub hdr10plus_json: Option, /// Pixel range #[clap(long, value_parser, help_heading = "VIDEO METADATA")] @@ -688,11 +700,42 @@ fn parse_config(matches: &CliOptions) -> Result { .expect("Failed to read film grain table file"); let table = av1_grain::parse_grain_table(&contents) .expect("Failed to parse film grain table"); + if !table.is_empty() { cfg.film_grain_params = Some(table); } } + #[cfg(feature = "unstable")] + if let Some(json_file) = matches.hdr10plus_json.as_ref() { + let contents = std::fs::read_to_string(json_file) + .expect("Failed to read HDR10+ metadata file"); + let metadata_root = + hdr10plus::metadata_json::MetadataJsonRoot::parse(&contents) + .expect("Failed to parse HDR10+ metadata"); + + let payloads: BTreeMap> = metadata_root + .scene_info + .iter() + .filter_map(|meta| { + hdr10plus::metadata::Hdr10PlusMetadata::try_from(meta) + .and_then(|meta| meta.encode(true)) + .ok() + .map(|mut bytes| { + // Serialized with country code, which shouldn't be present + bytes.remove(0); + bytes + }) + }) + .zip(0u64..) + .map(|(payload, frame_no)| (frame_no, payload)) + .collect(); + + if !payloads.is_empty() { + cfg.hdr10plus_payloads = Some(payloads); + } + } + if let Some(frame_rate) = matches.frame_rate { cfg.time_base = Rational::new(matches.time_scale, frame_rate); }