use crate::disc::{ColorSpace, DiscTitle, FrameRate, Stream as DiscStream, VideoStream};
use crate::mux::codec::PictureInfo;
use crate::mux::codec::coding::{CodingType, FieldOrder};
use crate::pes::{PesFrame, SourcePos};
pub const FVI_FORMAT: &str = "freemkv/video-index";
pub const FVI_VERSION: u32 = 1;
pub const FVI_GENERATOR: &str = concat!("freemkv/", env!("FREEMKV_VERSION"), env!("GIT_SUFFIX"));
pub const FVI_TIMESCALE: u64 = 1_000_000_000;
pub const FVI_SECTOR_SIZE: u32 = crate::consts::SECTOR_BYTES as u32;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Colour {
pub primaries: u8,
pub transfer: u8,
pub matrix: u8,
pub full_range: bool,
}
impl Colour {
pub fn from_video(v: &VideoStream) -> Self {
let (matrix, transfer, primaries, range) = crate::mux::mkv::cicp_for_video(v);
Self {
primaries,
transfer,
matrix,
full_range: range == 2,
}
}
pub fn from_color_space(cs: ColorSpace) -> Self {
let (p, t, m) = match cs {
ColorSpace::Bt709 => (1, 1, 1),
ColorSpace::Bt2020 => (9, 14, 9), ColorSpace::Bt470bg => (5, 5, 5),
ColorSpace::Smpte170m => (6, 6, 6),
ColorSpace::Unknown => (2, 2, 2), };
Self {
primaries: p,
transfer: t,
matrix: m,
full_range: false,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Scan {
Progressive,
Interlaced,
}
impl Scan {
pub fn as_str(self) -> &'static str {
match self {
Scan::Progressive => "progressive",
Scan::Interlaced => "interlaced",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Medium {
Disc,
Iso,
#[default]
File,
Stream,
}
impl Medium {
pub fn as_str(self) -> &'static str {
match self {
Medium::Disc => "disc",
Medium::Iso => "iso",
Medium::File => "file",
Medium::Stream => "stream",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct SourceInfo {
pub medium: Medium,
pub path: String,
pub title: usize,
pub playlist: String,
pub volume_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StreamInfo {
pub codec: &'static str,
pub width: u32,
pub height: u32,
pub dar: (u32, u32),
pub frame_rate: (u32, u32),
pub scan: Scan,
pub colour: Colour,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MapHeader {
pub stream: StreamInfo,
pub source: SourceInfo,
pub picture_count: Option<u64>,
}
fn fvi_codec_id(codec: crate::disc::Codec) -> &'static str {
use crate::disc::Codec;
match codec {
Codec::Mpeg2 => "mpeg2video",
Codec::Mpeg1 => "mpeg1video",
Codec::H264 => "h264",
Codec::Hevc => "hevc",
Codec::Vc1 => "vc1",
other => other.id(),
}
}
impl MapHeader {
pub fn from_title(title: &DiscTitle, source: SourceInfo) -> Self {
let video: Option<&VideoStream> = title.streams.iter().find_map(|s| match s {
DiscStream::Video(v) => Some(v),
_ => None,
});
let stream = match video {
Some(v) => {
let (width, height) = v.resolution.pixels();
StreamInfo {
codec: fvi_codec_id(v.codec),
width,
height,
dar: display_aspect_ratio(v, width, height),
frame_rate: v.frame_rate.as_fraction(),
scan: if v.resolution.is_interlaced() {
Scan::Interlaced
} else {
Scan::Progressive
},
colour: Colour::from_video(v),
}
}
None => StreamInfo {
codec: "unknown",
width: 0,
height: 0,
dar: (0, 1),
frame_rate: (0, 1),
scan: Scan::Progressive,
colour: Colour::from_color_space(ColorSpace::Unknown),
},
};
Self {
stream,
source,
picture_count: None,
}
}
}
fn display_aspect_ratio(v: &VideoStream, w: u32, h: u32) -> (u32, u32) {
match v.display_aspect {
Some((a, b)) if b != 0 => (a, b),
_ if h != 0 => (w, h),
_ => (0, 1),
}
}
#[allow(dead_code)]
fn frame_rate_fraction(fr: FrameRate) -> (u32, u32) {
fr.as_fraction()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PictureRecord {
pub n: u64,
pub coding: Option<PictureInfo>,
pub keyframe: bool,
pub pts_ns: Option<i64>,
pub source: Option<SourcePos>,
}
pub fn type_label(coding: Option<PictureInfo>, keyframe: bool) -> &'static str {
match coding {
Some(c) => match c.coding_type() {
CodingType::I => "I",
CodingType::P => "P",
CodingType::B => "B",
},
None => {
if keyframe {
"I"
} else {
"P"
}
}
}
}
pub fn field_order_label(coding: Option<PictureInfo>) -> Option<&'static str> {
match coding?.field_order()? {
FieldOrder::Tff => Some("tff"),
FieldOrder::Bff => Some("bff"),
FieldOrder::Progressive => Some("progressive"),
}
}
pub fn is_random_access(coding: Option<PictureInfo>, keyframe: bool) -> bool {
let _ = coding;
keyframe
}
#[derive(Clone, Debug)]
pub struct VideoMap {
header: MapHeader,
records: Vec<PictureRecord>,
}
impl VideoMap {
pub fn new(title: &DiscTitle, source: SourceInfo) -> Self {
Self {
header: MapHeader::from_title(title, source),
records: Vec::new(),
}
}
pub fn header(&self) -> &MapHeader {
&self.header
}
pub fn records(&self) -> &[PictureRecord] {
&self.records
}
pub fn append_frame(&mut self, frame: &PesFrame) -> &PictureRecord {
let rec = PictureRecord {
n: self.records.len() as u64,
coding: frame.coding,
keyframe: frame.keyframe,
pts_ns: Some(frame.pts),
source: frame.source,
};
self.records.push(rec);
self.records.last().expect("just pushed")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::disc::{
Codec, ColorSpace, ContentFormat, FrameRate, HdrFormat, Resolution, VideoStream,
};
fn video_title(codec: Codec, res: Resolution, fr: FrameRate, cs: ColorSpace) -> DiscTitle {
let mut t = DiscTitle::empty();
t.streams = vec![DiscStream::Video(VideoStream {
pid: 0x1011,
codec,
resolution: res,
frame_rate: fr,
hdr: HdrFormat::Sdr,
color_space: cs,
display_aspect: None,
secondary: false,
label: String::new(),
measured_cicp: None,
})];
t.content_format = ContentFormat::BdTs;
t
}
fn src(medium: Medium, path: &str, title: usize) -> SourceInfo {
SourceInfo {
medium,
path: path.to_string(),
title,
..Default::default()
}
}
fn vframe(coding: Option<PictureInfo>, pts: i64, source: Option<SourcePos>) -> PesFrame {
let keyframe = coding.map(|c| c.keyframe()).unwrap_or(false);
PesFrame {
track: 0,
pts,
keyframe,
data: vec![0u8; 4],
duration_ns: None,
source,
coding,
}
}
use crate::mux::codec::coding::Mpeg2Coding;
fn mpeg2_pic(ct: CodingType) -> PictureInfo {
PictureInfo::mpeg2(
ct,
Mpeg2Coding {
top_field_first: true,
repeat_first_field: false,
progressive_frame: false,
progressive_sequence: false,
frame_picture: true,
},
)
}
fn i_picture() -> PictureInfo {
mpeg2_pic(CodingType::I)
}
#[test]
fn colour_maps_cicp_code_points() {
assert_eq!(
Colour::from_color_space(ColorSpace::Bt709),
Colour {
primaries: 1,
transfer: 1,
matrix: 1,
full_range: false
}
);
assert_eq!(
Colour::from_color_space(ColorSpace::Bt2020),
Colour {
primaries: 9,
transfer: 14,
matrix: 9,
full_range: false
}
);
assert_eq!(Colour::from_color_space(ColorSpace::Unknown).primaries, 2);
}
#[test]
fn fvi_colour_follows_hdr_and_measured_cicp() {
use crate::disc::MeasuredCicp;
let mk = |hdr: HdrFormat, cs: ColorSpace, cicp: Option<MeasuredCicp>| VideoStream {
pid: 0x1011,
codec: Codec::Hevc,
resolution: Resolution::R2160p,
frame_rate: FrameRate::F23_976,
hdr,
color_space: cs,
display_aspect: None,
secondary: false,
label: String::new(),
measured_cicp: cicp,
};
let c = Colour::from_video(&mk(HdrFormat::Hdr10, ColorSpace::Bt2020, None));
assert_eq!(
c,
Colour {
primaries: 9,
transfer: 16, matrix: 9,
full_range: false,
}
);
let c = Colour::from_video(&mk(HdrFormat::Hlg, ColorSpace::Bt2020, None));
assert_eq!(c.transfer, 18, "HLG transfer must be 18");
let measured = MeasuredCicp {
matrix: 9,
transfer: 16,
primaries: 9,
range: 2,
};
let c = Colour::from_video(&mk(HdrFormat::Sdr, ColorSpace::Bt709, Some(measured)));
assert_eq!(
c,
Colour {
primaries: 9,
transfer: 16,
matrix: 9,
full_range: true,
},
"measured CICP must override the coarse color_space enum"
);
let c = Colour::from_video(&mk(HdrFormat::Sdr, ColorSpace::Unknown, None));
assert_eq!(
c,
Colour {
primaries: 2,
transfer: 2,
matrix: 2,
full_range: false,
},
"Unknown colorimetry must emit CICP 'unspecified' (2), not 0"
);
}
#[test]
fn type_label_full_and_codec_agnostic_fallback() {
let mk = |ct| Some(mpeg2_pic(ct));
assert_eq!(type_label(mk(CodingType::I), false), "I");
assert_eq!(type_label(mk(CodingType::P), false), "P");
assert_eq!(type_label(mk(CodingType::B), false), "B");
assert_eq!(
type_label(Some(PictureInfo::coding_type_only(CodingType::B)), false),
"B"
);
assert_eq!(type_label(None, true), "I");
assert_eq!(type_label(None, false), "P");
}
#[test]
fn field_order_label_omitted_when_unmeasured() {
assert_eq!(field_order_label(Some(i_picture())), Some("tff"));
let prog = PictureInfo::mpeg2(
CodingType::I,
Mpeg2Coding {
top_field_first: true,
repeat_first_field: false,
progressive_frame: true,
progressive_sequence: false,
frame_picture: true,
},
);
assert_eq!(field_order_label(Some(prog)), Some("progressive"));
assert_eq!(
field_order_label(Some(PictureInfo::coding_type_only(CodingType::I))),
None
);
assert_eq!(field_order_label(None), None);
}
#[test]
fn is_random_access_codec_agnostic() {
assert!(is_random_access(Some(i_picture()), true));
assert!(!is_random_access(Some(i_picture()), false));
assert!(!is_random_access(Some(mpeg2_pic(CodingType::P)), false));
assert!(is_random_access(None, true));
assert!(!is_random_access(None, false));
}
#[test]
fn fvi_codec_ids_use_bitstream_names() {
assert_eq!(fvi_codec_id(Codec::Mpeg2), "mpeg2video");
assert_eq!(fvi_codec_id(Codec::Mpeg1), "mpeg1video");
assert_eq!(fvi_codec_id(Codec::H264), "h264");
assert_eq!(fvi_codec_id(Codec::Hevc), "hevc");
assert_eq!(fvi_codec_id(Codec::Vc1), "vc1");
}
#[test]
fn header_from_title_pulls_video_facts() {
let t = video_title(
Codec::Mpeg2,
Resolution::R576i,
FrameRate::F25,
ColorSpace::Bt470bg,
);
let h = MapHeader::from_title(&t, src(Medium::Iso, "iso://x.iso", 2));
assert_eq!(h.stream.codec, "mpeg2video");
assert_eq!((h.stream.width, h.stream.height), (720, 576));
assert_eq!(h.stream.dar, (720, 576)); assert_eq!(h.stream.frame_rate, (25, 1));
assert_eq!(h.stream.scan, Scan::Interlaced);
assert_eq!(h.stream.colour.matrix, 5);
assert_eq!(h.source.path, "iso://x.iso");
assert_eq!(h.source.title, 2);
assert_eq!(h.source.medium, Medium::Iso);
}
#[test]
fn header_audio_only_title_is_neutral_not_panic() {
let t = DiscTitle::empty();
let h = MapHeader::from_title(&t, SourceInfo::default());
assert_eq!(h.stream.codec, "unknown");
assert_eq!((h.stream.width, h.stream.height), (0, 0));
}
#[test]
fn append_frame_numbers_records_in_order() {
let t = video_title(
Codec::Mpeg2,
Resolution::R1080p,
FrameRate::F23_976,
ColorSpace::Bt709,
);
let mut map = VideoMap::new(&t, SourceInfo::default());
map.append_frame(&vframe(
Some(i_picture()),
0,
Some(SourcePos::at_byte(2048)),
));
map.append_frame(&vframe(
Some(mpeg2_pic(CodingType::B)),
42,
Some(SourcePos::at_byte(4096)),
));
assert_eq!(map.records().len(), 2);
assert_eq!(map.records()[0].n, 0);
assert_eq!(map.records()[1].n, 1);
assert_eq!(map.records()[0].source.unwrap().sector, 1);
assert_eq!(map.records()[1].pts_ns, Some(42));
}
}