use std::{path::Path, process::Command};
use anyhow::{bail, Result};
use nom::{
bytes::complete::tag,
character::complete::{char, digit1},
combinator::map,
sequence::{delimited, preceded, separated_pair},
IResult,
};
use crate::{
metadata::{BasicMetadata, ChromaLocation, ColorCoordinates, HdrMetadata, Metadata},
values::{
parse_color_primaries, parse_color_range, parse_matrix_coefficients,
parse_transfer_characteristics,
},
};
pub fn parse_mkvinfo(input: &Path) -> Result<Metadata> {
let result = Command::new("mkvinfo").arg(input).output()?;
let output = String::from_utf8_lossy(&result.stdout);
let mut basic = BasicMetadata::default();
let mut has_basic = false;
let mut hdr = HdrMetadata::default();
let mut has_hdr = false;
let mut chroma_location = (0, 0);
for line in output.lines() {
if line.contains("Colour matrix coefficients:") {
basic.matrix = line.split_once(": ").unwrap().1.parse()?;
has_basic = true;
continue;
}
if line.contains("Colour range:") {
basic.range = line.split_once(": ").unwrap().1.parse()?;
has_basic = true;
continue;
}
if line.contains("Colour transfer:") {
basic.transfer = line.split_once(": ").unwrap().1.parse()?;
has_basic = true;
continue;
}
if line.contains("Colour primaries:") {
basic.primaries = line.split_once(": ").unwrap().1.parse()?;
has_basic = true;
continue;
}
if line.contains("Horizontal chroma siting:") {
chroma_location.0 = line.split_once(": ").unwrap().1.parse()?;
has_basic = true;
continue;
}
if line.contains("Vertical chroma siting:") {
chroma_location.1 = line.split_once(": ").unwrap().1.parse()?;
has_basic = true;
continue;
}
if line.contains("Video colour mastering metadata") {
has_hdr = true;
continue;
}
if line.contains("Maximum content light:") {
hdr.max_content_light = line.split_once(": ").unwrap().1.parse()?;
continue;
}
if line.contains("Maximum frame light:") {
hdr.max_frame_light = line.split_once(": ").unwrap().1.parse()?;
continue;
}
if line.contains("Red colour coordinate x:") {
hdr.color_coords = Some(ColorCoordinates::default());
hdr.color_coords.as_mut().unwrap().red.0 = line.split_once(": ").unwrap().1.parse()?;
continue;
}
if line.contains("Red colour coordinate y:") {
hdr.color_coords.as_mut().unwrap().red.1 = line.split_once(": ").unwrap().1.parse()?;
continue;
}
if line.contains("Green colour coordinate x:") {
hdr.color_coords.as_mut().unwrap().green.0 =
line.split_once(": ").unwrap().1.parse()?;
continue;
}
if line.contains("Green colour coordinate y:") {
hdr.color_coords.as_mut().unwrap().green.1 =
line.split_once(": ").unwrap().1.parse()?;
continue;
}
if line.contains("Blue colour coordinate x:") {
hdr.color_coords.as_mut().unwrap().blue.0 = line.split_once(": ").unwrap().1.parse()?;
continue;
}
if line.contains("Blue colour coordinate y:") {
hdr.color_coords.as_mut().unwrap().blue.1 = line.split_once(": ").unwrap().1.parse()?;
continue;
}
if line.contains("White colour coordinate x:") {
hdr.color_coords.as_mut().unwrap().white.0 =
line.split_once(": ").unwrap().1.parse()?;
continue;
}
if line.contains("White colour coordinate y:") {
hdr.color_coords.as_mut().unwrap().white.1 =
line.split_once(": ").unwrap().1.parse()?;
continue;
}
if line.contains("Maximum luminance:") {
hdr.max_luma = line.split_once(": ").unwrap().1.parse()?;
continue;
}
if line.contains("Minimum luminance:") {
hdr.min_luma = line.split_once(": ").unwrap().1.parse()?;
continue;
}
}
if has_basic {
basic.chroma_location = match chroma_location {
(0, 0) => {
match basic.range {
0 => ChromaLocation::Center,
1 => ChromaLocation::Left,
_ => bail!("Unrecognized color range"),
}
}
(0, 1) => match basic.range {
0 => ChromaLocation::Top,
1 => ChromaLocation::TopLeft,
_ => bail!("Unrecognized color range"),
},
(0, 2) => match basic.range {
0 => ChromaLocation::Center,
1 => ChromaLocation::Left,
_ => bail!("Unrecognized color range"),
},
(0, 3) => match basic.range {
0 => ChromaLocation::Bottom,
1 => ChromaLocation::BottomLeft,
_ => bail!("Unrecognized color range"),
},
(1, 0) => ChromaLocation::Left,
(1, 1) => ChromaLocation::TopLeft,
(1, 2) => ChromaLocation::Left,
(1, 3) => ChromaLocation::BottomLeft,
(2, 0) => ChromaLocation::Center,
(2, 1) => ChromaLocation::Top,
(2, 2) => ChromaLocation::Center,
(2, 3) => ChromaLocation::Bottom,
(x, y) => bail!("Unrecognized chroma location values: {x}, {y}"),
}
}
Ok(Metadata {
basic: if has_basic { Some(basic) } else { None },
hdr: if has_hdr { Some(hdr) } else { None },
})
}
pub fn parse_mediainfo(input: &Path) -> Result<Metadata> {
let result = Command::new("mediainfo").arg(input).output()?;
let output = String::from_utf8_lossy(&result.stdout);
let mut basic = BasicMetadata::default();
let mut has_basic = false;
let mut hdr = HdrMetadata::default();
let mut has_hdr = false;
for line in output.lines() {
if line.contains("Matrix coefficients") {
basic.matrix = parse_matrix_coefficients(line.split_once(": ").unwrap().1);
has_basic = true;
continue;
}
if line.contains("Color range") {
basic.range = parse_color_range(line.split_once(": ").unwrap().1);
has_basic = true;
continue;
}
if line.contains("Transfer characteristics") {
basic.transfer = parse_transfer_characteristics(line.split_once(": ").unwrap().1);
has_basic = true;
continue;
}
if line.contains("Color primaries") {
basic.primaries = parse_color_primaries(line.split_once(": ").unwrap().1);
has_basic = true;
continue;
}
if line.contains("Mastering display color primaries") {
has_hdr = true;
continue;
}
if line.contains("Maximum Content Light Level") {
hdr.max_content_light = line
.split_once(": ")
.unwrap()
.1
.trim_end_matches(" cd/m2")
.replace(' ', "")
.parse()?;
continue;
}
if line.contains("Maximum Frame-Average Light Level") {
hdr.max_frame_light = line
.split_once(": ")
.unwrap()
.1
.trim_end_matches(" cd/m2")
.replace(' ', "")
.parse()?;
continue;
}
if line.contains("Mastering display luminance") {
let output = line.split_once(": ").unwrap().1;
let (min, max) = output.split_once(", ").unwrap();
hdr.min_luma = min
.trim_start_matches("min: ")
.trim_end_matches(" cd/m2")
.parse()?;
hdr.max_luma = max
.trim_start_matches("max: ")
.trim_end_matches(" cd/m2")
.parse()?;
continue;
}
if line.contains("Encoding settings") && line.contains("master-display") {
let settings = line.split_once(": ").unwrap().1;
hdr.color_coords = Some(parse_x265_settings(settings)?);
}
}
Ok(Metadata {
basic: if has_basic { Some(basic) } else { None },
hdr: if has_hdr { Some(hdr) } else { None },
})
}
fn parse_x265_settings(input: &str) -> Result<ColorCoordinates> {
const MASTER_DISPLAY_HEADER: &str = "master-display=";
let header_pos = input
.find(MASTER_DISPLAY_HEADER)
.ok_or_else(|| anyhow::anyhow!("Failed to find master display header"))?;
let input = &input[(header_pos + MASTER_DISPLAY_HEADER.len())..];
let (input, (gx, gy)) = preceded(char('G'), get_coordinate_pair)(input).unwrap();
let (input, (bx, by)) = preceded(char('B'), get_coordinate_pair)(input).unwrap();
let (input, (rx, ry)) = preceded(char('R'), get_coordinate_pair)(input).unwrap();
let (_, (wx, wy)) = preceded(tag("WP"), get_coordinate_pair)(input).unwrap();
Ok(ColorCoordinates {
red: (rx as f64 / 50000., ry as f64 / 50000.),
green: (gx as f64 / 50000., gy as f64 / 50000.),
blue: (bx as f64 / 50000., by as f64 / 50000.),
white: (wx as f64 / 50000., wy as f64 / 50000.),
})
}
fn get_coordinate_pair(input: &str) -> IResult<&str, (u32, u32)> {
map(
delimited(
char('('),
separated_pair(digit1, char(','), digit1),
char(')'),
),
|(x, y): (&str, &str)| (x.parse::<u32>().unwrap(), y.parse::<u32>().unwrap()),
)(input)
}
pub fn parse_ffprobe(input: &Path) -> Result<Option<HdrMetadata>> {
let result = Command::new("ffprobe")
.arg("-v")
.arg("quiet")
.arg("-select_streams")
.arg("v:0")
.arg("-show_frames")
.arg("-read_intervals")
.arg("%+#1")
.arg(input)
.output()?;
let output = String::from_utf8_lossy(&result.stdout);
if !(output.contains("side_data_type=Mastering display metadata")
&& output.contains("side_data_type=Content light level metadata"))
{
return Ok(None);
}
let mut hdr = HdrMetadata::default();
for line in output.lines() {
if line.starts_with("red_x=") {
hdr.color_coords = Some(ColorCoordinates::default());
let (num, denom) = line.split_once('=').unwrap().1.split_once('/').unwrap();
hdr.color_coords.as_mut().unwrap().red.0 =
num.parse::<f64>()? / denom.parse::<f64>()?;
continue;
}
if line.starts_with("red_y=") {
let (num, denom) = line.split_once('=').unwrap().1.split_once('/').unwrap();
hdr.color_coords.as_mut().unwrap().red.1 =
num.parse::<f64>()? / denom.parse::<f64>()?;
continue;
}
if line.starts_with("green_x=") {
let (num, denom) = line.split_once('=').unwrap().1.split_once('/').unwrap();
hdr.color_coords.as_mut().unwrap().green.0 =
num.parse::<f64>()? / denom.parse::<f64>()?;
continue;
}
if line.starts_with("green_y=") {
let (num, denom) = line.split_once('=').unwrap().1.split_once('/').unwrap();
hdr.color_coords.as_mut().unwrap().green.1 =
num.parse::<f64>()? / denom.parse::<f64>()?;
continue;
}
if line.starts_with("blue_x=") {
let (num, denom) = line.split_once('=').unwrap().1.split_once('/').unwrap();
hdr.color_coords.as_mut().unwrap().blue.0 =
num.parse::<f64>()? / denom.parse::<f64>()?;
continue;
}
if line.starts_with("blue_y=") {
let (num, denom) = line.split_once('=').unwrap().1.split_once('/').unwrap();
hdr.color_coords.as_mut().unwrap().blue.1 =
num.parse::<f64>()? / denom.parse::<f64>()?;
continue;
}
if line.starts_with("white_point_x=") {
let (num, denom) = line.split_once('=').unwrap().1.split_once('/').unwrap();
hdr.color_coords.as_mut().unwrap().white.0 =
num.parse::<f64>()? / denom.parse::<f64>()?;
continue;
}
if line.starts_with("white_point_y=") {
let (num, denom) = line.split_once('=').unwrap().1.split_once('/').unwrap();
hdr.color_coords.as_mut().unwrap().white.1 =
num.parse::<f64>()? / denom.parse::<f64>()?;
continue;
}
if line.starts_with("min_luminance=") {
let (num, denom) = line.split_once('=').unwrap().1.split_once('/').unwrap();
hdr.min_luma = num.parse::<f64>()? / denom.parse::<f64>()?;
continue;
}
if line.starts_with("max_luminance=") {
let (num, denom) = line.split_once('=').unwrap().1.split_once('/').unwrap();
hdr.max_luma = num.parse::<u32>()? / denom.parse::<u32>()?;
continue;
}
if line.starts_with("max_content=") {
hdr.max_content_light = line.split_once('=').unwrap().1.parse()?;
continue;
}
if line.starts_with("max_average=") {
hdr.max_frame_light = line.split_once('=').unwrap().1.parse()?;
continue;
}
}
Ok(Some(hdr))
}