use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom};
use std::path::Path;
use oxideav_core::packet::PacketFlags;
use oxideav_core::{CodecId, CodecParameters, Muxer, Packet, StreamInfo, TimeBase, WriteSeek};
use oxideav_mkv::mux::MkvMuxer;
use crate::disc::{DvdDisc, DvdFileKind};
use crate::error::{Error, Result};
use crate::ifo::{DvdChapter, PgcTime, DVD_SECTOR};
use crate::vob::{
looks_like_nav_pack, DvdSubstream, NavPack, PackHeader, PesPacket, SC_PADDING_STREAM,
SC_PRIVATE_STREAM_1, SC_PRIVATE_STREAM_2, SC_SYSTEM_HEADER,
};
const PES_TIME_BASE: TimeBase = TimeBase::new(1, 90_000);
pub fn pgc_time_to_ns(t: PgcTime) -> u64 {
t.to_nanoseconds()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum DvdMkvStream {
Video,
Ac3(u8),
Dts(u8),
Lpcm(u8),
Subpicture(u8),
}
impl DvdMkvStream {
fn codec_id(self) -> CodecId {
match self {
Self::Video => CodecId::new("mpeg2video"),
Self::Ac3(_) => CodecId::new("ac3"),
Self::Dts(_) => CodecId::new("dts"),
Self::Lpcm(_) => CodecId::new("pcm_s16be"),
Self::Subpicture(_) => CodecId::new("dvd_subtitle"),
}
}
fn media_type(self) -> oxideav_core::MediaType {
match self {
Self::Video => oxideav_core::MediaType::Video,
Self::Ac3(_) | Self::Dts(_) | Self::Lpcm(_) => oxideav_core::MediaType::Audio,
Self::Subpicture(_) => oxideav_core::MediaType::Subtitle,
}
}
fn from_pes(pes: &PesPacket<'_>) -> Option<Self> {
match pes.stream_id {
0xE0..=0xEF => Some(Self::Video),
SC_PRIVATE_STREAM_1 => pes.dvd_substream().map(|s| match s {
DvdSubstream::Ac3(_) => Self::Ac3(s.track()),
DvdSubstream::Dts(_) => Self::Dts(s.track()),
DvdSubstream::Lpcm(_) => Self::Lpcm(s.track()),
DvdSubstream::Subpicture(_) => Self::Subpicture(s.track()),
}),
_ => None,
}
}
}
#[derive(Debug, Default)]
struct TitleProbe {
streams: Vec<DvdMkvStream>,
chapters: Vec<DvdChapter>,
}
pub fn write_title_to_mkv(
disc: &DvdDisc,
title_idx: usize,
image_path: impl AsRef<Path>,
out_path: impl AsRef<Path>,
) -> Result<()> {
let image_path = image_path.as_ref();
let mut reader = BufReader::new(File::open(image_path)?);
let titles = disc.enumerate_titles(&mut reader)?;
let title_entry = titles
.get(title_idx.checked_sub(1).unwrap_or(usize::MAX))
.copied()
.ok_or(Error::NotDvdVideo(
"write_title_to_mkv: title_idx out of range",
))?;
let vts = disc.parse_vts(&mut reader, title_entry.vts_number)?;
let vts_title_idx = usize::from(title_entry.vts_title_number.saturating_sub(1));
let title = vts.titles.get(vts_title_idx).ok_or(Error::NotDvdVideo(
"write_title_to_mkv: VTS_TTN out of range vs VtsIfo.titles",
))?;
let mut probe = TitleProbe {
streams: Vec::new(),
chapters: title.chapters.clone(),
};
for ch in &title.chapters {
let pgc = vts
.pgcs
.get(usize::from(ch.pgcn.saturating_sub(1)))
.ok_or(Error::InvalidUdf(
"write_title_to_mkv: chapter PGCN out of range",
))?;
for cell_no in ch.start_cell..=ch.end_cell {
let pos = pgc
.cell_positions
.get(usize::from(cell_no.saturating_sub(1)))
.ok_or(Error::InvalidUdf(
"write_title_to_mkv: cell index past cell_positions",
))?;
let pb = pgc
.cells
.get(usize::from(cell_no.saturating_sub(1)))
.ok_or(Error::InvalidUdf(
"write_title_to_mkv: cell index past cell_playback",
))?;
let _ = (pos.vob_id, pos.cell_id);
walk_cell_sectors(
disc,
title_entry.vts_number,
&mut reader,
pb.first_vobu_start_sector,
pb.last_vobu_end_sector,
|pes| {
if let Some(s) = DvdMkvStream::from_pes(&pes) {
if !probe.streams.contains(&s) {
probe.streams.push(s);
}
}
Ok(())
},
)?;
}
}
if probe.streams.is_empty() {
return Err(Error::NotDvdVideo(
"write_title_to_mkv: no playable streams found in title",
));
}
probe.streams.sort();
let stream_infos: Vec<StreamInfo> = probe
.streams
.iter()
.enumerate()
.map(|(i, s)| StreamInfo {
index: i as u32,
time_base: PES_TIME_BASE,
duration: None,
start_time: None,
params: codec_parameters_for(*s),
})
.collect();
let out_file = File::create(out_path.as_ref())?;
let writer: Box<dyn WriteSeek> = Box::new(BufWriter::new(out_file));
let mut muxer = MkvMuxer::new_matroska(writer, &stream_infos)
.map_err(|e| Error::InvalidUdf(io_static("MKV mux init failed", e.to_string())))?;
let mut acc_ns: u64 = 0;
for (i, ch) in probe.chapters.iter().enumerate() {
let ch_ns = pgc_time_to_ns(ch.playback_time);
let start_ns = acc_ns;
let end_ns = acc_ns.saturating_add(ch_ns);
let next_start = probe
.chapters
.get(i + 1)
.map(|n| {
u64::from(n.number)
})
.map(|_| end_ns);
muxer
.add_chapter(start_ns, next_start, format!("Chapter {}", ch.number))
.map_err(|e| Error::InvalidUdf(io_static("MKV add_chapter failed", e.to_string())))?;
acc_ns = end_ns;
}
muxer
.write_header()
.map_err(|e| Error::InvalidUdf(io_static("MKV write_header failed", e.to_string())))?;
let mut anchor_pts_90khz: Option<u64> = None;
for ch in &title.chapters {
let pgc = &vts.pgcs[usize::from(ch.pgcn.saturating_sub(1))];
for cell_no in ch.start_cell..=ch.end_cell {
let pos = &pgc.cell_positions[usize::from(cell_no.saturating_sub(1))];
let pb = &pgc.cells[usize::from(cell_no.saturating_sub(1))];
let _ = (pos.vob_id, pos.cell_id);
walk_cell_sectors(
disc,
title_entry.vts_number,
&mut reader,
pb.first_vobu_start_sector,
pb.last_vobu_end_sector,
|pes| {
let stream = match DvdMkvStream::from_pes(&pes) {
Some(s) => s,
None => return Ok(()),
};
let stream_idx =
probe
.streams
.iter()
.position(|x| *x == stream)
.ok_or(Error::InvalidUdf(
"write_title_to_mkv: probe missed a substream the mux pass found",
))? as u32;
let raw_pts = pes.pts;
let pts = match raw_pts {
Some(p) => {
let anchor = *anchor_pts_90khz.get_or_insert(p);
Some(p.saturating_sub(anchor) as i64)
}
None => None,
};
let mut data = pes.payload.to_vec();
if pes.stream_id == SC_PRIVATE_STREAM_1 && !data.is_empty() {
let is_lpcm = matches!(stream, DvdMkvStream::Lpcm(_));
data.remove(0);
if is_lpcm && data.len() >= crate::LPCM_HEADER_LEN - 1 {
data.drain(0..crate::LPCM_HEADER_LEN - 1);
}
}
let mut flags = PacketFlags::default();
if matches!(stream, DvdMkvStream::Video)
&& data.len() >= 4
&& data[0..4] == [0x00, 0x00, 0x01, 0xB3]
{
flags.keyframe = true;
}
if matches!(stream.media_type(), oxideav_core::MediaType::Audio)
|| matches!(stream.media_type(), oxideav_core::MediaType::Subtitle)
{
flags.keyframe = true;
}
let packet = Packet {
stream_index: stream_idx,
time_base: PES_TIME_BASE,
pts,
dts: pes.dts.map(|d| {
let anchor = *anchor_pts_90khz.get_or_insert(d);
d.saturating_sub(anchor) as i64
}),
duration: None,
flags,
data,
};
muxer.write_packet(&packet).map_err(|e| {
Error::InvalidUdf(io_static("MKV write_packet failed", e.to_string()))
})
},
)?;
}
}
muxer
.write_trailer()
.map_err(|e| Error::InvalidUdf(io_static("MKV write_trailer failed", e.to_string())))?;
Ok(())
}
fn codec_parameters_for(stream: DvdMkvStream) -> CodecParameters {
let id = stream.codec_id();
match stream {
DvdMkvStream::Video => {
let mut p = CodecParameters::video(id);
p.width = Some(720);
p.height = Some(480);
p
}
DvdMkvStream::Ac3(_) | DvdMkvStream::Dts(_) | DvdMkvStream::Lpcm(_) => {
let mut p = CodecParameters::audio(id);
p.sample_rate = Some(48_000);
p.channels = Some(2);
p
}
DvdMkvStream::Subpicture(_) => CodecParameters::subtitle(id),
}
}
fn walk_cell_sectors<R, F>(
disc: &DvdDisc,
vts_number: u8,
reader: &mut R,
first_sector: u32,
last_sector: u32,
mut f: F,
) -> Result<()>
where
R: Read + Seek,
F: FnMut(PesPacket<'_>) -> Result<()>,
{
let base_lba = disc
.video_ts_files
.iter()
.find(|f| {
matches!(
f.kind,
DvdFileKind::VtsTitle { ts, vob: 1 } if ts == vts_number
)
})
.map(|f| f.lba)
.ok_or(Error::NotDvdVideo(
"walk_cell_sectors: title set has no VTS_xx_1.VOB",
))?;
if last_sector < first_sector {
return Ok(());
}
let count = last_sector - first_sector + 1;
let mut buf = vec![0u8; DVD_SECTOR];
for s in 0..count {
let abs_lba = base_lba.saturating_add(first_sector).saturating_add(s);
reader.seek(SeekFrom::Start(u64::from(abs_lba) * DVD_SECTOR as u64))?;
reader.read_exact(&mut buf)?;
if looks_like_nav_pack(&buf) {
let _ = NavPack::parse(&buf)?;
continue;
}
let pack = PackHeader::parse(&buf)?;
let mut cursor = PackHeader::SIZE + pack.stuffing_bytes as usize;
while cursor + 6 <= buf.len() {
if buf[cursor..cursor + 4] == [0x00, 0x00, 0x01, SC_SYSTEM_HEADER] {
let len = ((buf[cursor + 4] as usize) << 8) | buf[cursor + 5] as usize;
cursor += 6 + len;
continue;
}
if buf[cursor..cursor + 4] == [0x00, 0x00, 0x01, SC_PADDING_STREAM] {
let len = ((buf[cursor + 4] as usize) << 8) | buf[cursor + 5] as usize;
cursor += 6 + len;
continue;
}
if buf[cursor..cursor + 4] == [0x00, 0x00, 0x01, SC_PRIVATE_STREAM_2] {
let len = ((buf[cursor + 4] as usize) << 8) | buf[cursor + 5] as usize;
cursor += 6 + len;
continue;
}
if buf[cursor..cursor + 3] != [0x00, 0x00, 0x01] {
break;
}
let pes = PesPacket::parse(&buf[cursor..])?;
cursor += pes.wire_size;
f(pes)?;
}
}
Ok(())
}
fn io_static(label: &'static str, _payload: String) -> &'static str {
label
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ifo::FrameRate;
#[test]
fn pgc_time_ns_ntsc_30() {
let t = PgcTime {
hours: 0,
minutes: 0,
seconds: 1,
frames: 15,
frame_rate: FrameRate::Ntsc30,
};
assert_eq!(pgc_time_to_ns(t), 1_500_000_000);
}
#[test]
fn pgc_time_ns_pal_25() {
let t = PgcTime {
hours: 0,
minutes: 0,
seconds: 0,
frames: 5,
frame_rate: FrameRate::Pal25,
};
assert_eq!(pgc_time_to_ns(t), 200_000_000);
}
#[test]
fn pgc_time_ns_illegal_drops_frames() {
let t = PgcTime {
hours: 0,
minutes: 0,
seconds: 3,
frames: 12,
frame_rate: FrameRate::Illegal,
};
assert_eq!(pgc_time_to_ns(t), 3_000_000_000);
}
#[test]
fn pgc_time_ns_hour_boundary() {
let t = PgcTime {
hours: 1,
minutes: 0,
seconds: 0,
frames: 0,
frame_rate: FrameRate::Ntsc30,
};
assert_eq!(pgc_time_to_ns(t), 3_600_000_000_000);
}
#[test]
fn dvd_mkv_stream_codec_id_mapping() {
assert_eq!(DvdMkvStream::Video.codec_id().as_str(), "mpeg2video");
assert_eq!(DvdMkvStream::Ac3(0).codec_id().as_str(), "ac3");
assert_eq!(DvdMkvStream::Dts(2).codec_id().as_str(), "dts");
assert_eq!(DvdMkvStream::Lpcm(0).codec_id().as_str(), "pcm_s16be");
assert_eq!(
DvdMkvStream::Subpicture(1).codec_id().as_str(),
"dvd_subtitle"
);
}
#[test]
fn dvd_mkv_stream_media_types() {
use oxideav_core::MediaType;
assert_eq!(DvdMkvStream::Video.media_type(), MediaType::Video);
assert_eq!(DvdMkvStream::Ac3(7).media_type(), MediaType::Audio);
assert_eq!(DvdMkvStream::Dts(0).media_type(), MediaType::Audio);
assert_eq!(DvdMkvStream::Lpcm(0).media_type(), MediaType::Audio);
assert_eq!(
DvdMkvStream::Subpicture(0).media_type(),
MediaType::Subtitle
);
}
#[test]
fn dvd_mkv_stream_sort_order() {
let mut v = [
DvdMkvStream::Subpicture(0),
DvdMkvStream::Ac3(2),
DvdMkvStream::Video,
DvdMkvStream::Dts(0),
DvdMkvStream::Lpcm(0),
];
v.sort();
assert_eq!(v[0], DvdMkvStream::Video);
assert!(matches!(v[1], DvdMkvStream::Ac3(_)));
assert!(matches!(v[v.len() - 1], DvdMkvStream::Subpicture(_)));
}
}