Skip to content

Commit 82dd0df

Browse files
committed
Implement support for encoding HDR10+ from JSON metadata file
1 parent 184bf4b commit 82dd0df

File tree

8 files changed

+132
-11
lines changed

8 files changed

+132
-11
lines changed

Cargo.lock

+23
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ new_debug_unreachable = "1.0.4"
108108
once_cell = "1.18.0"
109109
av1-grain = { version = "0.2.2", features = ["serialize"] }
110110
serde-big-array = { version = "0.5.1", optional = true }
111+
hdr10plus = { version = "2.0.0", features = ["json"] }
111112

112113
[dependencies.image]
113114
version = "0.24.6"

src/api/config/encoder.rs

+8-1
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99

1010
use itertools::*;
1111

12-
use crate::api::color::*;
1312
use crate::api::config::GrainTableSegment;
13+
use crate::api::{color::*, T35};
1414
use crate::api::{Rational, SpeedSettings};
1515
use crate::encoder::Tune;
1616
use crate::serialize::{Deserialize, Serialize};
1717

18+
use std::collections::BTreeMap;
1819
use std::fmt;
1920

2021
// We add 1 to rdo_lookahead_frames in a bunch of places.
@@ -91,6 +92,11 @@ pub struct EncoderConfig {
9192
pub tune: Tune,
9293
/// Parameters for grain synthesis.
9394
pub film_grain_params: Option<Vec<GrainTableSegment>>,
95+
/// HDR10+, ST2094-40 T.35 metadata payload map, by input frame index.
96+
///
97+
/// The T.35 metadata is expected to follow the specification
98+
/// defined at https://aomediacodec.github.io/av1-hdr10plus.
99+
pub hdr10plus_payloads: Option<BTreeMap<u64, T35>>,
94100
/// Number of tiles horizontally. Must be a power of two.
95101
///
96102
/// Overridden by [`tiles`], if present.
@@ -167,6 +173,7 @@ impl EncoderConfig {
167173
bitrate: 0,
168174
tune: Tune::default(),
169175
film_grain_params: None,
176+
hdr10plus_payloads: None,
170177
tile_cols: 0,
171178
tile_rows: 0,
172179
tiles: 0,

src/api/test.rs

+2
Original file line numberDiff line numberDiff line change
@@ -2127,6 +2127,7 @@ fn log_q_exp_overflow() {
21272127
bitrate: 1,
21282128
tune: Tune::Psychovisual,
21292129
film_grain_params: None,
2130+
hdr10plus_payloads: None,
21302131
tile_cols: 0,
21312132
tile_rows: 0,
21322133
tiles: 0,
@@ -2204,6 +2205,7 @@ fn guess_frame_subtypes_assert() {
22042205
bitrate: 16384,
22052206
tune: Tune::Psychovisual,
22062207
film_grain_params: None,
2208+
hdr10plus_payloads: None,
22072209
tile_cols: 0,
22082210
tile_rows: 0,
22092211
tiles: 0,

src/api/util.rs

+17
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,14 @@ impl fmt::Display for FrameType {
137137
}
138138
}
139139

140+
/// ST2094-40 T.35 metadata payload expected prefix.
141+
pub const ST2094_40_PREFIX: &[u8] = &[
142+
0x00, 0x03C, // Samsung Electronics America
143+
0x00, 0x01, // ST-2094-40
144+
0x04, // application_identifier = 4
145+
0x01, // application_mode = 1
146+
];
147+
140148
/// A single T.35 metadata packet.
141149
#[derive(Clone, Debug, Default)]
142150
pub struct T35 {
@@ -299,3 +307,12 @@ impl<T: Pixel> IntoFrame<T> for (Frame<T>, Option<FrameParameters>) {
299307
(Some(Arc::new(self.0)), self.1)
300308
}
301309
}
310+
311+
impl T35 {
312+
/// Whether the T.35 metadata is HDR10+ Metadata.
313+
///
314+
/// According to the [AV1 HDR10+ specification](https://aomediacodec.github.io/av1-hdr10plus).
315+
pub fn is_hdr10plus_metadata(&self) -> bool {
316+
self.country_code == 0xB5 && self.data.starts_with(ST2094_40_PREFIX)
317+
}
318+
}

src/bin/common.rs

+43
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ use rav1e::prelude::*;
1818
use scan_fmt::scan_fmt;
1919

2020
use rav1e::config::CpuFeatureLevel;
21+
22+
use std::collections::BTreeMap;
2123
use std::fs::File;
2224
use std::io;
2325
use std::io::prelude::*;
@@ -195,6 +197,14 @@ pub struct CliOptions {
195197
help_heading = "ENCODE SETTINGS"
196198
)]
197199
pub film_grain_table: Option<PathBuf>,
200+
/// Uses a HDR10+ metadata JSON file to add as T.35 metadata to the encode.
201+
#[clap(
202+
long,
203+
alias = "dhdr10-info",
204+
value_parser,
205+
help_heading = "ENCODE SETTINGS"
206+
)]
207+
pub hdr10plus_json: Option<PathBuf>,
198208

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

697+
if let Some(json_file) = matches.hdr10plus_json.as_ref() {
698+
let contents = std::fs::read_to_string(json_file)
699+
.expect("Failed to read HDR10+ metadata file");
700+
let metadata_root =
701+
hdr10plus::metadata_json::MetadataJsonRoot::parse(&contents)
702+
.expect("Failed to parse HDR10+ metadata");
703+
704+
let hdr10plus_enc_opts = hdr10plus::metadata::Hdr10PlusMetadataEncOpts {
705+
with_country_code: false,
706+
..Default::default()
707+
};
708+
let payloads: BTreeMap<u64, T35> = metadata_root
709+
.scene_info
710+
.iter()
711+
.filter_map(|meta| {
712+
hdr10plus::metadata::Hdr10PlusMetadata::try_from(meta)
713+
.and_then(|meta| meta.encode_with_opts(&hdr10plus_enc_opts))
714+
.map(|payload| T35 {
715+
country_code: 0xB5,
716+
country_code_extension_byte: 0x00,
717+
data: payload.into_boxed_slice(),
718+
})
719+
.ok()
720+
})
721+
.zip(0u64..)
722+
.map(|(payload, frame_no)| (frame_no, payload))
723+
.collect();
724+
725+
if !payloads.is_empty() {
726+
cfg.hdr10plus_payloads = Some(payloads);
727+
}
728+
}
729+
687730
if let Some(frame_rate) = matches.frame_rate {
688731
cfg.time_base = Rational::new(matches.time_scale, frame_rate);
689732
}

src/encoder.rs

+37-10
Original file line numberDiff line numberDiff line change
@@ -1284,6 +1284,19 @@ impl<T: Pixel> FrameInvariants<T> {
12841284
self.input_frameno * TIMESTAMP_BASE_UNIT * self.sequence.time_base.num
12851285
/ self.sequence.time_base.den
12861286
}
1287+
1288+
/// HDR10+ Metadata as T.35 metadata from [`EncoderConfig`]
1289+
pub fn hdr10plus_metadata(&self) -> Option<&T35> {
1290+
if !(self.show_frame || self.is_show_existing_frame()) {
1291+
return None;
1292+
}
1293+
1294+
self
1295+
.config
1296+
.hdr10plus_payloads
1297+
.as_ref()
1298+
.and_then(|payloads| payloads.get(&self.input_frameno))
1299+
}
12871300
}
12881301

12891302
impl<T: Pixel> fmt::Display for FrameInvariants<T> {
@@ -3686,11 +3699,14 @@ pub fn encode_show_existing_frame<T: Pixel>(
36863699
}
36873700

36883701
for t35 in fi.t35_metadata.iter() {
3689-
let mut t35_buf = Vec::new();
3690-
let mut t35_bw = BitWriter::endian(&mut t35_buf, BigEndian);
3691-
t35_bw.write_t35_metadata_obu(t35).unwrap();
3692-
packet.write_all(&t35_buf).unwrap();
3693-
t35_buf.clear();
3702+
write_t35_metadata_packet(&mut packet, t35);
3703+
}
3704+
3705+
// HDR10+ Metadata OBU from config
3706+
if let Some(t35) = fi.hdr10plus_metadata() {
3707+
if !fi.t35_metadata.iter().any(|t35| t35.is_hdr10plus_metadata()) {
3708+
write_t35_metadata_packet(&mut packet, t35);
3709+
}
36943710
}
36953711

36963712
let mut buf1 = Vec::new();
@@ -3767,11 +3783,14 @@ pub fn encode_frame<T: Pixel>(
37673783
}
37683784

37693785
for t35 in fi.t35_metadata.iter() {
3770-
let mut t35_buf = Vec::new();
3771-
let mut t35_bw = BitWriter::endian(&mut t35_buf, BigEndian);
3772-
t35_bw.write_t35_metadata_obu(t35).unwrap();
3773-
packet.write_all(&t35_buf).unwrap();
3774-
t35_buf.clear();
3786+
write_t35_metadata_packet(&mut packet, t35);
3787+
}
3788+
3789+
// HDR10+ Metadata OBU from config
3790+
if let Some(t35) = fi.hdr10plus_metadata() {
3791+
if !fi.t35_metadata.iter().any(|t35| t35.is_hdr10plus_metadata()) {
3792+
write_t35_metadata_packet(&mut packet, t35);
3793+
}
37753794
}
37763795

37773796
let mut buf1 = Vec::new();
@@ -3827,6 +3846,14 @@ pub fn update_rec_buffer<T: Pixel>(
38273846
}
38283847
}
38293848

3849+
fn write_t35_metadata_packet(packet: &mut Vec<u8>, t35: &T35) {
3850+
let mut t35_buf = Vec::new();
3851+
let mut t35_bw = BitWriter::endian(&mut t35_buf, BigEndian);
3852+
t35_bw.write_t35_metadata_obu(t35).unwrap();
3853+
packet.write_all(&t35_buf).unwrap();
3854+
t35_buf.clear();
3855+
}
3856+
38303857
#[cfg(test)]
38313858
mod test {
38323859
use super::*;

src/fuzzing.rs

+1
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ impl Arbitrary<'_> for ArbitraryEncoder {
258258
switch_frame_interval: u.int_in_range(0..=3)?,
259259
tune: *u.choose(&[Tune::Psnr, Tune::Psychovisual])?,
260260
film_grain_params: None,
261+
hdr10plus_payloads: None,
261262
};
262263

263264
let frame_count =

0 commit comments

Comments
 (0)