use std::path::PathBuf;
use rff_codec::{CodecParams, Decoder, Encoder};
use rff_core::{
AudioFrame, CodecId, ColorRange, Dictionary, Error, Frame, MediaType, Packet, PixelFormat,
Result, SampleFormat, VideoFrame,
};
use rff_filter::{FilterChain, FilterComplex};
use rff_format::{Muxer, Stream};
use rff_resample::Resampler;
use crate::Engine;
mod phases {
use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
use std::sync::OnceLock;
pub static OPEN: AtomicU64 = AtomicU64::new(0);
pub static DEMUX: AtomicU64 = AtomicU64::new(0);
pub static DECODE: AtomicU64 = AtomicU64::new(0);
pub static CONFORM: AtomicU64 = AtomicU64::new(0);
pub static ENCODE: AtomicU64 = AtomicU64::new(0);
pub static MUX: AtomicU64 = AtomicU64::new(0);
pub fn on() -> bool {
static ON: OnceLock<bool> = OnceLock::new();
*ON.get_or_init(|| std::env::var_os("RFF_PHASES").is_some())
}
pub fn time<T>(acc: &AtomicU64, f: impl FnOnce() -> T) -> T {
if !on() {
return f();
}
let t = std::time::Instant::now();
let r = f();
acc.fetch_add(t.elapsed().as_nanos() as u64, Relaxed);
r
}
pub fn report(total: std::time::Duration) {
let total_ns = total.as_nanos().max(1) as f64;
eprintln!("--- rff phases (total {:.1} ms) ---", total_ns / 1e6);
let mut sum = 0u64;
for (name, acc) in [
("open", &OPEN),
("demux", &DEMUX),
("decode", &DECODE),
("conform", &CONFORM),
("encode", &ENCODE),
("mux", &MUX),
] {
let ns = acc.swap(0, Relaxed);
sum += ns;
eprintln!(
" {name:<8} {:>9.1} ms {:>5.1}%",
ns as f64 / 1e6,
100.0 * ns as f64 / total_ns
);
}
let rest = total_ns - sum as f64;
eprintln!(
" {:<8} {:>9.1} ms {:>5.1}%",
"other",
rest / 1e6,
100.0 * rest / total_ns
);
}
}
fn is_full_range_codec(codec: CodecId) -> bool {
matches!(
codec,
CodecId::Jpeg | CodecId::Png | CodecId::Gif | CodecId::Webp
)
}
fn effective_color_range(stream: &Stream) -> ColorRange {
match stream.color_range {
ColorRange::Unspecified if is_full_range_codec(stream.codec_id) => ColorRange::Full,
ColorRange::Unspecified => ColorRange::Limited,
explicit => explicit,
}
}
#[derive(Debug, Clone)]
pub struct InputSpec {
pub path: PathBuf,
pub format: Option<String>,
}
#[derive(Debug, Clone)]
pub struct StreamCodec {
pub codec: CodecId,
pub options: Dictionary,
pub sample_format: Option<SampleFormat>,
}
#[derive(Debug, Clone)]
pub enum MapSelector {
All,
Kind(MediaType),
Index(usize),
}
#[derive(Debug, Clone)]
pub struct MapSpec {
pub input: usize,
pub selector: MapSelector,
}
#[derive(Debug, Clone, Default)]
pub struct OutputSpec {
pub path: PathBuf,
pub format: Option<String>,
pub video_codec: Option<StreamCodec>,
pub audio_codec: Option<StreamCodec>,
pub subtitle_codec: Option<CodecId>,
pub video_filters: Option<String>,
pub filter_complex: Option<String>,
pub maps: Vec<MapSpec>,
pub overwrite: bool,
pub max_video_frames: Option<u64>,
pub trim_start: Option<f64>,
pub trim_end: Option<f64>,
pub frame_rate: Option<(u32, u32)>,
pub audio_rate: Option<u32>,
pub audio_channels: Option<u16>,
pub audio_filters: Option<String>,
pub metadata: Dictionary,
pub format_options: Dictionary,
}
#[derive(Debug, Clone, Default)]
pub struct TranscodeSpec {
pub inputs: Vec<InputSpec>,
pub outputs: Vec<OutputSpec>,
}
#[derive(Debug, Clone, Default)]
pub struct TranscodeReport {
pub packets_written: u64,
pub frames_decoded: u64,
}
#[derive(Clone, Copy)]
struct Trim {
start: f64,
end: Option<f64>,
}
impl Trim {
fn from_spec(o: &OutputSpec) -> Option<Trim> {
if o.trim_start.is_none() && o.trim_end.is_none() {
return None;
}
Some(Trim {
start: o.trim_start.unwrap_or(0.0).max(0.0),
end: o.trim_end,
})
}
fn keeps(&self, t: f64) -> bool {
t >= self.start - 1e-9 && self.end.map_or(true, |e| t < e - 1e-9)
}
}
struct FpsConv {
num: u32,
den: u32,
next_slot: i64,
last: Option<VideoFrame>,
}
impl FpsConv {
fn new(num: u32, den: u32) -> FpsConv {
FpsConv {
num: num.max(1),
den: den.max(1),
next_slot: 0,
last: None,
}
}
fn slot_time(&self, slot: i64) -> f64 {
slot as f64 * self.den as f64 / self.num as f64
}
fn push(&mut self, frame: VideoFrame, t: f64) -> Vec<VideoFrame> {
let mut out = Vec::new();
while let Some(last) = &self.last {
if self.slot_time(self.next_slot) < t - 1e-9 {
let mut f = last.clone();
f.pts = Some(self.next_slot);
out.push(f);
self.next_slot += 1;
} else {
break;
}
}
self.last = Some(frame);
out
}
fn finish(&mut self) -> Option<VideoFrame> {
self.last.take().map(|mut f| {
f.pts = Some(self.next_slot);
self.next_slot += 1;
f
})
}
}
enum StreamOp {
Skip,
Copy(CopyOp),
Transcode(Box<TranscodeOp>),
}
struct CopyOp {
out_index: usize,
is_video: bool,
start_ticks: i64,
end_ticks: Option<i64>,
trimming: bool,
started: bool,
}
impl CopyOp {
fn process(&mut self, mut packet: Packet) -> Option<Packet> {
if self.trimming {
let ts = packet.pts.or(packet.dts).unwrap_or(0);
if ts < self.start_ticks {
return None;
}
if let Some(end) = self.end_ticks {
if ts >= end {
return None;
}
}
if self.is_video && !self.started {
if !packet.flags.keyframe {
return None; }
self.started = true;
}
packet.pts = packet.pts.map(|p| p - self.start_ticks);
packet.dts = packet.dts.map(|d| d - self.start_ticks);
}
packet.stream_index = self.out_index;
Some(packet)
}
}
struct TranscodeOp {
decoder: Box<dyn Decoder>,
encoder: Box<dyn Encoder>,
filters: FilterChain,
overlay: Option<(VideoFrame, u32, u32)>,
in_time_base: rff_core::Rational,
is_video: bool,
trim: Option<Trim>,
shift_ticks: i64,
audio_clock_samples: u64,
fps: Option<FpsConv>,
audio_chain: rff_filter::AudioFilterChain,
target_channels: u16,
target_rate: u32,
target_sample_format: Option<SampleFormat>,
resampler: Option<Resampler>,
accepted_formats: Option<Vec<PixelFormat>>,
pixel_converter: Option<FilterChain>,
source_range: ColorRange,
out_index: usize,
max_video_frames: Option<u64>,
video_frames_sent: u64,
pending_pts: std::collections::VecDeque<Option<i64>>,
}
fn apply_overlay(overlay: &Option<(VideoFrame, u32, u32)>, frame: Frame) -> Result<Frame> {
match (overlay, frame) {
(Some((over, x, y)), Frame::Video(v)) => {
Ok(Frame::Video(rff_filter::overlay(v, over, *x, *y)?))
}
(_, frame) => Ok(frame),
}
}
fn apply_filters(filters: &mut FilterChain, frame: Frame) -> Result<Frame> {
if filters.is_empty() {
return Ok(frame);
}
match frame {
Frame::Video(v) => Ok(Frame::Video(filters.apply(v)?)),
other => Ok(other),
}
}
fn nearest_rate(rates: &[u32], target: u32) -> u32 {
rates
.iter()
.copied()
.min_by_key(|r| (*r as i64 - target as i64).abs())
.unwrap_or(target)
}
fn audio_to_f32(af: &AudioFrame) -> Result<Vec<f32>> {
match af.format {
SampleFormat::F32 => Ok(af.planes[0]
.chunks_exact(4)
.map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
.collect()),
SampleFormat::S16 => Ok(af.planes[0]
.chunks_exact(2)
.map(|b| i16::from_le_bytes([b[0], b[1]]) as f32 / 32768.0)
.collect()),
other => Err(Error::unsupported(format!(
"resample: sample format `{}` (need interleaved s16/f32)",
other.name()
))),
}
}
fn f32_le_bytes(samples: &[f32]) -> Vec<u8> {
let mut bytes = vec![0u8; samples.len() * 4];
for (d, s) in bytes.chunks_exact_mut(4).zip(samples) {
d.copy_from_slice(&s.to_le_bytes());
}
bytes
}
fn f32_to_s16(s: f32) -> i16 {
(s * 32768.0).round().clamp(-32768.0, 32767.0) as i16
}
fn f32_frame(samples: Vec<f32>, rate: u32, channels: u16, pts: Option<i64>) -> Frame {
let bytes = f32_le_bytes(&samples);
Frame::Audio(AudioFrame {
sample_rate: rate,
channels,
format: SampleFormat::F32,
samples: samples.len() / channels.max(1) as usize,
planes: vec![bytes],
pts,
})
}
fn conform_sample_format(target: Option<SampleFormat>, frame: Frame) -> Result<Frame> {
let Some(target) = target else {
return Ok(frame);
};
let Frame::Audio(af) = frame else {
return Ok(frame);
};
if af.format == target {
return Ok(Frame::Audio(af));
}
let src = &af.planes[0];
let plane = match (af.format, target) {
(SampleFormat::F32, SampleFormat::S16) => {
let mut out = vec![0u8; src.len() / 4 * 2];
for (d, b) in out.chunks_exact_mut(2).zip(src.chunks_exact(4)) {
let s = f32::from_le_bytes([b[0], b[1], b[2], b[3]]);
d.copy_from_slice(&f32_to_s16(s).to_le_bytes());
}
out
}
(SampleFormat::S16, SampleFormat::F32) => {
let mut out = vec![0u8; src.len() / 2 * 4];
for (d, b) in out.chunks_exact_mut(4).zip(src.chunks_exact(2)) {
let s = i16::from_le_bytes([b[0], b[1]]) as f32 / 32768.0;
d.copy_from_slice(&s.to_le_bytes());
}
out
}
_ => {
audio_to_f32(&af)?;
return Err(Error::unsupported(format!(
"sample format conversion to `{}` (only interleaved s16/f32)",
target.name()
)));
}
};
let planes = vec![plane];
Ok(Frame::Audio(AudioFrame {
sample_rate: af.sample_rate,
channels: af.channels,
format: target,
samples: af.samples,
planes,
pts: af.pts,
}))
}
fn conform_audio(
resampler: &mut Option<Resampler>,
target_rate: u32,
frame: Frame,
) -> Result<Frame> {
if target_rate == 0 {
return Ok(frame);
}
let Frame::Audio(af) = frame else {
return Ok(frame);
};
if af.sample_rate == target_rate {
return Ok(Frame::Audio(af));
}
let rs =
resampler.get_or_insert_with(|| Resampler::new(af.sample_rate, target_rate, af.channels));
let out = rs.process(&audio_to_f32(&af)?);
let pts = af
.pts
.map(|p| p * target_rate as i64 / af.sample_rate.max(1) as i64);
Ok(f32_frame(out, target_rate, af.channels, pts))
}
fn conform_video(
converter: &mut Option<FilterChain>,
accepted: &Option<Vec<PixelFormat>>,
source_range: ColorRange,
frame: Frame,
) -> Result<Frame> {
let Some(accepted) = accepted else {
return Ok(frame);
};
let Frame::Video(vf) = frame else {
return Ok(frame);
};
if accepted.is_empty() || accepted.contains(&vf.format) {
return Ok(Frame::Video(vf));
}
if converter.is_none() {
let target = accepted
.iter()
.find(|f| matches!(f, PixelFormat::Rgb24 | PixelFormat::Yuv420p))
.ok_or_else(|| {
Error::unsupported(format!(
"no conversion from `{}` to any format this encoder accepts",
vf.format.name()
))
})?;
let range = match source_range {
ColorRange::Limited => "limited",
_ => "full",
};
*converter = Some(FilterChain::parse(&format!(
"format={}:bt601:{}",
target.name(),
range
))?);
}
let chain = converter.as_mut().expect("just built");
Ok(Frame::Video(chain.apply(vf)?))
}
fn conform_channels(target: u16, frame: Frame) -> Result<Frame> {
if target == 0 {
return Ok(frame);
}
let Frame::Audio(af) = frame else {
return Ok(frame);
};
if af.channels == target {
return Ok(Frame::Audio(af));
}
let samples = audio_to_f32(&af)?;
let mixed: Vec<f32> = match (af.channels, target) {
(2, 1) => samples
.chunks_exact(2)
.map(|p| 0.5 * (p[0] + p[1]))
.collect(),
(1, 2) => samples.iter().flat_map(|s| [*s, *s]).collect(),
(from, to) => {
return Err(Error::unsupported(format!(
"-ac: only 1<->2 channel mixing supported (input {from} ch, requested {to})"
)))
}
};
Ok(f32_frame(mixed, af.sample_rate, target, af.pts))
}
impl TranscodeOp {
fn frame_time(&self, frame: &Frame) -> Result<f64> {
let tb = self.in_time_base;
let tick = if tb.num > 0 && tb.den > 0 {
tb.num as f64 / tb.den as f64
} else {
0.001
};
match frame.pts() {
Some(p) => Ok(p as f64 * tick),
None => match frame {
Frame::Audio(af) => {
Ok(self.audio_clock_samples as f64 / af.sample_rate.max(1) as f64)
}
Frame::Video(_) => Err(Error::unsupported(
"-ss/-t/-to/-r need timestamps and this video stream has none",
)),
},
}
}
fn handle_frame(
&mut self,
frame: Frame,
muxer: &mut dyn Muxer,
report: &mut TranscodeReport,
) -> Result<()> {
report.frames_decoded += 1;
let needs_time = self.trim.is_some() || (self.is_video && self.fps.is_some());
let t = if needs_time {
Some(self.frame_time(&frame)?)
} else {
None
};
if let Frame::Audio(af) = &frame {
self.audio_clock_samples += af.samples as u64;
}
let mut frame = frame;
if let (Some(trim), Some(t)) = (self.trim, t) {
match frame {
Frame::Audio(af) => {
match rff_filter::trim_audio_frame(af, Some(t), Some(trim.start), trim.end)? {
Some(af) => frame = Frame::Audio(af),
None => return Ok(()),
}
}
Frame::Video(_) => {
if !trim.keeps(t) {
return Ok(());
}
}
}
}
if self.shift_ticks != 0 {
match &mut frame {
Frame::Video(v) => v.pts = v.pts.map(|p| p - self.shift_ticks),
Frame::Audio(a) => a.pts = a.pts.map(|p| p - self.shift_ticks),
}
}
match frame {
Frame::Video(v) if self.fps.is_some() => {
let t_out = t.expect("fps requires time") - self.trim.map_or(0.0, |w| w.start);
let mut fps = self.fps.take().expect("checked");
let due = fps.push(v, t_out);
self.fps = Some(fps);
for f in due {
self.emit(Frame::Video(f), muxer, report)?;
}
Ok(())
}
other => self.emit(other, muxer, report),
}
}
fn emit(
&mut self,
frame: Frame,
muxer: &mut dyn Muxer,
report: &mut TranscodeReport,
) -> Result<()> {
if let Some(limit) = self.max_video_frames {
if matches!(frame, Frame::Video(_)) {
if self.video_frames_sent >= limit {
return Ok(());
}
self.video_frames_sent += 1;
}
}
let Some(frame) = phases::time(&phases::CONFORM, || self.conform(frame))? else {
return Ok(()); };
if self.is_video {
self.pending_pts.push_back(frame.pts());
}
phases::time(&phases::ENCODE, || self.encoder.send_frame(&frame))?;
self.drain(muxer, report)
}
fn conform(&mut self, frame: Frame) -> Result<Option<Frame>> {
let frame = apply_filters(&mut self.filters, frame)?;
let frame = apply_overlay(&self.overlay, frame)?;
let frame = conform_channels(self.target_channels, frame)?;
let frame = match frame {
Frame::Audio(af) if !self.audio_chain.is_empty() => {
let tb = self.in_time_base;
let t = af
.pts
.map(|p| p as f64 * tb.num as f64 / tb.den.max(1) as f64);
match self.audio_chain.apply(af, t)? {
Some(af) => Frame::Audio(af),
None => return Ok(None),
}
}
other => other,
};
let frame = conform_audio(&mut self.resampler, self.target_rate, frame)?;
let frame = conform_sample_format(self.target_sample_format, frame)?;
let frame = conform_video(
&mut self.pixel_converter,
&self.accepted_formats,
self.source_range,
frame,
)?;
Ok(Some(frame))
}
fn drain(&mut self, muxer: &mut dyn Muxer, report: &mut TranscodeReport) -> Result<()> {
loop {
match phases::time(&phases::ENCODE, || self.encoder.receive_packet()) {
Ok(mut packet) => {
if self.is_video {
let queued = self.pending_pts.pop_front().flatten();
if packet.pts.is_none() {
packet.pts = queued;
}
}
packet.stream_index = self.out_index;
phases::time(&phases::MUX, || muxer.write_packet(&packet))?;
report.packets_written += 1;
}
Err(Error::Again) | Err(Error::Eof) => break,
Err(e) => return Err(e),
}
}
Ok(())
}
fn finish(&mut self, muxer: &mut dyn Muxer, report: &mut TranscodeReport) -> Result<()> {
self.decoder.flush();
loop {
match phases::time(&phases::DECODE, || self.decoder.receive_frame()) {
Ok(frame) => self.handle_frame(frame, muxer, report)?,
Err(Error::Again) | Err(Error::Eof) => break,
Err(e) => return Err(e),
}
}
if let Some(mut fps) = self.fps.take() {
let held = fps.finish();
self.fps = Some(fps);
if let Some(f) = held {
self.emit(Frame::Video(f), muxer, report)?;
}
}
if let Some(rs) = &mut self.resampler {
let tail = rs.finish();
if !tail.is_empty() {
let frame = f32_frame(tail, rs.out_rate(), rs.channels(), None);
let frame = conform_sample_format(self.target_sample_format, frame)?;
self.encoder.send_frame(&frame)?;
self.drain(muxer, report)?;
}
}
phases::time(&phases::ENCODE, || self.encoder.flush());
self.drain(muxer, report)
}
}
pub fn run(engine: &Engine, spec: &TranscodeSpec) -> Result<TranscodeReport> {
if spec.inputs.is_empty() {
return Err(Error::Option("no input files specified".into()));
}
if spec.outputs.is_empty() {
return Err(Error::Option("no output file specified".into()));
}
if spec.outputs.len() > 1 {
return Err(Error::unsupported("multiple outputs are not supported yet"));
}
let output = &spec.outputs[0];
let overlay_xy = output
.filter_complex
.as_deref()
.map(FilterComplex::parse)
.transpose()?
.and_then(|fc| fc.overlay);
let overlay_input = match overlay_xy {
Some(_) if spec.inputs.len() >= 2 => Some(spec.inputs.len() - 1),
Some(_) => {
return Err(Error::Option(
"filter_complex overlay needs a second input (the overlay image/video)".into(),
))
}
None => None,
};
let t_start = phases::on().then(std::time::Instant::now);
let mut demuxers: Vec<Box<dyn rff_format::Demuxer>> = Vec::new();
let mut input_streams: Vec<Vec<Stream>> = Vec::new();
for input in &spec.inputs {
let (demuxer, streams) = phases::time(&phases::OPEN, || -> Result<_> {
let (in_format, source) = open_input(engine, input)?;
let mut demuxer = source.open_demuxer(engine, &in_format)?;
let streams = demuxer.read_header()?;
Ok((demuxer, streams))
})?;
input_streams.push(streams);
demuxers.push(demuxer);
}
let mut selection = select_streams(&input_streams, output)?;
if let Some(oin) = overlay_input {
selection.retain(|(inp, _)| *inp != oin);
}
if selection.is_empty() {
return Err(Error::unsupported("no streams selected for the output"));
}
let mut output_owned;
let output = if output.video_codec.is_none() {
let format_name = resolve_output_format(engine, output).ok();
let required = match format_name.as_deref() {
Some("yuv4mpegpipe") => Some((CodecId::RawVideo, true)),
Some("webp") => Some((CodecId::Webp, false)),
Some("png") => Some((CodecId::Png, false)),
Some("jpeg") => Some((CodecId::Jpeg, false)),
Some("gif") => Some((CodecId::Gif, false)),
Some("avif") => Some((CodecId::Avif, false)),
Some("jpegxl") => Some((CodecId::Jxl, false)),
_ => None,
};
let apply = match required {
Some((_, true)) => true,
Some((codec, false)) => !selection.iter().any(|&(inp, local)| {
let s = &input_streams[inp][local];
s.media_type == MediaType::Video && s.codec_id == codec
}),
None => false,
};
if apply {
output_owned = output.clone();
output_owned.video_codec = Some(StreamCodec {
codec: required.expect("apply implies required").0,
options: Dictionary::default(),
sample_format: None, });
&output_owned
} else {
output
}
} else {
output
};
let mut per_input_ops: Vec<Vec<StreamOp>> = input_streams
.iter()
.map(|s| (0..s.len()).map(|_| StreamOp::Skip).collect())
.collect();
let mut out_streams: Vec<Stream> = Vec::new();
for (out_index, &(inp, local)) in selection.iter().enumerate() {
let (op, os) = build_op(engine, &input_streams[inp][local], output, out_index)?;
per_input_ops[inp][local] = op;
out_streams.push(os);
}
if let (Some((x, y)), Some(oin)) = (overlay_xy, overlay_input) {
let over = decode_overlay_frame(engine, &mut *demuxers[oin], &input_streams[oin])?;
let vidx = input_streams[0]
.iter()
.position(|s| s.media_type == MediaType::Video)
.ok_or_else(|| Error::Option("filter_complex overlay: input #0 has no video".into()))?;
match &mut per_input_ops[0][vidx] {
StreamOp::Transcode(op) => op.overlay = Some((over, x, y)),
_ => {
return Err(Error::unsupported(
"filter_complex overlay needs input #0's video re-encoded — pass -c:v",
))
}
}
}
let out_format = resolve_output_format(engine, output)?;
let muxer_path_factory = engine
.formats
.by_name(&out_format)
.filter(|fmt| fmt.can_mux())
.ok_or_else(|| Error::MuxerNotFound(out_format.clone()))?
.muxer_path;
let out_path_str = output.path.to_str().unwrap_or_default();
let is_sink_stream = rff_io::is_pipe(out_path_str)
|| rff_io::is_udp(out_path_str)
|| rff_io::is_rtmp(out_path_str);
if !is_sink_stream && !output.overwrite && output.path.exists() {
return Err(Error::Option(format!(
"{} already exists (pass -y to overwrite)",
output.path.display()
)));
}
let mut muxer: Box<dyn Muxer> = match muxer_path_factory {
Some(factory) => factory(&output.path, &output.format_options)?,
None => match rff_io::create_seekable(out_path_str)? {
Some(file) => engine
.formats
.open_muxer_seekable(&out_format, Box::new(file))?,
None => engine
.formats
.open_muxer(&out_format, rff_io::create(out_path_str)?)?,
},
};
if !output.metadata.is_empty() {
muxer.set_metadata(&output.metadata);
}
muxer.write_header(&out_streams)?;
let mut report = TranscodeReport::default();
for (demuxer, ops) in demuxers.iter_mut().zip(per_input_ops.iter_mut()) {
loop {
match phases::time(&phases::DEMUX, || demuxer.read_packet()) {
Ok(packet) => process_packet(packet, ops, &mut *muxer, &mut report)?,
Err(Error::Eof) => break,
Err(e) => return Err(e),
}
}
flush_streams(ops, &mut *muxer, &mut report)?;
}
phases::time(&phases::MUX, || muxer.write_trailer())?;
if let Some(t) = t_start {
phases::report(t.elapsed());
}
Ok(report)
}
fn select_streams(inputs: &[Vec<Stream>], output: &OutputSpec) -> Result<Vec<(usize, usize)>> {
let mut selection = Vec::new();
if output.maps.is_empty() {
for (ii, streams) in inputs.iter().enumerate() {
for (i, s) in streams.iter().enumerate() {
if matches!(
s.media_type,
MediaType::Video | MediaType::Audio | MediaType::Subtitle
) {
selection.push((ii, i));
}
}
}
} else {
for map in &output.maps {
let streams = inputs
.get(map.input)
.ok_or_else(|| Error::Option(format!("-map: no input #{}", map.input)))?;
for (i, s) in streams.iter().enumerate() {
let hit = match &map.selector {
MapSelector::All => true,
MapSelector::Kind(k) => s.media_type == *k,
MapSelector::Index(idx) => i == *idx,
};
if hit {
selection.push((map.input, i));
}
}
}
}
Ok(selection)
}
fn build_op(
engine: &Engine,
stream: &Stream,
output: &OutputSpec,
out_index: usize,
) -> Result<(StreamOp, Stream)> {
let requested = match stream.media_type {
MediaType::Video => output.video_codec.as_ref(),
MediaType::Audio => output.audio_codec.as_ref(),
_ => None,
};
match requested {
Some(target) => {
let mut decoder = engine.codecs.find_decoder(stream.codec_id)?;
decoder.configure(&codec_params(stream))?;
let mut encoder = engine.codecs.find_encoder(target.codec)?;
encoder.configure(&target.options)?; let mut filters = if stream.media_type == MediaType::Video {
FilterChain::parse(output.video_filters.as_deref().unwrap_or(""))?
} else {
FilterChain::default()
};
let mut source_range = ColorRange::Unspecified;
if stream.media_type == MediaType::Video {
source_range = effective_color_range(stream);
if is_full_range_codec(target.codec) && source_range == ColorRange::Limited {
filters.prepend_range_conversion(true);
source_range = ColorRange::Full;
}
filters.set_source_color_range(source_range);
}
let (out_w, out_h) = filters.output_dims(stream.width, stream.height);
let is_video = stream.media_type == MediaType::Video;
let is_audio = stream.media_type == MediaType::Audio;
let audio_chain = if is_audio {
rff_filter::AudioFilterChain::parse(output.audio_filters.as_deref().unwrap_or(""))?
} else {
rff_filter::AudioFilterChain::default()
};
let target_channels = match output.audio_channels {
Some(c) if is_audio => {
if !(1..=2).contains(&c) {
return Err(Error::unsupported(format!(
"-ac {c}: only 1 or 2 channels supported"
)));
}
c
}
_ => 0,
};
let mut target_rate = 0;
let mut out_rate = stream.sample_rate;
if is_audio && stream.sample_rate > 0 {
let desired = output
.audio_rate
.or(audio_chain.resample_target())
.unwrap_or(stream.sample_rate);
let desired = match encoder.accepted_sample_rates() {
Some(rates) if !rates.contains(&desired) => nearest_rate(&rates, desired),
_ => desired,
};
if desired != stream.sample_rate {
target_rate = desired;
}
out_rate = desired;
}
let trim = Trim::from_spec(output);
let tb = stream.time_base;
let shift_ticks = trim
.map(|t| (t.start * tb.den as f64 / tb.num.max(1) as f64).round() as i64)
.unwrap_or(0);
let fps = match output.frame_rate {
Some((num, den)) if is_video => Some(FpsConv::new(num, den)),
_ => None,
};
let mut accepted_formats = encoder.accepted_pixel_formats();
let mut os = Stream::new(out_index, target.codec);
os.media_type = stream.media_type;
os.time_base = stream.time_base;
os.width = out_w;
os.height = out_h;
os.pixel_format = stream.pixel_format;
if target.codec == CodecId::RawVideo && stream.media_type == MediaType::Video {
let pinned = os.pixel_format.unwrap_or(PixelFormat::Yuv420p);
os.pixel_format = Some(pinned);
accepted_formats = Some(vec![pinned]);
}
os.color_range = filters.output_color_range().unwrap_or(source_range);
os.sample_rate = out_rate;
os.channels = if target_channels > 0 {
target_channels
} else {
stream.channels
};
if let Some((num, den)) = output.frame_rate {
if is_video {
os.time_base = rff_core::Rational::new(den as i32, num as i32);
}
}
let target_sample_format = target.sample_format;
os.sample_format = target_sample_format
.or(stream.sample_format)
.or(Some(SampleFormat::F32));
let target_sample_format = target_sample_format.or(os.sample_format);
if stream.media_type == MediaType::Audio && out_rate > 0 {
os.time_base = rff_core::Rational::new(1, out_rate as i32);
}
Ok((
StreamOp::Transcode(Box::new(TranscodeOp {
decoder,
encoder,
filters,
overlay: None,
in_time_base: stream.time_base,
is_video,
trim,
shift_ticks,
audio_clock_samples: 0,
fps,
audio_chain,
target_channels,
target_rate,
target_sample_format,
resampler: None,
accepted_formats,
pixel_converter: None,
source_range,
out_index,
max_video_frames: output.max_video_frames,
video_frames_sent: 0,
pending_pts: std::collections::VecDeque::new(),
})),
os,
))
}
None => {
let mut os = stream.clone();
os.index = out_index;
if stream.media_type == MediaType::Subtitle {
if let Some(target) = output.subtitle_codec {
if target.media_type() != MediaType::Subtitle {
return Err(Error::unsupported(format!(
"-c:s: `{}` is not a subtitle codec",
target.name()
)));
}
os.codec_id = target;
}
}
let trim = Trim::from_spec(output);
let tb = stream.time_base;
let to_ticks = |s: f64| (s * tb.den as f64 / tb.num.max(1) as f64).round() as i64;
Ok((
StreamOp::Copy(CopyOp {
out_index,
is_video: stream.media_type == MediaType::Video,
start_ticks: trim.map(|t| to_ticks(t.start)).unwrap_or(0),
end_ticks: trim.and_then(|t| t.end).map(to_ticks),
trimming: trim.is_some(),
started: false,
}),
os,
))
}
}
}
fn decode_overlay_frame(
engine: &Engine,
demuxer: &mut dyn rff_format::Demuxer,
streams: &[Stream],
) -> Result<VideoFrame> {
let vidx = streams
.iter()
.position(|s| s.media_type == MediaType::Video)
.ok_or_else(|| {
Error::Option("filter_complex overlay: overlay input has no video".into())
})?;
let mut decoder = engine.codecs.find_decoder(streams[vidx].codec_id)?;
decoder.configure(&codec_params(&streams[vidx]))?;
let mut to_yuv = FilterChain::parse("format=yuv420p")?;
let mut got_eof = false;
loop {
let frame = match demuxer.read_packet() {
Ok(pkt) if pkt.stream_index as usize != vidx => continue,
Ok(pkt) => {
decoder.send_packet(&pkt)?;
decoder.receive_frame()
}
Err(Error::Eof) if !got_eof => {
got_eof = true;
decoder.flush();
decoder.receive_frame()
}
Err(e) => return Err(e),
};
match frame {
Ok(Frame::Video(v)) => return to_yuv.apply(v),
Ok(_) => continue,
Err(Error::Again) if !got_eof => continue,
Err(Error::Again) | Err(Error::Eof) => {
return Err(Error::Option(
"filter_complex overlay: no decodable frame in the overlay input".into(),
))
}
Err(e) => return Err(e),
}
}
}
fn codec_params(s: &Stream) -> CodecParams {
CodecParams {
codec_id: s.codec_id,
width: s.width,
height: s.height,
pixel_format: s.pixel_format,
sample_rate: s.sample_rate,
channels: s.channels,
sample_format: s.sample_format,
extradata: s.extradata.clone(),
}
}
fn process_packet(
packet: Packet,
ops: &mut [StreamOp],
muxer: &mut dyn Muxer,
report: &mut TranscodeReport,
) -> Result<()> {
let Some(op) = ops.get_mut(packet.stream_index) else {
return Ok(()); };
match op {
StreamOp::Skip => Ok(()),
StreamOp::Copy(copy) => {
if let Some(packet) = copy.process(packet) {
muxer.write_packet(&packet)?;
report.packets_written += 1;
}
Ok(())
}
StreamOp::Transcode(op) => {
phases::time(&phases::DECODE, || op.decoder.send_packet(&packet))?;
loop {
match phases::time(&phases::DECODE, || op.decoder.receive_frame()) {
Ok(frame) => op.handle_frame(frame, muxer, report)?,
Err(Error::Again) | Err(Error::Eof) => break,
Err(e) => return Err(e),
}
}
Ok(())
}
}
}
fn flush_streams(
ops: &mut [StreamOp],
muxer: &mut dyn Muxer,
report: &mut TranscodeReport,
) -> Result<()> {
for op in ops.iter_mut() {
if let StreamOp::Transcode(op) = op {
op.finish(muxer, report)?;
}
}
Ok(())
}
fn open_input(engine: &Engine, input: &InputSpec) -> Result<(String, crate::probe::Source)> {
let path = input
.path
.to_str()
.ok_or_else(|| Error::Option("input path is not valid UTF-8".into()))?;
crate::probe::open_source(engine, path, input.format.as_deref())
}
fn resolve_output_format(engine: &Engine, output: &OutputSpec) -> Result<String> {
if let Some(forced) = &output.format {
return Ok(forced.clone());
}
if output.path.to_str().is_some_and(rff_io::is_rtmp) {
return Ok("flv".to_string());
}
let ext = output
.path
.extension()
.and_then(|e| e.to_str())
.unwrap_or_default();
engine
.formats
.by_extension(ext)
.map(|f| f.name.to_string())
.ok_or_else(|| {
Error::MuxerNotFound(format!(
"{} (no format for this extension — pass -f)",
output.path.display()
))
})
}
#[cfg(test)]
mod sample_format_tests {
use super::*;
fn f32_af(samples: &[f32]) -> Frame {
f32_frame(samples.to_vec(), 44_100, 1, Some(0))
}
#[test]
fn pinned_format_converts_f32_to_s16() {
let src = [0.0f32, 0.5, -0.5, 1.0, -1.0];
let out = conform_sample_format(Some(SampleFormat::S16), f32_af(&src)).unwrap();
let Frame::Audio(af) = out else {
unreachable!()
};
assert_eq!(af.format, SampleFormat::S16);
assert_eq!(af.planes[0].len(), src.len() * 2, "s16 is 2 bytes/sample");
let got: Vec<i16> = af.planes[0]
.chunks_exact(2)
.map(|b| i16::from_le_bytes([b[0], b[1]]))
.collect();
assert_eq!(got, vec![0, 16384, -16384, 32767, -32768]);
}
#[test]
fn unpinned_or_matching_format_is_a_passthrough() {
let src = [0.25f32, -0.25];
let Frame::Audio(a) = conform_sample_format(None, f32_af(&src)).unwrap() else {
unreachable!()
};
assert_eq!(a.format, SampleFormat::F32);
let Frame::Audio(b) = conform_sample_format(Some(SampleFormat::F32), f32_af(&src)).unwrap()
else {
unreachable!()
};
assert_eq!(b.format, SampleFormat::F32);
assert_eq!(b.planes[0], a.planes[0]);
}
#[test]
fn codec_name_pins_the_sample_format() {
assert_eq!(
CodecId::sample_format_from_name("pcm_s16le"),
Some(SampleFormat::S16)
);
assert_eq!(
CodecId::sample_format_from_name("pcm_f32le"),
Some(SampleFormat::F32)
);
assert_eq!(CodecId::sample_format_from_name("pcm"), None);
assert_eq!(CodecId::sample_format_from_name("mp3"), None);
}
}