use std::collections::{BTreeMap, BTreeSet};
use std::io::{BufRead, Write, stdout};
use std::time::Duration;
use anyhow::bail;
use arib::demux::{MediaPacket, Packet, SignalingEvent, TrackType};
use arib::mmt::descriptor::{
AdditionalAribSubtitleInfo, AdditionalDataComponentInfo, Descriptor, SubtitleTimeControlMode,
};
use arib::mmt::message::Message;
use arib::ts::caption::{DataGroup, DataGroupData};
use arib::ts::table::{Table, Tdt, Tot};
use chrono::{NaiveDateTime, TimeDelta};
use clap::Args;
use tracing::warn;
use crate::input::{Format, InputArgs};
#[derive(Clone, Debug, Args)]
pub struct Options {
#[command(flatten)]
input: InputArgs,
#[arg(long)]
raw: bool,
}
pub fn run(options: &Options) -> anyhow::Result<()> {
let input = options.input.open()?;
let mut output = stdout().lock();
match input.format {
Format::Ts if options.raw => bail!("--raw is for MMT only, whose captions are TTML"),
Format::Ts => read_ts(input.reader, &mut output),
Format::Mmt => read_mmt(input.reader, options.raw, &mut output),
}
}
fn read_ts(reader: impl BufRead, output: &mut impl Write) -> anyhow::Result<()> {
let mut demuxer = arib::ts::demux::Demuxer::new(reader);
let mut captions = BTreeSet::new();
let mut pcr = None;
let mut clock: Option<(NaiveDateTime, Option<f64>)> = None;
loop {
let packet = demuxer.read_packet()?;
pcr = packet
.as_ref()
.and_then(|packet| packet.pcr())
.map(|pcr| pcr as f64 / 90_000.0)
.or(pcr);
for packet in packets(demuxer.take_packets()) {
match packet? {
Packet::Media(MediaPacket::Track {
track_id,
ty: TrackType::AribCaption,
}) => {
captions.insert(track_id);
}
Packet::Signaling(SignalingEvent::Ts {
table:
Table::Tot(Tot {
jst_time: Some(time),
..
})
| Table::Tdt(Tdt {
jst_time: Some(time),
..
}),
..
}) => clock = Some((time, pcr)),
Packet::Media(MediaPacket::Sample {
track_id,
data,
pts,
..
}) if captions.contains(&track_id) => {
let group = match DataGroup::read_pes_data(data) {
Ok(group) => group,
Err(error) => {
warn!(%error, "Ignoring a malformed caption");
continue;
}
};
let DataGroupData::Statement { statement, .. } = group.data else {
continue;
};
let time = clock.map(|(time, anchor)| match pts.zip(anchor) {
Some((pts, anchor)) => {
time + TimeDelta::milliseconds(((pts - anchor) * 1000.0).round() as i64)
}
None => time,
});
print_caption(output, time.map(When::At), &statement.text())?;
}
_ => {}
}
}
if packet.is_none() {
return Ok(());
}
}
}
fn read_mmt(reader: impl BufRead, raw: bool, output: &mut impl Write) -> anyhow::Result<()> {
let mut tracks = BTreeMap::<u16, Option<AdditionalAribSubtitleInfo>>::new();
for packet in packets(arib::mmt::demux::Demuxer::new(reader)) {
match packet? {
Packet::Media(MediaPacket::Track {
track_id,
ty: TrackType::Ttml,
}) => {
tracks.entry(track_id).or_default();
}
Packet::Signaling(SignalingEvent::Mmt(Message::Pa(message))) => {
let assets = message.tables.iter().flat_map(|table| match table {
arib::mmt::table::Table::Mpt(mpt) => mpt.assets.as_slice(),
_ => &[],
});
for asset in assets {
let Some(info) = asset
.locations
.last()
.and_then(|location| tracks.get_mut(&location.packet_id()?))
else {
continue;
};
for descriptor in &asset.asset_descriptors {
if let Descriptor::MhDataComponent(descriptor) = descriptor
&& let AdditionalDataComponentInfo::Subtitle(subtitle) =
&descriptor.additional_data_component_info
{
*info = Some(subtitle.clone());
}
}
}
}
Packet::Media(MediaPacket::Sample {
track_id,
data,
pts,
..
}) => {
let Some(info) = tracks.get(&track_id) else {
continue;
};
if raw {
output.write_all(&data)?;
if !data.ends_with(b"\n") {
output.write_all(b"\n")?;
}
output.flush()?;
continue;
}
let cues = match std::str::from_utf8(&data)
.map_err(std::io::Error::other)
.and_then(arib::mmt::ttml::read_cues)
{
Ok(cues) => cues,
Err(error) => {
warn!(%error, "Ignoring a malformed TTML document");
continue;
}
};
let origin = info
.as_ref()
.and_then(|info| match info.time_control_mode {
SubtitleTimeControlMode::ReferenceStartTime => {
info.reference_start_time.map(|time| {
(time >> 32) as f64 + (time as u32) as f64 / 2_f64.powi(32)
})
}
SubtitleTimeControlMode::MpuTimestamp => pts,
_ => None,
})
.map(ntp_to_jst);
for cue in cues {
let when = match (origin, cue.begin) {
(Some(origin), begin) => Some(When::At(origin + begin.unwrap_or_default())),
(None, Some(begin)) => Some(When::After(begin)),
(None, None) => None,
};
print_caption(output, when, &cue.text)?;
}
}
_ => {}
}
}
Ok(())
}
fn packets(
demuxer: impl Iterator<Item = arib::Result<Packet>>,
) -> impl Iterator<Item = anyhow::Result<Packet>> {
demuxer.filter_map(|packet| match packet {
Ok(packet) => Some(Ok(packet)),
Err(arib::Error::Io(error)) => Some(Err(error.into())),
Err(error) => {
warn!(%error, "Could not read a packet");
None
}
})
}
enum When {
At(NaiveDateTime),
After(Duration),
}
fn print_caption(output: &mut impl Write, when: Option<When>, text: &str) -> anyhow::Result<()> {
let text = text
.split('\n')
.map(str::trim)
.collect::<Vec<_>>()
.join(" ");
let text = text.trim();
if text.is_empty() {
return Ok(());
}
let when = match when {
Some(When::At(time)) => time.format("%Y-%m-%d %H:%M:%S%.3f").to_string(),
Some(When::After(time)) => {
let millis = time.as_millis();
format!(
"+{:02}:{:02}:{:02}.{:03}",
millis / 3_600_000,
millis / 60_000 % 60,
millis / 1000 % 60,
millis % 1000,
)
}
None => "-".to_owned(),
};
writeln!(output, "{when}\t{text}")?;
output.flush()?;
Ok(())
}
fn ntp_to_jst(seconds: f64) -> NaiveDateTime {
const NTP_TO_UNIX: f64 = 2_208_988_800.0;
const JST: TimeDelta = TimeDelta::hours(9);
let millis = ((seconds - NTP_TO_UNIX) * 1000.0).round() as i64;
chrono::DateTime::from_timestamp_millis(millis)
.unwrap_or_default()
.naive_utc()
+ JST
}