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..855c97f14f 100644 --- a/src/api/config/encoder.rs +++ b/src/api/config/encoder.rs @@ -15,6 +15,7 @@ use crate::api::{Rational, SpeedSettings}; use crate::encoder::Tune; use crate::serialize::{Deserialize, Serialize}; +use std::collections::BTreeMap; use std::fmt; // We add 1 to rdo_lookahead_frames in a bunch of places. @@ -85,6 +86,11 @@ pub struct EncoderConfig { pub tune: Tune, /// Parameters for grain synthesis. pub film_grain_params: Option>, + /// HDR10+, ST2094-40 T.35 metadata payload map, by input frame index. + /// + /// The payloads are expected to follow the specification + /// defined at https://aomediacodec.github.io/av1-hdr10plus. + pub hdr10plus_payloads: Option>>, /// Number of tiles horizontally. Must be a power of two. /// /// Overridden by [`tiles`], if present. @@ -162,6 +168,7 @@ impl EncoderConfig { bitrate: 0, tune: Tune::default(), film_grain_params: None, + 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..b7c437344e 100644 --- a/src/api/internal.rs +++ b/src/api/internal.rs @@ -11,7 +11,8 @@ use crate::activity::ActivityMask; use crate::api::lookahead::*; use crate::api::{ - EncoderConfig, EncoderStatus, FrameType, Opaque, Packet, T35, + EncoderConfig, EncoderStatus, FrameType, Opaque, Packet, ST2094_40_PREFIX, + T35, }; use crate::color::ChromaSampling::Cs400; use crate::cpu_features::CpuFeatureLevel; @@ -349,6 +350,12 @@ impl ContextInner { } self.frame_q.insert(input_frameno, frame); + // 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,14 @@ impl ContextInner { if let Some(op) = params.opaque { self.opaque_q.insert(input_frameno, op); } - self.t35_q.insert(input_frameno, params.t35_metadata); + + 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 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 +1702,51 @@ impl ContextInner { (prev_keyframe_nframes, prev_keyframe_ntus) } } + + /// Updates the T.35 metadata to be added to the frame. + /// The existing T.35 array may come from `FrameParameters`. + /// + /// Added from [`EncoderConfig`]: + /// - HDR10+, ST2094-40 in `hdr10plus_payloads`. + /// + /// Returns an `Option`, where `None` means the T.35 metadata is unchanged. + /// Otherwise, the updated T.35 metadata 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 { + let has_existing_hdr10plus_meta = list.iter().any(|t35| { + t35.country_code == 0xB5 && t35.data.starts_with(ST2094_40_PREFIX) + }); + + 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..943a244cc5 100644 --- a/src/api/test.rs +++ b/src/api/test.rs @@ -2128,6 +2128,7 @@ fn log_q_exp_overflow() { bitrate: 1, tune: Tune::Psychovisual, film_grain_params: None, + hdr10plus_payloads: None, tile_cols: 0, tile_rows: 0, tiles: 0, @@ -2205,6 +2206,7 @@ fn guess_frame_subtypes_assert() { bitrate: 16384, tune: Tune::Psychovisual, film_grain_params: None, + hdr10plus_payloads: None, tile_cols: 0, tile_rows: 0, tiles: 0, diff --git a/src/api/util.rs b/src/api/util.rs index a2ab9794e6..19475c2a7c 100644 --- a/src/api/util.rs +++ b/src/api/util.rs @@ -137,6 +137,14 @@ impl fmt::Display for FrameType { } } +/// ST2094-40 T.35 metadata payload expected prefix. +pub const ST2094_40_PREFIX: &[u8] = &[ + 0x00, 0x03C, // Samsung Electronics America + 0x00, 0x01, // ST-2094-40 + 0x04, // application_identifier = 4 + 0x01, // application_mode =1 +]; + /// A single T.35 metadata packet. #[derive(Clone, Debug, Default)] pub struct T35 { diff --git a/src/bin/common.rs b/src/bin/common.rs index d8db6ea181..4029262033 100644 --- a/src/bin/common.rs +++ b/src/bin/common.rs @@ -17,6 +17,7 @@ use once_cell::sync::Lazy; use rav1e::prelude::*; use scan_fmt::scan_fmt; +use std::collections::BTreeMap; use std::fs::File; use std::io; use std::io::prelude::*; @@ -203,6 +204,14 @@ 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. + #[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")] @@ -693,6 +702,35 @@ fn parse_config(matches: &CliOptions) -> Result { } } + 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); }