use std::fs;
use std::path::PathBuf;
use clap::Parser;
use ts_fix::{PcrRestamp, PidFilter, Stuffing, TsFix};
#[derive(Parser)]
#[command(
name = "ts-fix",
version,
about = "MPEG-2 TS repair / remux engine (ISO/IEC 13818-1 §2.4)",
long_about = "Reads an input .ts file, applies repair operations, writes the repaired stream.\n\
Operations are applied in the engine's canonical order (continuity → filter → \n\
psi-regen → stuffing) regardless of flag order. All operations are opt-in."
)]
struct Cli {
#[arg(
long = "input",
short = 'i',
value_name = "PATH",
help = "Input TS file"
)]
input: PathBuf,
#[arg(
long = "output",
short = 'o',
value_name = "PATH",
help = "Output TS file"
)]
output: PathBuf,
#[arg(
long = "repair-continuity",
help = "Renumber per-PID continuity counters to monotonic (mod 16) sequences"
)]
repair_continuity: bool,
#[arg(
long = "keep-pids",
value_name = "PID,PID,...",
conflicts_with = "service",
value_delimiter = ',',
help = "Comma-separated PIDs to keep (PAT PID 0x0000 always included)"
)]
keep_pids: Option<Vec<u16>>,
#[arg(
long = "service",
value_name = "PROGRAM",
conflicts_with = "keep_pids",
help = "Extract a single programme by program_number (observes PAT/PMT)"
)]
service: Option<u16>,
#[arg(
long = "restamp-pcr-interpolate",
conflicts_with = "restamp_pcr_bitrate",
help = "Interpolate PCRs between observed anchors (preserves first PCR)"
)]
restamp_pcr_interpolate: bool,
#[arg(
long = "restamp-pcr-bitrate",
value_name = "BPS",
conflicts_with = "restamp_pcr_interpolate",
help = "Recompute PCRs from a fixed bitrate in bits/sec (e.g. 27000000)"
)]
restamp_pcr_bitrate: Option<u64>,
#[arg(
long = "regen-psi",
help = "Rebuild PAT from observed PMT PIDs on flush"
)]
regen_psi: bool,
#[arg(
long = "drop-nulls",
conflicts_with = "pad_to",
help = "Remove all null packets from the output"
)]
drop_nulls: bool,
#[arg(
long = "pad-to",
value_name = "RATE",
conflicts_with = "drop_nulls",
help = "Insert null packets to reach target rate (packets_per_input)"
)]
pad_to: Option<f64>,
}
fn main() {
if let Err(e) = run() {
eprintln!("error: {e}");
std::process::exit(1);
}
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
let input =
fs::read(&cli.input).map_err(|e| format!("cannot read {}: {e}", cli.input.display()))?;
let mut builder = TsFix::builder();
if cli.repair_continuity {
builder = builder.repair_continuity();
}
if let Some(pids) = &cli.keep_pids {
builder = builder.filter_pids(PidFilter::keep(pids.iter().copied()));
} else if let Some(program) = cli.service {
builder = builder.filter_pids(PidFilter::service(program));
}
if cli.regen_psi {
builder = builder.regen_psi();
}
if cli.restamp_pcr_interpolate {
builder = builder.restamp_pcr(PcrRestamp::interpolate());
} else if let Some(bps) = cli.restamp_pcr_bitrate {
builder = builder.restamp_pcr(PcrRestamp::from_bitrate(bps));
}
if cli.drop_nulls {
builder = builder.stuffing(Stuffing::drop_nulls());
} else if let Some(rate) = cli.pad_to {
builder = builder.stuffing(Stuffing::pad_to(rate));
}
let mut engine = builder.build()?;
let mut output = Vec::with_capacity(input.len());
for chunk in input.chunks(188) {
engine.push(chunk, |pkt| output.extend_from_slice(pkt))?;
}
engine.finish(|pkt| output.extend_from_slice(pkt));
fs::write(&cli.output, &output)
.map_err(|e| format!("cannot write {}: {e}", cli.output.display()))?;
eprintln!(
"wrote {} packets ({} bytes) to {}",
output.len() / 188,
output.len(),
cli.output.display(),
);
Ok(())
}