Skip to content

Commit

Permalink
Implement support for encoding HDR10+ from JSON metadata file
Browse files Browse the repository at this point in the history
  • Loading branch information
quietvoid committed Aug 4, 2023
1 parent 184bf4b commit 82dd0df
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 11 deletions.
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ new_debug_unreachable = "1.0.4"
once_cell = "1.18.0"
av1-grain = { version = "0.2.2", features = ["serialize"] }
serde-big-array = { version = "0.5.1", optional = true }
hdr10plus = { version = "2.0.0", features = ["json"] }

[dependencies.image]
version = "0.24.6"
Expand Down
9 changes: 8 additions & 1 deletion src/api/config/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -91,6 +92,11 @@ pub struct EncoderConfig {
pub tune: Tune,
/// Parameters for grain synthesis.
pub film_grain_params: Option<Vec<GrainTableSegment>>,
/// 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<BTreeMap<u64, T35>>,
/// Number of tiles horizontally. Must be a power of two.
///
/// Overridden by [`tiles`], if present.
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/api/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
17 changes: 17 additions & 0 deletions src/api/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -299,3 +307,12 @@ impl<T: Pixel> IntoFrame<T> for (Frame<T>, Option<FrameParameters>) {
(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)
}
}
43 changes: 43 additions & 0 deletions src/bin/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ use rav1e::prelude::*;
use scan_fmt::scan_fmt;

use rav1e::config::CpuFeatureLevel;

use std::collections::BTreeMap;
use std::fs::File;
use std::io;
use std::io::prelude::*;
Expand Down Expand Up @@ -195,6 +197,14 @@ pub struct CliOptions {
help_heading = "ENCODE SETTINGS"
)]
pub film_grain_table: Option<PathBuf>,
/// 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<PathBuf>,

/// Pixel range
#[clap(long, value_parser, help_heading = "VIDEO METADATA")]
Expand Down Expand Up @@ -684,6 +694,39 @@ fn parse_config(matches: &CliOptions) -> Result<EncoderConfig, CliError> {
}
}

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<u64, T35> = 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);
}
Expand Down
47 changes: 37 additions & 10 deletions src/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,19 @@ impl<T: Pixel> FrameInvariants<T> {
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<T: Pixel> fmt::Display for FrameInvariants<T> {
Expand Down Expand Up @@ -3686,11 +3699,14 @@ pub fn encode_show_existing_frame<T: Pixel>(
}

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();
Expand Down Expand Up @@ -3767,11 +3783,14 @@ pub fn encode_frame<T: Pixel>(
}

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();
Expand Down Expand Up @@ -3827,6 +3846,14 @@ pub fn update_rec_buffer<T: Pixel>(
}
}

fn write_t35_metadata_packet(packet: &mut Vec<u8>, 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::*;
Expand Down
1 change: 1 addition & 0 deletions src/fuzzing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down

0 comments on commit 82dd0df

Please sign in to comment.