Skip to content

Commit f147251

Browse files
committed
Implement support for encoding HDR10+ from JSON metadata file
1 parent 4f7af46 commit f147251

File tree

7 files changed

+119
-2
lines changed

7 files changed

+119
-2
lines changed

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.13.0"
109109
av1-grain = { version = "0.2.0", features = ["serialize"] }
110110
serde-big-array = { version = "0.4.1", optional = true }
111+
hdr10plus = { version = "1.2.0", features = ["json"] }
111112

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

src/api/config/encoder.rs

+7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ 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.
@@ -85,6 +86,11 @@ pub struct EncoderConfig {
8586
pub tune: Tune,
8687
/// Parameters for grain synthesis.
8788
pub film_grain_params: Option<Vec<GrainTableSegment>>,
89+
/// HDR10+, ST2094-40 T.35 metadata payload map, by input frame index.
90+
///
91+
/// The payloads are expected to follow the specification
92+
/// defined at https://aomediacodec.github.io/av1-hdr10plus.
93+
pub hdr10plus_payloads: Option<BTreeMap<u64, Vec<u8>>>,
8894
/// Number of tiles horizontally. Must be a power of two.
8995
///
9096
/// Overridden by [`tiles`], if present.
@@ -159,6 +165,7 @@ impl EncoderConfig {
159165
bitrate: 0,
160166
tune: Tune::default(),
161167
film_grain_params: None,
168+
hdr10plus_payloads: None,
162169
tile_cols: 0,
163170
tile_rows: 0,
164171
tiles: 0,

src/api/internal.rs

+63-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
use crate::activity::ActivityMask;
1212
use crate::api::lookahead::*;
1313
use crate::api::{
14-
EncoderConfig, EncoderStatus, FrameType, Opaque, Packet, T35,
14+
EncoderConfig, EncoderStatus, FrameType, Opaque, Packet, ST2094_40_PREFIX,
15+
T35,
1516
};
1617
use crate::color::ChromaSampling::Cs400;
1718
use crate::cpu_features::CpuFeatureLevel;
@@ -349,14 +350,27 @@ impl<T: Pixel> ContextInner<T> {
349350
}
350351
self.frame_q.insert(input_frameno, frame);
351352

353+
// Update T.35 metadata from encoder config
354+
let maybe_updated_t35_metadata = self.get_maybe_updated_t35_metadata(
355+
input_frameno,
356+
params.as_ref().map(|params| params.t35_metadata.as_ref()),
357+
);
358+
352359
if let Some(params) = params {
353360
if params.frame_type_override == FrameTypeOverride::Key {
354361
self.keyframes_forced.insert(input_frameno);
355362
}
356363
if let Some(op) = params.opaque {
357364
self.opaque_q.insert(input_frameno, op);
358365
}
359-
self.t35_q.insert(input_frameno, params.t35_metadata);
366+
367+
if let Some(new_t35_metadata) = maybe_updated_t35_metadata {
368+
self.t35_q.insert(input_frameno, new_t35_metadata.into_boxed_slice());
369+
} else {
370+
self.t35_q.insert(input_frameno, params.t35_metadata);
371+
}
372+
} else if let Some(new_t35_metadata) = maybe_updated_t35_metadata {
373+
self.t35_q.insert(input_frameno, new_t35_metadata.into_boxed_slice());
360374
}
361375

362376
if !self.needs_more_frame_q_lookahead(self.next_lookahead_frame) {
@@ -1719,4 +1733,51 @@ impl<T: Pixel> ContextInner<T> {
17191733
(prev_keyframe_nframes, prev_keyframe_ntus)
17201734
}
17211735
}
1736+
1737+
/// Updates the T.35 metadata to be added to the frame.
1738+
/// The existing T.35 array may come from `FrameParameters`.
1739+
///
1740+
/// Added from [`EncoderConfig`]:
1741+
/// - HDR10+, ST2094-40 in `hdr10plus_payloads`.
1742+
///
1743+
/// Returns an `Option`, where `None` means the T.35 metadata is unchanged.
1744+
/// Otherwise, the updated T.35 metadata is returned.
1745+
fn get_maybe_updated_t35_metadata(
1746+
&self, input_frameno: u64, maybe_existing_t35_metadata: Option<&[T35]>,
1747+
) -> Option<Vec<T35>> {
1748+
let hdr10plus_payload = self
1749+
.config
1750+
.hdr10plus_payloads
1751+
.as_ref()
1752+
.and_then(|list| list.get(&input_frameno));
1753+
1754+
let update_t35_metadata = hdr10plus_payload.is_some();
1755+
1756+
let mut new_t35_metadata = if update_t35_metadata {
1757+
Some(
1758+
maybe_existing_t35_metadata.map_or_else(Vec::new, |t35| t35.to_vec()),
1759+
)
1760+
} else {
1761+
None
1762+
};
1763+
1764+
if let Some(list) = new_t35_metadata.as_mut() {
1765+
// HDR10+, ST2094-40
1766+
if let Some(payload) = hdr10plus_payload {
1767+
let has_existing_hdr10plus_meta = list.iter().any(|t35| {
1768+
t35.country_code == 0xB5 && t35.data.starts_with(ST2094_40_PREFIX)
1769+
});
1770+
1771+
if !has_existing_hdr10plus_meta {
1772+
list.push(T35 {
1773+
country_code: 0xB5,
1774+
country_code_extension_byte: 0x00,
1775+
data: payload.clone().into_boxed_slice(),
1776+
});
1777+
}
1778+
}
1779+
}
1780+
1781+
new_t35_metadata
1782+
}
17221783
}

src/api/test.rs

+2
Original file line numberDiff line numberDiff line change
@@ -2128,6 +2128,7 @@ fn log_q_exp_overflow() {
21282128
bitrate: 1,
21292129
tune: Tune::Psychovisual,
21302130
film_grain_params: None,
2131+
hdr10plus_payloads: None,
21312132
tile_cols: 0,
21322133
tile_rows: 0,
21332134
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

+8
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 {

src/bin/common.rs

+37
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use once_cell::sync::Lazy;
1717
use rav1e::prelude::*;
1818
use scan_fmt::scan_fmt;
1919

20+
use std::collections::BTreeMap;
2021
use std::fs::File;
2122
use std::io;
2223
use std::io::prelude::*;
@@ -192,6 +193,14 @@ pub struct CliOptions {
192193
help_heading = "ENCODE SETTINGS"
193194
)]
194195
pub film_grain_table: Option<PathBuf>,
196+
/// Uses a HDR10+ metadata JSON file to add as T.35 metadata to the encode.
197+
#[clap(
198+
long,
199+
alias = "dhdr10-info",
200+
value_parser,
201+
help_heading = "ENCODE SETTINGS"
202+
)]
203+
pub hdr10plus_json: Option<PathBuf>,
195204

196205
/// Pixel range
197206
#[clap(long, value_parser, help_heading = "VIDEO METADATA")]
@@ -664,6 +673,34 @@ fn parse_config(matches: &CliOptions) -> Result<EncoderConfig, CliError> {
664673
}
665674
}
666675

676+
if let Some(json_file) = matches.hdr10plus_json.as_ref() {
677+
let contents = std::fs::read_to_string(json_file)
678+
.expect("Failed to read HDR10+ metadata file");
679+
let metadata_root =
680+
hdr10plus::metadata_json::MetadataJsonRoot::parse(&contents)
681+
.expect("Failed to parse HDR10+ metadata");
682+
683+
let hdr10plus_enc_opts = hdr10plus::metadata::Hdr10PlusMetadataEncOpts {
684+
with_country_code: false,
685+
..Default::default()
686+
};
687+
let payloads: BTreeMap<u64, Vec<u8>> = metadata_root
688+
.scene_info
689+
.iter()
690+
.filter_map(|meta| {
691+
hdr10plus::metadata::Hdr10PlusMetadata::try_from(meta)
692+
.and_then(|meta| meta.encode_with_opts(&hdr10plus_enc_opts))
693+
.ok()
694+
})
695+
.zip(0u64..)
696+
.map(|(payload, frame_no)| (frame_no, payload))
697+
.collect();
698+
699+
if !payloads.is_empty() {
700+
cfg.hdr10plus_payloads = Some(payloads);
701+
}
702+
}
703+
667704
if let Some(frame_rate) = matches.frame_rate {
668705
cfg.time_base = Rational::new(matches.time_scale, frame_rate);
669706
}

src/fuzzing.rs

+1
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ impl Arbitrary for ArbitraryEncoder {
257257
switch_frame_interval: u.int_in_range(0..=3)?,
258258
tune: *u.choose(&[Tune::Psnr, Tune::Psychovisual])?,
259259
film_grain_params: None,
260+
hdr10plus_payloads: None,
260261
};
261262

262263
let frame_count =

0 commit comments

Comments
 (0)