use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use broadcast_common::{Package, Unpackage};
use crate::dash::DashPackager;
use crate::flv::FlvDemux;
use crate::media::{CmafMux, Fmp4Demux, HlsPackager, Media};
use crate::progressive::ProgressiveMux;
use crate::ps_demux::PsDemux;
use crate::ts_demux::TsDemux;
use crate::ts_hls::TsHlsPackager;
use crate::ts_mux::TsMux;
use crate::webm_demux::WebmDemux;
const TS_SYNC_BYTE: u8 = 0x47;
const TS_PACKET_LEN: usize = 188;
const PS_PACK_START_CODE: [u8; 4] = [0x00, 0x00, 0x01, 0xBA];
const EBML_MAGIC: [u8; 4] = [0x1A, 0x45, 0xDF, 0xA3];
const FLV_SIGNATURE: [u8; 3] = *b"FLV";
const BOX_FOURCC_OFFSET: usize = 4;
const ISOBMFF_LEADING_FOURCCS: [[u8; 4]; 4] = [*b"ftyp", *b"styp", *b"moov", *b"moof"];
const LL_DASH_LATENCY_TARGET_MS: u32 = 3000;
const LL_DASH_AVAILABILITY_START: &str = "1970-01-01T00:00:00Z";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Container {
MpegTs,
Mp4,
MpegPs,
WebM,
Flv,
}
impl Container {
pub fn name(&self) -> &'static str {
match self {
Container::MpegTs => "mpeg-ts",
Container::Mp4 => "mp4",
Container::MpegPs => "mpeg-ps",
Container::WebM => "webm",
Container::Flv => "flv",
}
}
}
broadcast_common::impl_spec_display!(Container);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum OutputFormat {
Cmaf,
Hls,
TsHls,
Dash,
Ts,
Progressive,
}
impl OutputFormat {
pub fn name(&self) -> &'static str {
match self {
OutputFormat::Cmaf => "cmaf",
OutputFormat::Hls => "hls",
OutputFormat::TsHls => "ts-hls",
OutputFormat::Dash => "dash",
OutputFormat::Ts => "ts",
OutputFormat::Progressive => "progressive",
}
}
fn from_extension(ext: &str) -> Option<Self> {
match ext.to_ascii_lowercase().as_str() {
"m3u8" => Some(OutputFormat::Hls),
"mpd" => Some(OutputFormat::Dash),
"ts" => Some(OutputFormat::Ts),
"cmaf" | "m4s" => Some(OutputFormat::Cmaf),
"mp4" | "m4v" => Some(OutputFormat::Progressive),
_ => None,
}
}
}
broadcast_common::impl_spec_display!(OutputFormat);
#[derive(Debug, clap::Parser)]
#[command(name = "transmux", version, about, long_about = None)]
pub struct Args {
#[arg(value_name = "IN", required_unless_present = "input")]
pub in_positional: Option<PathBuf>,
#[arg(
short = 'i',
long = "input",
value_name = "PATH",
conflicts_with = "in_positional"
)]
pub input: Option<PathBuf>,
#[arg(short = 'o', long = "output", value_name = "PATH")]
pub output: PathBuf,
#[arg(short = 'f', long = "format", value_enum)]
pub format: Option<FormatArg>,
#[arg(long = "segment-duration", value_name = "SECS", default_value_t = 6)]
pub segment_duration: u32,
#[arg(long = "ll")]
pub ll: bool,
#[arg(long = "tracks", value_name = "IDS", value_delimiter = ',')]
pub tracks: Vec<u32>,
#[cfg(feature = "cenc")]
#[arg(long = "decrypt")]
pub decrypt: bool,
#[cfg(feature = "cenc")]
#[arg(long = "key", value_name = "KID:KEY")]
pub keys: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum FormatArg {
Cmaf,
Hls,
#[value(name = "ts-hls")]
TsHls,
Dash,
Ts,
Progressive,
}
impl From<FormatArg> for OutputFormat {
fn from(a: FormatArg) -> Self {
match a {
FormatArg::Cmaf => OutputFormat::Cmaf,
FormatArg::Hls => OutputFormat::Hls,
FormatArg::TsHls => OutputFormat::TsHls,
FormatArg::Dash => OutputFormat::Dash,
FormatArg::Ts => OutputFormat::Ts,
FormatArg::Progressive => OutputFormat::Progressive,
}
}
}
#[derive(Debug)]
pub enum CliError {
Io(std::io::Error),
Transmux(crate::Error),
UnknownContainer,
UndeterminedFormat,
NoTracksSelected,
BadKey(String),
}
impl fmt::Display for CliError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CliError::Io(e) => write!(f, "i/o error: {e}"),
CliError::Transmux(e) => write!(f, "transmux error: {e}"),
CliError::UnknownContainer => write!(
f,
"unknown input container: leading bytes match no supported format \
(MPEG-TS, MP4/CMAF, MPEG-PS, WebM, FLV)"
),
CliError::UndeterminedFormat => write!(
f,
"output format not given and not inferable from the output extension; \
pass -f/--format"
),
CliError::NoTracksSelected => {
write!(f, "the --tracks selection matched no tracks in the input")
}
CliError::BadKey(s) => {
write!(f, "invalid --key {s:?}: expected <32-hex-KID>:<32-hex-key>")
}
}
}
}
impl std::error::Error for CliError {}
impl From<std::io::Error> for CliError {
fn from(e: std::io::Error) -> Self {
CliError::Io(e)
}
}
impl From<crate::Error> for CliError {
fn from(e: crate::Error) -> Self {
CliError::Transmux(e)
}
}
pub type CliResult<T> = Result<T, CliError>;
pub fn detect_container(data: &[u8]) -> CliResult<Container> {
if data.len() >= FLV_SIGNATURE.len() && data[..FLV_SIGNATURE.len()] == FLV_SIGNATURE {
return Ok(Container::Flv);
}
if data.len() >= EBML_MAGIC.len() && data[..EBML_MAGIC.len()] == EBML_MAGIC {
return Ok(Container::WebM);
}
if data.len() >= PS_PACK_START_CODE.len()
&& data[..PS_PACK_START_CODE.len()] == PS_PACK_START_CODE
{
return Ok(Container::MpegPs);
}
if data.len() >= BOX_FOURCC_OFFSET + 4 {
let fourcc = &data[BOX_FOURCC_OFFSET..BOX_FOURCC_OFFSET + 4];
if ISOBMFF_LEADING_FOURCCS.iter().any(|f| f == fourcc) {
return Ok(Container::Mp4);
}
}
if data.first() == Some(&TS_SYNC_BYTE) && data.get(TS_PACKET_LEN) == Some(&TS_SYNC_BYTE) {
return Ok(Container::MpegTs);
}
Err(CliError::UnknownContainer)
}
#[derive(Debug)]
pub enum Output {
Bytes(Vec<u8>),
Manifest {
text: String,
segments: Vec<(String, Vec<u8>)>,
},
}
#[derive(Debug, Clone)]
pub struct Opts {
pub format: OutputFormat,
pub segment_duration: u32,
pub low_latency: bool,
pub tracks: Vec<u32>,
}
impl Default for Opts {
fn default() -> Self {
Self {
format: OutputFormat::Cmaf,
segment_duration: 6,
low_latency: false,
tracks: Vec::new(),
}
}
}
pub fn run_bytes(input: &[u8], opts: &Opts) -> CliResult<Output> {
let container = detect_container(input)?;
let mut media = demux(container, input)?;
if !opts.tracks.is_empty() {
media
.tracks
.retain(|t| opts.tracks.contains(&t.spec.track_id));
if media.tracks.is_empty() {
return Err(CliError::NoTracksSelected);
}
}
package(&media, opts)
}
fn demux(container: Container, input: &[u8]) -> CliResult<Media> {
let media = match container {
Container::MpegTs => TsDemux::new().unpackage(input)?,
Container::Mp4 => Fmp4Demux::new().unpackage(input)?,
Container::MpegPs => PsDemux::new().unpackage(input)?,
Container::WebM => WebmDemux::new().unpackage(input)?,
Container::Flv => FlvDemux::new()
.unpackage(input)
.map_err(|e| crate::Error::InvalidInput(flv_reason(e)))?,
};
Ok(media)
}
fn flv_reason(_e: crate::flv::FlvError) -> &'static str {
"FLV demux failed"
}
fn package(media: &Media, opts: &Opts) -> CliResult<Output> {
match opts.format {
OutputFormat::Cmaf => Ok(Output::Bytes(CmafMux::new(1).package(media)?)),
OutputFormat::Progressive => Ok(Output::Bytes(ProgressiveMux::new(true).package(media)?)),
OutputFormat::Ts => Ok(Output::Bytes(TsMux::new().package(media)?)),
OutputFormat::Hls => {
let text = HlsPackager::default().package(media)?;
let cmaf = CmafMux::new(1).package(media)?;
let segments = media
.tracks
.iter()
.map(|t| (format!("seg{}.m4s", t.spec.track_id), cmaf.clone()))
.collect();
Ok(Output::Manifest { text, segments })
}
OutputFormat::TsHls => {
let out = TsHlsPackager::new(opts.segment_duration).package(media)?;
let segments = out
.segments
.into_iter()
.enumerate()
.map(|(i, bytes)| (format!("seg{i}.ts"), bytes))
.collect();
Ok(Output::Manifest {
text: out.playlist,
segments,
})
}
OutputFormat::Dash => {
let text = if opts.low_latency {
let seg = opts.segment_duration.max(1) as f64;
crate::ll_dash::LlDashPackager::new(
seg,
seg / 2.0,
LL_DASH_LATENCY_TARGET_MS,
LL_DASH_AVAILABILITY_START,
)?
.package(media)?
} else {
DashPackager::default().package(media)?
};
let cmaf = CmafMux::new(1).package(media)?;
let mut segments = Vec::new();
for t in &media.tracks {
let id = t.spec.track_id;
segments.push((format!("init-stream{id}.m4s"), cmaf.clone()));
segments.push((format!("chunk-stream{id}-1.m4s"), cmaf.clone()));
}
Ok(Output::Manifest { text, segments })
}
}
}
fn input_path(args: &Args) -> &Path {
args.in_positional
.as_deref()
.or(args.input.as_deref())
.expect("clap requires one of <IN> or --input")
}
fn resolve_format(args: &Args) -> CliResult<OutputFormat> {
if let Some(f) = args.format {
return Ok(f.into());
}
args.output
.extension()
.and_then(|e| e.to_str())
.and_then(OutputFormat::from_extension)
.ok_or(CliError::UndeterminedFormat)
}
pub fn run(args: Args) -> CliResult<(Container, OutputFormat)> {
let in_path = input_path(&args).to_path_buf();
let format = resolve_format(&args)?;
let input = fs::read(&in_path)?;
let container = detect_container(&input)?;
let opts = Opts {
format,
segment_duration: args.segment_duration,
low_latency: args.ll,
tracks: args.tracks.clone(),
};
#[cfg(feature = "cenc")]
let media_bytes;
#[cfg(feature = "cenc")]
let input_ref: &[u8] = if args.decrypt {
media_bytes = decrypt_input(&input, container, &args.keys)?;
&media_bytes
} else {
&input
};
#[cfg(not(feature = "cenc"))]
let input_ref: &[u8] = &input;
let out = run_bytes(input_ref, &opts)?;
write_output(&args.output, out)?;
Ok((container, format))
}
fn write_output(out_path: &Path, out: Output) -> CliResult<()> {
match out {
Output::Bytes(b) => {
fs::write(out_path, b)?;
}
Output::Manifest { text, segments } => {
if let Some(parent) = out_path.parent() {
if !parent.as_os_str().is_empty() {
fs::create_dir_all(parent)?;
}
}
fs::write(out_path, text)?;
let dir = out_path.parent().unwrap_or_else(|| Path::new("."));
for (name, bytes) in segments {
fs::write(dir.join(name), bytes)?;
}
}
}
Ok(())
}
#[cfg(feature = "cenc")]
fn decrypt_input(input: &[u8], container: Container, keys: &[String]) -> CliResult<Vec<u8>> {
use broadcast_common::Decrypt;
if container != Container::Mp4 {
return Err(CliError::Transmux(crate::Error::InvalidInput(
"--decrypt only applies to CENC-protected MP4/CMAF input",
)));
}
let mut key_map = crate::cenc_decrypt::KeyMap::new();
for spec in keys {
let (kid, key) = parse_key(spec)?;
key_map.insert(kid, key);
}
let decryptor = crate::cenc_decrypt::CencDecryptor::from_fmp4(input)?;
let mut media = decryptor.demux()?;
decryptor.decrypt(&mut media, &key_map)?;
Ok(CmafMux::new(1).package(&media)?)
}
#[cfg(feature = "cenc")]
fn parse_key(spec: &str) -> CliResult<([u8; 16], [u8; 16])> {
let (kid_hex, key_hex) = spec
.split_once(':')
.ok_or_else(|| CliError::BadKey(spec.to_string()))?;
let kid = parse_hex16(kid_hex).ok_or_else(|| CliError::BadKey(spec.to_string()))?;
let key = parse_hex16(key_hex).ok_or_else(|| CliError::BadKey(spec.to_string()))?;
Ok((kid, key))
}
#[cfg(feature = "cenc")]
fn parse_hex16(s: &str) -> Option<[u8; 16]> {
let s = s.trim();
if s.len() != 32 {
return None;
}
let mut out = [0u8; 16];
for (i, byte) in out.iter_mut().enumerate() {
*byte = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).ok()?;
}
Some(out)
}