diff --git a/Cargo.lock b/Cargo.lock index d18c2449fb..8b3d6fc293 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,6 +185,15 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d28070975aaf4ef1fd0bd1f29b739c06c2cdd9972e090617fb6dca3b2cb564e" +[[package]] +name = "bitvec_helpers" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef6883bd86b4112b56be19de3a1628de6c4063be7be6e641d484c83069efb4a" +dependencies = [ + "bitstream-io", +] + [[package]] name = "bstr" version = "1.1.0" @@ -684,6 +693,18 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hdr10plus" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee3a6bfa9ada73834523412702bbcaa0f53c3393acb36278cd3489c38c808491" +dependencies = [ + "anyhow", + "bitvec_helpers", + "serde", + "serde_json", +] + [[package]] name = "heck" version = "0.4.0" @@ -1315,6 +1336,7 @@ dependencies = [ "crossbeam", "dav1d-sys", "fern", + "hdr10plus", "image", "interpolate_name", "itertools 0.10.5", @@ -1541,6 +1563,7 @@ version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" dependencies = [ + "indexmap", "itoa", "ryu", "serde", diff --git a/Cargo.toml b/Cargo.toml index 0bae34c924..33f98e2927 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,6 +109,7 @@ new_debug_unreachable = "1.0.4" once_cell = "1.17.1" av1-grain = { version = "0.2.0", features = ["serialize"] } serde-big-array = { version = "0.5.0", optional = true } +hdr10plus = { version = "2.0.0", features = ["json"] } [dependencies.image] version = "0.24.3" diff --git a/src/api/config/encoder.rs b/src/api/config/encoder.rs index be33370769..95e7021b75 100644 --- a/src/api/config/encoder.rs +++ b/src/api/config/encoder.rs @@ -9,12 +9,13 @@ use itertools::*; -use crate::api::color::*; use crate::api::config::GrainTableSegment; +use crate::api::{color::*, T35}; 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. @@ -91,6 +92,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 T.35 metadata is 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. @@ -167,6 +173,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/test.rs b/src/api/test.rs index 0a698ba4d5..8ee8503617 100644 --- a/src/api/test.rs +++ b/src/api/test.rs @@ -2127,6 +2127,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, @@ -2204,6 +2205,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..56061a6729 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 { @@ -299,3 +307,12 @@ impl IntoFrame for (Frame, Option) { (Some(Arc::new(self.0)), self.1) } } + +impl T35 { + /// Whether the T.35 metadata is HDR10+ Metadata. + /// + /// According to the [AV1 HDR10+ specification](https://aomediacodec.github.io/av1-hdr10plus). + pub fn is_hdr10plus_metadata(&self) -> bool { + self.country_code == 0xB5 && self.data.starts_with(ST2094_40_PREFIX) + } +} diff --git a/src/bin/common.rs b/src/bin/common.rs index 2c9b120365..1d249717d8 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::*; @@ -194,6 +195,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")] @@ -678,6 +687,39 @@ 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 hdr10plus_enc_opts = hdr10plus::metadata::Hdr10PlusMetadataEncOpts { + with_country_code: false, + ..Default::default() + }; + let payloads: BTreeMap = metadata_root + .scene_info + .iter() + .filter_map(|meta| { + hdr10plus::metadata::Hdr10PlusMetadata::try_from(meta) + .and_then(|meta| meta.encode_with_opts(&hdr10plus_enc_opts)) + .map(|payload| T35 { + country_code: 0xB5, + country_code_extension_byte: 0x00, + data: payload.into_boxed_slice(), + }) + .ok() + }) + .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); } diff --git a/src/encoder.rs b/src/encoder.rs index 2b8d2ee80e..0291ba4724 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -1287,6 +1287,19 @@ impl FrameInvariants { self.input_frameno * TIMESTAMP_BASE_UNIT * self.sequence.time_base.num / self.sequence.time_base.den } + + /// HDR10+ Metadata as T.35 metadata from [`EncoderConfig`] + pub fn hdr10plus_metadata(&self) -> Option<&T35> { + if !(self.show_frame || self.is_show_existing_frame()) { + return None; + } + + self + .config + .hdr10plus_payloads + .as_ref() + .and_then(|payloads| payloads.get(&self.input_frameno)) + } } impl fmt::Display for FrameInvariants { @@ -3682,11 +3695,14 @@ pub fn encode_show_existing_frame( } for t35 in fi.t35_metadata.iter() { - let mut t35_buf = Vec::new(); - let mut t35_bw = BitWriter::endian(&mut t35_buf, BigEndian); - t35_bw.write_t35_metadata_obu(t35).unwrap(); - packet.write_all(&t35_buf).unwrap(); - t35_buf.clear(); + write_t35_metadata_packet(&mut packet, t35); + } + + // HDR10+ Metadata OBU from config + if let Some(t35) = fi.hdr10plus_metadata() { + if !fi.t35_metadata.iter().any(|t35| t35.is_hdr10plus_metadata()) { + write_t35_metadata_packet(&mut packet, t35); + } } let mut buf1 = Vec::new(); @@ -3762,11 +3778,14 @@ pub fn encode_frame( } for t35 in fi.t35_metadata.iter() { - let mut t35_buf = Vec::new(); - let mut t35_bw = BitWriter::endian(&mut t35_buf, BigEndian); - t35_bw.write_t35_metadata_obu(t35).unwrap(); - packet.write_all(&t35_buf).unwrap(); - t35_buf.clear(); + write_t35_metadata_packet(&mut packet, t35); + } + + // HDR10+ Metadata OBU from config + if let Some(t35) = fi.hdr10plus_metadata() { + if !fi.t35_metadata.iter().any(|t35| t35.is_hdr10plus_metadata()) { + write_t35_metadata_packet(&mut packet, t35); + } } let mut buf1 = Vec::new(); @@ -3822,6 +3841,14 @@ pub fn update_rec_buffer( } } +fn write_t35_metadata_packet(packet: &mut Vec, t35: &T35) { + let mut t35_buf = Vec::new(); + let mut t35_bw = BitWriter::endian(&mut t35_buf, BigEndian); + t35_bw.write_t35_metadata_obu(t35).unwrap(); + packet.write_all(&t35_buf).unwrap(); + t35_buf.clear(); +} + #[cfg(test)] mod test { use super::*; diff --git a/src/fuzzing.rs b/src/fuzzing.rs index b51d29478b..d186f3b7a5 100644 --- a/src/fuzzing.rs +++ b/src/fuzzing.rs @@ -258,6 +258,7 @@ impl Arbitrary<'_> for ArbitraryEncoder { switch_frame_interval: u.int_in_range(0..=3)?, tune: *u.choose(&[Tune::Psnr, Tune::Psychovisual])?, film_grain_params: None, + hdr10plus_payloads: None, }; let frame_count =