Skip to content

Commit 85f734b

Browse files
committed
Implement support for encoding HDR10+ from JSON metadata file
1 parent 73f0b6e commit 85f734b

File tree

5 files changed

+132
-0
lines changed

5 files changed

+132
-0
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ new_debug_unreachable = "1.0.4"
105105
once_cell = "1.13.0"
106106
av1-grain = { version = "0.1.1", features = ["serialize"] }
107107
serde-big-array = { version = "0.4.1", optional = true }
108+
hdr10plus = { version = "1.1.1", features = ["json"] }
108109

109110
[dependencies.image]
110111
version = "0.23"

src/api/config/encoder.rs

+8
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ use crate::api::{Rational, SpeedSettings};
1515
use crate::encoder::Tune;
1616
use crate::serialize::{Deserialize, Serialize};
1717

18+
#[cfg(feature = "unstable")]
19+
use std::collections::BTreeMap;
20+
1821
use std::fmt;
1922

2023
// We add 1 to rdo_lookahead_frames in a bunch of places.
@@ -85,6 +88,9 @@ pub struct EncoderConfig {
8588
pub tune: Tune,
8689
/// Parameters for grain synthesis.
8790
pub film_grain_params: Option<Vec<GrainTableSegment>>,
91+
/// HDR10+ T.35 metadata payload map, by frame index.
92+
#[cfg(feature = "unstable")]
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.
@@ -162,6 +168,8 @@ impl EncoderConfig {
162168
bitrate: 0,
163169
tune: Tune::default(),
164170
film_grain_params: None,
171+
#[cfg(feature = "unstable")]
172+
hdr10plus_payloads: None,
165173
tile_cols: 0,
166174
tile_rows: 0,
167175
tiles: 0,

src/api/internal.rs

+76
Original file line numberDiff line numberDiff line change
@@ -349,14 +349,35 @@ impl<T: Pixel> ContextInner<T> {
349349
}
350350
self.frame_q.insert(input_frameno, frame);
351351

352+
#[cfg(feature = "unstable")]
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
}
366+
367+
#[cfg(not(feature = "unstable"))]
359368
self.t35_q.insert(input_frameno, params.t35_metadata);
369+
370+
#[cfg(feature = "unstable")]
371+
if let Some(new_t35_metadata) = maybe_updated_t35_metadata {
372+
self.t35_q.insert(input_frameno, new_t35_metadata.into_boxed_slice());
373+
} else {
374+
self.t35_q.insert(input_frameno, params.t35_metadata);
375+
}
376+
} else {
377+
#[cfg(feature = "unstable")]
378+
if let Some(new_t35_metadata) = maybe_updated_t35_metadata {
379+
self.t35_q.insert(input_frameno, new_t35_metadata.into_boxed_slice());
380+
}
360381
}
361382

362383
if !self.needs_more_frame_q_lookahead(self.next_lookahead_frame) {
@@ -1688,4 +1709,59 @@ impl<T: Pixel> ContextInner<T> {
16881709
(prev_keyframe_nframes, prev_keyframe_ntus)
16891710
}
16901711
}
1712+
1713+
#[cfg(feature = "unstable")]
1714+
/// Updates the T.35 metadata to be added to the frame.
1715+
/// The existing T.35 array may come from `FrameParameters`.
1716+
/// New metadata is added from the encoder config:
1717+
/// - HDR10+, ST2094-40 in `EncoderConfig.hdr10plus_payloads`
1718+
///
1719+
/// Returns an `Option`, where `None` means the T.35 metadata is unchanged.
1720+
/// Otherwise, the updated T.35 metadata array is returned.
1721+
fn get_maybe_updated_t35_metadata(
1722+
&self, input_frameno: u64, maybe_existing_t35_metadata: Option<&[T35]>,
1723+
) -> Option<Vec<T35>> {
1724+
let hdr10plus_payload = self
1725+
.config
1726+
.hdr10plus_payloads
1727+
.as_ref()
1728+
.and_then(|list| list.get(&input_frameno));
1729+
1730+
let update_t35_metadata = hdr10plus_payload.is_some();
1731+
1732+
let mut new_t35_metadata = if update_t35_metadata {
1733+
Some(
1734+
maybe_existing_t35_metadata.map_or_else(Vec::new, |t35| t35.to_vec()),
1735+
)
1736+
} else {
1737+
None
1738+
};
1739+
1740+
if let Some(list) = new_t35_metadata.as_mut() {
1741+
// HDR10+, ST2094-40
1742+
if let Some(payload) = hdr10plus_payload {
1743+
// FIXME: Make const
1744+
let st2094_40_needle = &[
1745+
0x00, 0x03C, // Samsung Electronics America
1746+
0x00, 0x01, // ST-2094-40
1747+
0x04, // application_identifier = 4
1748+
0x01, // application_mode =1
1749+
];
1750+
1751+
let has_existing_hdr10plus_meta = list.iter().any(|t35| {
1752+
t35.country_code == 0xB5 && t35.data.starts_with(st2094_40_needle)
1753+
});
1754+
1755+
if !has_existing_hdr10plus_meta {
1756+
list.push(T35 {
1757+
country_code: 0xB5,
1758+
country_code_extension_byte: 0x00,
1759+
data: payload.clone().into_boxed_slice(),
1760+
});
1761+
}
1762+
}
1763+
}
1764+
1765+
new_t35_metadata
1766+
}
16911767
}

src/api/test.rs

+4
Original file line numberDiff line numberDiff line change
@@ -2128,6 +2128,8 @@ fn log_q_exp_overflow() {
21282128
bitrate: 1,
21292129
tune: Tune::Psychovisual,
21302130
film_grain_params: None,
2131+
#[cfg(feature = "unstable")]
2132+
hdr10plus_payloads: None,
21312133
tile_cols: 0,
21322134
tile_rows: 0,
21332135
tiles: 0,
@@ -2205,6 +2207,8 @@ fn guess_frame_subtypes_assert() {
22052207
bitrate: 16384,
22062208
tune: Tune::Psychovisual,
22072209
film_grain_params: None,
2210+
#[cfg(feature = "unstable")]
2211+
hdr10plus_payloads: None,
22082212
tile_cols: 0,
22092213
tile_rows: 0,
22102214
tiles: 0,

src/bin/common.rs

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

20+
#[cfg(feature = "unstable")]
21+
use std::collections::BTreeMap;
22+
2023
use std::fs::File;
2124
use std::io;
2225
use std::io::prelude::*;
@@ -203,6 +206,15 @@ pub struct CliOptions {
203206
help_heading = "ENCODE SETTINGS"
204207
)]
205208
pub film_grain_table: Option<PathBuf>,
209+
/// Uses a HDR10+ metadata JSON file to add as T.35 metadata to the encode.
210+
#[cfg(feature = "unstable")]
211+
#[clap(
212+
long,
213+
alias = "dhdr10-info",
214+
value_parser,
215+
help_heading = "ENCODE SETTINGS"
216+
)]
217+
pub hdr10plus_json: Option<PathBuf>,
206218

207219
/// Pixel range
208220
#[clap(long, value_parser, help_heading = "VIDEO METADATA")]
@@ -688,11 +700,42 @@ fn parse_config(matches: &CliOptions) -> Result<EncoderConfig, CliError> {
688700
.expect("Failed to read film grain table file");
689701
let table = av1_grain::parse_grain_table(&contents)
690702
.expect("Failed to parse film grain table");
703+
691704
if !table.is_empty() {
692705
cfg.film_grain_params = Some(table);
693706
}
694707
}
695708

709+
#[cfg(feature = "unstable")]
710+
if let Some(json_file) = matches.hdr10plus_json.as_ref() {
711+
let contents = std::fs::read_to_string(json_file)
712+
.expect("Failed to read HDR10+ metadata file");
713+
let metadata_root =
714+
hdr10plus::metadata_json::MetadataJsonRoot::parse(&contents)
715+
.expect("Failed to parse HDR10+ metadata");
716+
717+
let payloads: BTreeMap<u64, Vec<u8>> = metadata_root
718+
.scene_info
719+
.iter()
720+
.filter_map(|meta| {
721+
hdr10plus::metadata::Hdr10PlusMetadata::try_from(meta)
722+
.and_then(|meta| meta.encode(true))
723+
.ok()
724+
.map(|mut bytes| {
725+
// Serialized with country code, which shouldn't be present
726+
bytes.remove(0);
727+
bytes
728+
})
729+
})
730+
.zip(0u64..)
731+
.map(|(payload, frame_no)| (frame_no, payload))
732+
.collect();
733+
734+
if !payloads.is_empty() {
735+
cfg.hdr10plus_payloads = Some(payloads);
736+
}
737+
}
738+
696739
if let Some(frame_rate) = matches.frame_rate {
697740
cfg.time_base = Rational::new(matches.time_scale, frame_rate);
698741
}

0 commit comments

Comments
 (0)