1use std::io::Read;
23use std::path::PathBuf;
24
25use rff_codec::{CodecParams, Decoder, Encoder};
26use rff_core::{
27 AudioFrame, CodecId, ColorRange, Dictionary, Error, Frame, MediaType, Packet, PixelFormat,
28 Result, SampleFormat, VideoFrame,
29};
30use rff_filter::{FilterChain, FilterComplex};
31
32use rff_format::{Muxer, Stream};
33use rff_resample::Resampler;
34
35use crate::Engine;
36
37mod phases {
44 use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
45 use std::sync::OnceLock;
46
47 pub static OPEN: AtomicU64 = AtomicU64::new(0);
48 pub static DEMUX: AtomicU64 = AtomicU64::new(0);
49 pub static DECODE: AtomicU64 = AtomicU64::new(0);
50 pub static CONFORM: AtomicU64 = AtomicU64::new(0);
51 pub static ENCODE: AtomicU64 = AtomicU64::new(0);
52 pub static MUX: AtomicU64 = AtomicU64::new(0);
53
54 pub fn on() -> bool {
55 static ON: OnceLock<bool> = OnceLock::new();
56 *ON.get_or_init(|| std::env::var_os("RFF_PHASES").is_some())
57 }
58
59 pub fn time<T>(acc: &AtomicU64, f: impl FnOnce() -> T) -> T {
61 if !on() {
62 return f();
63 }
64 let t = std::time::Instant::now();
65 let r = f();
66 acc.fetch_add(t.elapsed().as_nanos() as u64, Relaxed);
67 r
68 }
69
70 pub fn report(total: std::time::Duration) {
71 let total_ns = total.as_nanos().max(1) as f64;
72 eprintln!("--- rff phases (total {:.1} ms) ---", total_ns / 1e6);
73 let mut sum = 0u64;
74 for (name, acc) in [
75 ("open", &OPEN),
76 ("demux", &DEMUX),
77 ("decode", &DECODE),
78 ("conform", &CONFORM),
79 ("encode", &ENCODE),
80 ("mux", &MUX),
81 ] {
82 let ns = acc.swap(0, Relaxed);
83 sum += ns;
84 eprintln!(
85 " {name:<8} {:>9.1} ms {:>5.1}%",
86 ns as f64 / 1e6,
87 100.0 * ns as f64 / total_ns
88 );
89 }
90 let rest = total_ns - sum as f64;
91 eprintln!(
92 " {:<8} {:>9.1} ms {:>5.1}%",
93 "other",
94 rest / 1e6,
95 100.0 * rest / total_ns
96 );
97 }
98}
99
100fn is_full_range_codec(codec: CodecId) -> bool {
104 matches!(
105 codec,
106 CodecId::Jpeg | CodecId::Png | CodecId::Gif | CodecId::Webp
107 )
108}
109
110fn effective_color_range(stream: &Stream) -> ColorRange {
117 match stream.color_range {
118 ColorRange::Unspecified if is_full_range_codec(stream.codec_id) => ColorRange::Full,
119 ColorRange::Unspecified => ColorRange::Limited,
120 explicit => explicit,
121 }
122}
123
124#[derive(Debug, Clone)]
126pub struct InputSpec {
127 pub path: PathBuf,
128 pub format: Option<String>,
131}
132
133#[derive(Debug, Clone)]
135pub struct StreamCodec {
136 pub codec: CodecId,
137 pub options: Dictionary,
139 pub sample_format: Option<SampleFormat>,
143}
144
145#[derive(Debug, Clone)]
147pub enum MapSelector {
148 All,
150 Kind(MediaType),
152 Index(usize),
154}
155
156#[derive(Debug, Clone)]
158pub struct MapSpec {
159 pub input: usize,
160 pub selector: MapSelector,
161}
162
163#[derive(Debug, Clone, Default)]
165pub struct OutputSpec {
166 pub path: PathBuf,
167 pub format: Option<String>,
170 pub video_codec: Option<StreamCodec>,
172 pub audio_codec: Option<StreamCodec>,
174 pub subtitle_codec: Option<CodecId>,
178 pub video_filters: Option<String>,
181 pub filter_complex: Option<String>,
184 pub maps: Vec<MapSpec>,
187 pub overwrite: bool,
189 pub max_video_frames: Option<u64>,
195 pub trim_start: Option<f64>,
199 pub trim_end: Option<f64>,
201 pub frame_rate: Option<(u32, u32)>,
204 pub audio_rate: Option<u32>,
207 pub audio_channels: Option<u16>,
209 pub audio_filters: Option<String>,
211 pub metadata: Dictionary,
213 pub format_options: Dictionary,
216}
217
218#[derive(Debug, Clone, Default)]
220pub struct TranscodeSpec {
221 pub inputs: Vec<InputSpec>,
222 pub outputs: Vec<OutputSpec>,
223}
224
225#[derive(Debug, Clone, Default)]
228pub struct TranscodeReport {
229 pub packets_written: u64,
230 pub frames_decoded: u64,
231}
232
233#[derive(Clone, Copy)]
235struct Trim {
236 start: f64,
237 end: Option<f64>,
238}
239
240impl Trim {
241 fn from_spec(o: &OutputSpec) -> Option<Trim> {
242 if o.trim_start.is_none() && o.trim_end.is_none() {
243 return None;
244 }
245 Some(Trim {
246 start: o.trim_start.unwrap_or(0.0).max(0.0),
247 end: o.trim_end,
248 })
249 }
250
251 fn keeps(&self, t: f64) -> bool {
252 t >= self.start - 1e-9 && self.end.map_or(true, |e| t < e - 1e-9)
253 }
254}
255
256struct FpsConv {
261 num: u32,
262 den: u32,
263 next_slot: i64,
264 last: Option<VideoFrame>,
265}
266
267impl FpsConv {
268 fn new(num: u32, den: u32) -> FpsConv {
269 FpsConv {
270 num: num.max(1),
271 den: den.max(1),
272 next_slot: 0,
273 last: None,
274 }
275 }
276
277 fn slot_time(&self, slot: i64) -> f64 {
278 slot as f64 * self.den as f64 / self.num as f64
279 }
280
281 fn push(&mut self, frame: VideoFrame, t: f64) -> Vec<VideoFrame> {
284 let mut out = Vec::new();
285 while let Some(last) = &self.last {
286 if self.slot_time(self.next_slot) < t - 1e-9 {
287 let mut f = last.clone();
288 f.pts = Some(self.next_slot);
289 out.push(f);
290 self.next_slot += 1;
291 } else {
292 break;
293 }
294 }
295 self.last = Some(frame);
296 out
297 }
298
299 fn finish(&mut self) -> Option<VideoFrame> {
301 self.last.take().map(|mut f| {
302 f.pts = Some(self.next_slot);
303 self.next_slot += 1;
304 f
305 })
306 }
307}
308
309enum StreamOp {
311 Skip,
313 Copy(CopyOp),
315 Transcode(Box<TranscodeOp>),
317}
318
319struct CopyOp {
323 out_index: usize,
324 is_video: bool,
325 start_ticks: i64,
327 end_ticks: Option<i64>,
328 trimming: bool,
329 started: bool,
331}
332
333impl CopyOp {
334 fn process(&mut self, mut packet: Packet) -> Option<Packet> {
336 if self.trimming {
337 let ts = packet.pts.or(packet.dts).unwrap_or(0);
338 if ts < self.start_ticks {
339 return None;
340 }
341 if let Some(end) = self.end_ticks {
342 if ts >= end {
343 return None;
344 }
345 }
346 if self.is_video && !self.started {
347 if !packet.flags.keyframe {
348 return None; }
350 self.started = true;
351 }
352 packet.pts = packet.pts.map(|p| p - self.start_ticks);
353 packet.dts = packet.dts.map(|d| d - self.start_ticks);
354 }
355 packet.stream_index = self.out_index;
356 Some(packet)
357 }
358}
359
360struct TranscodeOp {
362 decoder: Box<dyn Decoder>,
363 encoder: Box<dyn Encoder>,
364 filters: FilterChain,
365 overlay: Option<(VideoFrame, u32, u32)>,
368 in_time_base: rff_core::Rational,
370 is_video: bool,
371 trim: Option<Trim>,
373 shift_ticks: i64,
374 audio_clock_samples: u64,
376 fps: Option<FpsConv>,
378 audio_chain: rff_filter::AudioFilterChain,
380 target_channels: u16,
382 target_rate: u32,
384 target_sample_format: Option<SampleFormat>,
386 resampler: Option<Resampler>,
388 accepted_formats: Option<Vec<PixelFormat>>,
390 pixel_converter: Option<FilterChain>,
392 source_range: ColorRange,
394 out_index: usize,
395 max_video_frames: Option<u64>,
397 video_frames_sent: u64,
398 pending_pts: std::collections::VecDeque<Option<i64>>,
402}
403
404fn apply_overlay(overlay: &Option<(VideoFrame, u32, u32)>, frame: Frame) -> Result<Frame> {
407 match (overlay, frame) {
408 (Some((over, x, y)), Frame::Video(v)) => {
409 Ok(Frame::Video(rff_filter::overlay(v, over, *x, *y)?))
410 }
411 (_, frame) => Ok(frame),
412 }
413}
414
415fn apply_filters(filters: &mut FilterChain, frame: Frame) -> Result<Frame> {
418 if filters.is_empty() {
419 return Ok(frame);
420 }
421 match frame {
422 Frame::Video(v) => Ok(Frame::Video(filters.apply(v)?)),
423 other => Ok(other),
424 }
425}
426
427fn nearest_rate(rates: &[u32], target: u32) -> u32 {
429 rates
430 .iter()
431 .copied()
432 .min_by_key(|r| (*r as i64 - target as i64).abs())
433 .unwrap_or(target)
434}
435
436fn audio_to_f32(af: &AudioFrame) -> Result<Vec<f32>> {
438 match af.format {
439 SampleFormat::F32 => Ok(af.planes[0]
440 .chunks_exact(4)
441 .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
442 .collect()),
443 SampleFormat::S16 => Ok(af.planes[0]
444 .chunks_exact(2)
445 .map(|b| i16::from_le_bytes([b[0], b[1]]) as f32 / 32768.0)
446 .collect()),
447 other => Err(Error::unsupported(format!(
448 "resample: sample format `{}` (need interleaved s16/f32)",
449 other.name()
450 ))),
451 }
452}
453
454fn f32_le_bytes(samples: &[f32]) -> Vec<u8> {
458 let mut bytes = vec![0u8; samples.len() * 4];
459 for (d, s) in bytes.chunks_exact_mut(4).zip(samples) {
460 d.copy_from_slice(&s.to_le_bytes());
461 }
462 bytes
463}
464
465fn f32_to_s16(s: f32) -> i16 {
468 (s * 32768.0).round().clamp(-32768.0, 32767.0) as i16
469}
470
471fn f32_frame(samples: Vec<f32>, rate: u32, channels: u16, pts: Option<i64>) -> Frame {
473 let bytes = f32_le_bytes(&samples);
474 Frame::Audio(AudioFrame {
475 sample_rate: rate,
476 channels,
477 format: SampleFormat::F32,
478 samples: samples.len() / channels.max(1) as usize,
479 planes: vec![bytes],
480 pts,
481 })
482}
483
484fn conform_sample_format(target: Option<SampleFormat>, frame: Frame) -> Result<Frame> {
489 let Some(target) = target else {
490 return Ok(frame);
491 };
492 let Frame::Audio(af) = frame else {
493 return Ok(frame);
494 };
495 if af.format == target {
496 return Ok(Frame::Audio(af));
497 }
498 let src = &af.planes[0];
504 let plane = match (af.format, target) {
505 (SampleFormat::F32, SampleFormat::S16) => {
506 let mut out = vec![0u8; src.len() / 4 * 2];
507 for (d, b) in out.chunks_exact_mut(2).zip(src.chunks_exact(4)) {
508 let s = f32::from_le_bytes([b[0], b[1], b[2], b[3]]);
509 d.copy_from_slice(&f32_to_s16(s).to_le_bytes());
510 }
511 out
512 }
513 (SampleFormat::S16, SampleFormat::F32) => {
514 let mut out = vec![0u8; src.len() / 2 * 4];
515 for (d, b) in out.chunks_exact_mut(4).zip(src.chunks_exact(2)) {
516 let s = i16::from_le_bytes([b[0], b[1]]) as f32 / 32768.0;
517 d.copy_from_slice(&s.to_le_bytes());
518 }
519 out
520 }
521 _ => {
522 audio_to_f32(&af)?;
524 return Err(Error::unsupported(format!(
525 "sample format conversion to `{}` (only interleaved s16/f32)",
526 target.name()
527 )));
528 }
529 };
530 let planes = vec![plane];
531 Ok(Frame::Audio(AudioFrame {
532 sample_rate: af.sample_rate,
533 channels: af.channels,
534 format: target,
535 samples: af.samples,
536 planes,
537 pts: af.pts,
538 }))
539}
540
541fn conform_audio(
544 resampler: &mut Option<Resampler>,
545 target_rate: u32,
546 frame: Frame,
547) -> Result<Frame> {
548 if target_rate == 0 {
549 return Ok(frame);
550 }
551 let Frame::Audio(af) = frame else {
552 return Ok(frame);
553 };
554 if af.sample_rate == target_rate {
555 return Ok(Frame::Audio(af));
556 }
557 let rs =
558 resampler.get_or_insert_with(|| Resampler::new(af.sample_rate, target_rate, af.channels));
559 let out = rs.process(&audio_to_f32(&af)?);
560 let pts = af
561 .pts
562 .map(|p| p * target_rate as i64 / af.sample_rate.max(1) as i64);
563 Ok(f32_frame(out, target_rate, af.channels, pts))
564}
565
566fn conform_video(
574 converter: &mut Option<FilterChain>,
575 accepted: &Option<Vec<PixelFormat>>,
576 source_range: ColorRange,
577 frame: Frame,
578) -> Result<Frame> {
579 let Some(accepted) = accepted else {
580 return Ok(frame);
581 };
582 let Frame::Video(vf) = frame else {
583 return Ok(frame);
584 };
585 if accepted.is_empty() || accepted.contains(&vf.format) {
586 return Ok(Frame::Video(vf));
587 }
588 if converter.is_none() {
589 let target = accepted
591 .iter()
592 .find(|f| matches!(f, PixelFormat::Rgb24 | PixelFormat::Yuv420p))
593 .ok_or_else(|| {
594 Error::unsupported(format!(
595 "no conversion from `{}` to any format this encoder accepts",
596 vf.format.name()
597 ))
598 })?;
599 let range = match source_range {
602 ColorRange::Limited => "limited",
603 _ => "full",
604 };
605 *converter = Some(FilterChain::parse(&format!(
606 "format={}:bt601:{}",
607 target.name(),
608 range
609 ))?);
610 }
611 let chain = converter.as_mut().expect("just built");
612 Ok(Frame::Video(chain.apply(vf)?))
613}
614
615fn conform_channels(target: u16, frame: Frame) -> Result<Frame> {
618 if target == 0 {
619 return Ok(frame);
620 }
621 let Frame::Audio(af) = frame else {
622 return Ok(frame);
623 };
624 if af.channels == target {
625 return Ok(Frame::Audio(af));
626 }
627 let samples = audio_to_f32(&af)?;
628 let mixed: Vec<f32> = match (af.channels, target) {
629 (2, 1) => samples
630 .chunks_exact(2)
631 .map(|p| 0.5 * (p[0] + p[1]))
632 .collect(),
633 (1, 2) => samples.iter().flat_map(|s| [*s, *s]).collect(),
634 (from, to) => {
635 return Err(Error::unsupported(format!(
636 "-ac: only 1<->2 channel mixing supported (input {from} ch, requested {to})"
637 )))
638 }
639 };
640 Ok(f32_frame(mixed, af.sample_rate, target, af.pts))
641}
642
643impl TranscodeOp {
644 fn frame_time(&self, frame: &Frame) -> Result<f64> {
648 let tb = self.in_time_base;
649 let tick = if tb.num > 0 && tb.den > 0 {
650 tb.num as f64 / tb.den as f64
651 } else {
652 0.001
653 };
654 match frame.pts() {
655 Some(p) => Ok(p as f64 * tick),
656 None => match frame {
657 Frame::Audio(af) => {
658 Ok(self.audio_clock_samples as f64 / af.sample_rate.max(1) as f64)
659 }
660 Frame::Video(_) => Err(Error::unsupported(
661 "-ss/-t/-to/-r need timestamps and this video stream has none",
662 )),
663 },
664 }
665 }
666
667 fn handle_frame(
670 &mut self,
671 frame: Frame,
672 muxer: &mut dyn Muxer,
673 report: &mut TranscodeReport,
674 ) -> Result<()> {
675 report.frames_decoded += 1;
676 let needs_time = self.trim.is_some() || (self.is_video && self.fps.is_some());
677 let t = if needs_time {
678 Some(self.frame_time(&frame)?)
679 } else {
680 None
681 };
682 if let Frame::Audio(af) = &frame {
684 self.audio_clock_samples += af.samples as u64;
685 }
686 let mut frame = frame;
687 if let (Some(trim), Some(t)) = (self.trim, t) {
688 match frame {
689 Frame::Audio(af) => {
692 match rff_filter::trim_audio_frame(af, Some(t), Some(trim.start), trim.end)? {
693 Some(af) => frame = Frame::Audio(af),
694 None => return Ok(()),
695 }
696 }
697 Frame::Video(_) => {
698 if !trim.keeps(t) {
699 return Ok(());
700 }
701 }
702 }
703 }
704 if self.shift_ticks != 0 {
706 match &mut frame {
707 Frame::Video(v) => v.pts = v.pts.map(|p| p - self.shift_ticks),
708 Frame::Audio(a) => a.pts = a.pts.map(|p| p - self.shift_ticks),
709 }
710 }
711 match frame {
712 Frame::Video(v) if self.fps.is_some() => {
713 let t_out = t.expect("fps requires time") - self.trim.map_or(0.0, |w| w.start);
714 let mut fps = self.fps.take().expect("checked");
715 let due = fps.push(v, t_out);
716 self.fps = Some(fps);
717 for f in due {
718 self.emit(Frame::Video(f), muxer, report)?;
719 }
720 Ok(())
721 }
722 other => self.emit(other, muxer, report),
723 }
724 }
725
726 fn emit(
729 &mut self,
730 frame: Frame,
731 muxer: &mut dyn Muxer,
732 report: &mut TranscodeReport,
733 ) -> Result<()> {
734 if let Some(limit) = self.max_video_frames {
735 if matches!(frame, Frame::Video(_)) {
736 if self.video_frames_sent >= limit {
737 return Ok(());
738 }
739 self.video_frames_sent += 1;
740 }
741 }
742 let Some(frame) = phases::time(&phases::CONFORM, || self.conform(frame))? else {
743 return Ok(()); };
745 if self.is_video {
746 self.pending_pts.push_back(frame.pts());
747 }
748 phases::time(&phases::ENCODE, || self.encoder.send_frame(&frame))?;
749 self.drain(muxer, report)
750 }
751
752 fn conform(&mut self, frame: Frame) -> Result<Option<Frame>> {
755 let frame = apply_filters(&mut self.filters, frame)?;
756 let frame = apply_overlay(&self.overlay, frame)?;
757 let frame = conform_channels(self.target_channels, frame)?;
758 let frame = match frame {
759 Frame::Audio(af) if !self.audio_chain.is_empty() => {
760 let tb = self.in_time_base;
761 let t = af
762 .pts
763 .map(|p| p as f64 * tb.num as f64 / tb.den.max(1) as f64);
764 match self.audio_chain.apply(af, t)? {
765 Some(af) => Frame::Audio(af),
766 None => return Ok(None),
767 }
768 }
769 other => other,
770 };
771 let frame = conform_audio(&mut self.resampler, self.target_rate, frame)?;
772 let frame = conform_sample_format(self.target_sample_format, frame)?;
773 let frame = conform_video(
774 &mut self.pixel_converter,
775 &self.accepted_formats,
776 self.source_range,
777 frame,
778 )?;
779 Ok(Some(frame))
780 }
781
782 fn drain(&mut self, muxer: &mut dyn Muxer, report: &mut TranscodeReport) -> Result<()> {
787 loop {
788 match phases::time(&phases::ENCODE, || self.encoder.receive_packet()) {
789 Ok(mut packet) => {
790 if self.is_video {
791 let queued = self.pending_pts.pop_front().flatten();
792 if packet.pts.is_none() {
793 packet.pts = queued;
794 }
795 }
796 packet.stream_index = self.out_index;
797 phases::time(&phases::MUX, || muxer.write_packet(&packet))?;
798 report.packets_written += 1;
799 }
800 Err(Error::Again) | Err(Error::Eof) => break,
801 Err(e) => return Err(e),
802 }
803 }
804 Ok(())
805 }
806
807 fn finish(&mut self, muxer: &mut dyn Muxer, report: &mut TranscodeReport) -> Result<()> {
810 self.decoder.flush();
811 loop {
812 match phases::time(&phases::DECODE, || self.decoder.receive_frame()) {
813 Ok(frame) => self.handle_frame(frame, muxer, report)?,
814 Err(Error::Again) | Err(Error::Eof) => break,
815 Err(e) => return Err(e),
816 }
817 }
818 if let Some(mut fps) = self.fps.take() {
819 let held = fps.finish();
820 self.fps = Some(fps);
821 if let Some(f) = held {
822 self.emit(Frame::Video(f), muxer, report)?;
823 }
824 }
825 if let Some(rs) = &mut self.resampler {
826 let tail = rs.finish();
827 if !tail.is_empty() {
828 let frame = f32_frame(tail, rs.out_rate(), rs.channels(), None);
829 let frame = conform_sample_format(self.target_sample_format, frame)?;
830 self.encoder.send_frame(&frame)?;
831 self.drain(muxer, report)?;
832 }
833 }
834 phases::time(&phases::ENCODE, || self.encoder.flush());
835 self.drain(muxer, report)
836 }
837}
838
839pub fn run(engine: &Engine, spec: &TranscodeSpec) -> Result<TranscodeReport> {
845 if spec.inputs.is_empty() {
846 return Err(Error::Option("no input files specified".into()));
847 }
848 if spec.outputs.is_empty() {
849 return Err(Error::Option("no output file specified".into()));
850 }
851 if spec.outputs.len() > 1 {
852 return Err(Error::unsupported("multiple outputs are not supported yet"));
853 }
854 let output = &spec.outputs[0];
855
856 let overlay_xy = output
859 .filter_complex
860 .as_deref()
861 .map(FilterComplex::parse)
862 .transpose()?
863 .and_then(|fc| fc.overlay);
864 let overlay_input = match overlay_xy {
865 Some(_) if spec.inputs.len() >= 2 => Some(spec.inputs.len() - 1),
866 Some(_) => {
867 return Err(Error::Option(
868 "filter_complex overlay needs a second input (the overlay image/video)".into(),
869 ))
870 }
871 None => None,
872 };
873
874 let t_start = phases::on().then(std::time::Instant::now);
878 let mut demuxers: Vec<Box<dyn rff_format::Demuxer>> = Vec::new();
879 let mut input_streams: Vec<Vec<Stream>> = Vec::new();
880 for input in &spec.inputs {
881 let (demuxer, streams) = phases::time(&phases::OPEN, || -> Result<_> {
882 let (in_format, reader) = open_input(engine, input)?;
883 let mut demuxer = engine.formats.open_demuxer(&in_format, reader)?;
884 let streams = demuxer.read_header()?;
885 Ok((demuxer, streams))
886 })?;
887 input_streams.push(streams);
888 demuxers.push(demuxer);
889 }
890
891 let mut selection = select_streams(&input_streams, output)?;
893 if let Some(oin) = overlay_input {
895 selection.retain(|(inp, _)| *inp != oin);
896 }
897 if selection.is_empty() {
898 return Err(Error::unsupported("no streams selected for the output"));
899 }
900
901 let mut output_owned;
907 let output = if output.video_codec.is_none() {
908 let format_name = resolve_output_format(engine, output).ok();
909 let required = match format_name.as_deref() {
912 Some("yuv4mpegpipe") => Some((CodecId::RawVideo, true)),
913 Some("webp") => Some((CodecId::Webp, false)),
914 Some("png") => Some((CodecId::Png, false)),
915 Some("jpeg") => Some((CodecId::Jpeg, false)),
916 Some("gif") => Some((CodecId::Gif, false)),
917 Some("avif") => Some((CodecId::Avif, false)),
918 Some("jpegxl") => Some((CodecId::Jxl, false)),
919 _ => None,
920 };
921 let apply = match required {
922 Some((_, true)) => true,
923 Some((codec, false)) => !selection.iter().any(|&(inp, local)| {
926 let s = &input_streams[inp][local];
927 s.media_type == MediaType::Video && s.codec_id == codec
928 }),
929 None => false,
930 };
931 if apply {
932 output_owned = output.clone();
933 output_owned.video_codec = Some(StreamCodec {
934 codec: required.expect("apply implies required").0,
935 options: Dictionary::default(),
936 sample_format: None, });
938 &output_owned
939 } else {
940 output
941 }
942 } else {
943 output
944 };
945
946 let mut per_input_ops: Vec<Vec<StreamOp>> = input_streams
948 .iter()
949 .map(|s| (0..s.len()).map(|_| StreamOp::Skip).collect())
950 .collect();
951 let mut out_streams: Vec<Stream> = Vec::new();
952 for (out_index, &(inp, local)) in selection.iter().enumerate() {
953 let (op, os) = build_op(engine, &input_streams[inp][local], output, out_index)?;
954 per_input_ops[inp][local] = op;
955 out_streams.push(os);
956 }
957
958 if let (Some((x, y)), Some(oin)) = (overlay_xy, overlay_input) {
961 let over = decode_overlay_frame(engine, &mut *demuxers[oin], &input_streams[oin])?;
962 let vidx = input_streams[0]
963 .iter()
964 .position(|s| s.media_type == MediaType::Video)
965 .ok_or_else(|| Error::Option("filter_complex overlay: input #0 has no video".into()))?;
966 match &mut per_input_ops[0][vidx] {
967 StreamOp::Transcode(op) => op.overlay = Some((over, x, y)),
968 _ => {
969 return Err(Error::unsupported(
970 "filter_complex overlay needs input #0's video re-encoded — pass -c:v",
971 ))
972 }
973 }
974 }
975
976 let out_format = resolve_output_format(engine, output)?;
980 let muxer_path_factory = engine
981 .formats
982 .by_name(&out_format)
983 .filter(|fmt| fmt.can_mux())
984 .ok_or_else(|| Error::MuxerNotFound(out_format.clone()))?
985 .muxer_path;
986
987 let out_path_str = output.path.to_str().unwrap_or_default();
990 let is_sink_stream = rff_io::is_pipe(out_path_str)
991 || rff_io::is_udp(out_path_str)
992 || rff_io::is_rtmp(out_path_str);
993 if !is_sink_stream && !output.overwrite && output.path.exists() {
994 return Err(Error::Option(format!(
995 "{} already exists (pass -y to overwrite)",
996 output.path.display()
997 )));
998 }
999 let mut muxer: Box<dyn Muxer> = match muxer_path_factory {
1000 Some(factory) => factory(&output.path, &output.format_options)?,
1001 None => engine
1002 .formats
1003 .open_muxer(&out_format, rff_io::create(out_path_str)?)?,
1004 };
1005 if !output.metadata.is_empty() {
1006 muxer.set_metadata(&output.metadata);
1007 }
1008 muxer.write_header(&out_streams)?;
1009
1010 let mut report = TranscodeReport::default();
1011
1012 for (demuxer, ops) in demuxers.iter_mut().zip(per_input_ops.iter_mut()) {
1014 loop {
1015 match phases::time(&phases::DEMUX, || demuxer.read_packet()) {
1016 Ok(packet) => process_packet(packet, ops, &mut *muxer, &mut report)?,
1017 Err(Error::Eof) => break,
1018 Err(e) => return Err(e),
1019 }
1020 }
1021 flush_streams(ops, &mut *muxer, &mut report)?;
1022 }
1023 phases::time(&phases::MUX, || muxer.write_trailer())?;
1024 if let Some(t) = t_start {
1025 phases::report(t.elapsed());
1026 }
1027
1028 Ok(report)
1029}
1030
1031fn select_streams(inputs: &[Vec<Stream>], output: &OutputSpec) -> Result<Vec<(usize, usize)>> {
1035 let mut selection = Vec::new();
1036 if output.maps.is_empty() {
1037 for (ii, streams) in inputs.iter().enumerate() {
1038 for (i, s) in streams.iter().enumerate() {
1039 if matches!(
1040 s.media_type,
1041 MediaType::Video | MediaType::Audio | MediaType::Subtitle
1042 ) {
1043 selection.push((ii, i));
1044 }
1045 }
1046 }
1047 } else {
1048 for map in &output.maps {
1049 let streams = inputs
1050 .get(map.input)
1051 .ok_or_else(|| Error::Option(format!("-map: no input #{}", map.input)))?;
1052 for (i, s) in streams.iter().enumerate() {
1053 let hit = match &map.selector {
1054 MapSelector::All => true,
1055 MapSelector::Kind(k) => s.media_type == *k,
1056 MapSelector::Index(idx) => i == *idx,
1057 };
1058 if hit {
1059 selection.push((map.input, i));
1060 }
1061 }
1062 }
1063 }
1064 Ok(selection)
1065}
1066
1067fn build_op(
1070 engine: &Engine,
1071 stream: &Stream,
1072 output: &OutputSpec,
1073 out_index: usize,
1074) -> Result<(StreamOp, Stream)> {
1075 let requested = match stream.media_type {
1076 MediaType::Video => output.video_codec.as_ref(),
1077 MediaType::Audio => output.audio_codec.as_ref(),
1078 _ => None,
1079 };
1080 match requested {
1081 Some(target) => {
1083 let mut decoder = engine.codecs.find_decoder(stream.codec_id)?;
1084 decoder.configure(&codec_params(stream))?;
1085 let mut encoder = engine.codecs.find_encoder(target.codec)?;
1086 encoder.configure(&target.options)?; let mut filters = if stream.media_type == MediaType::Video {
1089 FilterChain::parse(output.video_filters.as_deref().unwrap_or(""))?
1090 } else {
1091 FilterChain::default()
1092 };
1093 let mut source_range = ColorRange::Unspecified;
1100 if stream.media_type == MediaType::Video {
1101 source_range = effective_color_range(stream);
1102 if is_full_range_codec(target.codec) && source_range == ColorRange::Limited {
1103 filters.prepend_range_conversion(true);
1104 source_range = ColorRange::Full;
1105 }
1106 filters.set_source_color_range(source_range);
1107 }
1108 let (out_w, out_h) = filters.output_dims(stream.width, stream.height);
1109 let is_video = stream.media_type == MediaType::Video;
1110 let is_audio = stream.media_type == MediaType::Audio;
1111
1112 let audio_chain = if is_audio {
1114 rff_filter::AudioFilterChain::parse(output.audio_filters.as_deref().unwrap_or(""))?
1115 } else {
1116 rff_filter::AudioFilterChain::default()
1117 };
1118
1119 let target_channels = match output.audio_channels {
1121 Some(c) if is_audio => {
1122 if !(1..=2).contains(&c) {
1123 return Err(Error::unsupported(format!(
1124 "-ac {c}: only 1 or 2 channels supported"
1125 )));
1126 }
1127 c
1128 }
1129 _ => 0,
1130 };
1131
1132 let mut target_rate = 0;
1136 let mut out_rate = stream.sample_rate;
1137 if is_audio && stream.sample_rate > 0 {
1138 let desired = output
1139 .audio_rate
1140 .or(audio_chain.resample_target())
1141 .unwrap_or(stream.sample_rate);
1142 let desired = match encoder.accepted_sample_rates() {
1143 Some(rates) if !rates.contains(&desired) => nearest_rate(&rates, desired),
1144 _ => desired,
1145 };
1146 if desired != stream.sample_rate {
1147 target_rate = desired;
1148 }
1149 out_rate = desired;
1150 }
1151
1152 let trim = Trim::from_spec(output);
1154 let tb = stream.time_base;
1155 let shift_ticks = trim
1156 .map(|t| (t.start * tb.den as f64 / tb.num.max(1) as f64).round() as i64)
1157 .unwrap_or(0);
1158
1159 let fps = match output.frame_rate {
1161 Some((num, den)) if is_video => Some(FpsConv::new(num, den)),
1162 _ => None,
1163 };
1164
1165 let mut accepted_formats = encoder.accepted_pixel_formats();
1166
1167 let mut os = Stream::new(out_index, target.codec);
1168 os.media_type = stream.media_type;
1169 os.time_base = stream.time_base;
1170 os.width = out_w;
1171 os.height = out_h;
1172 os.pixel_format = stream.pixel_format;
1173 if target.codec == CodecId::RawVideo && stream.media_type == MediaType::Video {
1180 let pinned = os.pixel_format.unwrap_or(PixelFormat::Yuv420p);
1181 os.pixel_format = Some(pinned);
1182 accepted_formats = Some(vec![pinned]);
1183 }
1184 os.color_range = filters.output_color_range().unwrap_or(source_range);
1193 os.sample_rate = out_rate;
1194 os.channels = if target_channels > 0 {
1195 target_channels
1196 } else {
1197 stream.channels
1198 };
1199 if let Some((num, den)) = output.frame_rate {
1201 if is_video {
1202 os.time_base = rff_core::Rational::new(den as i32, num as i32);
1203 }
1204 }
1205 let target_sample_format = target.sample_format;
1211 os.sample_format = target_sample_format
1212 .or(stream.sample_format)
1213 .or(Some(SampleFormat::F32));
1214 let target_sample_format = target_sample_format.or(os.sample_format);
1219 if stream.media_type == MediaType::Audio && out_rate > 0 {
1222 os.time_base = rff_core::Rational::new(1, out_rate as i32);
1223 }
1224 Ok((
1225 StreamOp::Transcode(Box::new(TranscodeOp {
1226 decoder,
1227 encoder,
1228 filters,
1229 overlay: None,
1230 in_time_base: stream.time_base,
1231 is_video,
1232 trim,
1233 shift_ticks,
1234 audio_clock_samples: 0,
1235 fps,
1236 audio_chain,
1237 target_channels,
1238 target_rate,
1239 target_sample_format,
1240 resampler: None,
1241 accepted_formats,
1242 pixel_converter: None,
1243 source_range,
1244 out_index,
1245 max_video_frames: output.max_video_frames,
1246 video_frames_sent: 0,
1247 pending_pts: std::collections::VecDeque::new(),
1248 })),
1249 os,
1250 ))
1251 }
1252 None => {
1255 let mut os = stream.clone();
1256 os.index = out_index;
1257 if stream.media_type == MediaType::Subtitle {
1260 if let Some(target) = output.subtitle_codec {
1261 if target.media_type() != MediaType::Subtitle {
1262 return Err(Error::unsupported(format!(
1263 "-c:s: `{}` is not a subtitle codec",
1264 target.name()
1265 )));
1266 }
1267 os.codec_id = target;
1268 }
1269 }
1270 let trim = Trim::from_spec(output);
1271 let tb = stream.time_base;
1272 let to_ticks = |s: f64| (s * tb.den as f64 / tb.num.max(1) as f64).round() as i64;
1273 Ok((
1274 StreamOp::Copy(CopyOp {
1275 out_index,
1276 is_video: stream.media_type == MediaType::Video,
1277 start_ticks: trim.map(|t| to_ticks(t.start)).unwrap_or(0),
1278 end_ticks: trim.and_then(|t| t.end).map(to_ticks),
1279 trimming: trim.is_some(),
1280 started: false,
1281 }),
1282 os,
1283 ))
1284 }
1285 }
1286}
1287
1288fn decode_overlay_frame(
1291 engine: &Engine,
1292 demuxer: &mut dyn rff_format::Demuxer,
1293 streams: &[Stream],
1294) -> Result<VideoFrame> {
1295 let vidx = streams
1296 .iter()
1297 .position(|s| s.media_type == MediaType::Video)
1298 .ok_or_else(|| {
1299 Error::Option("filter_complex overlay: overlay input has no video".into())
1300 })?;
1301 let mut decoder = engine.codecs.find_decoder(streams[vidx].codec_id)?;
1302 decoder.configure(&codec_params(&streams[vidx]))?;
1303 let mut to_yuv = FilterChain::parse("format=yuv420p")?;
1304 let mut got_eof = false;
1305 loop {
1306 let frame = match demuxer.read_packet() {
1307 Ok(pkt) if pkt.stream_index as usize != vidx => continue,
1308 Ok(pkt) => {
1309 decoder.send_packet(&pkt)?;
1310 decoder.receive_frame()
1311 }
1312 Err(Error::Eof) if !got_eof => {
1313 got_eof = true;
1314 decoder.flush();
1315 decoder.receive_frame()
1316 }
1317 Err(e) => return Err(e),
1318 };
1319 match frame {
1320 Ok(Frame::Video(v)) => return to_yuv.apply(v),
1321 Ok(_) => continue,
1322 Err(Error::Again) if !got_eof => continue,
1323 Err(Error::Again) | Err(Error::Eof) => {
1324 return Err(Error::Option(
1325 "filter_complex overlay: no decodable frame in the overlay input".into(),
1326 ))
1327 }
1328 Err(e) => return Err(e),
1329 }
1330 }
1331}
1332
1333fn codec_params(s: &Stream) -> CodecParams {
1335 CodecParams {
1336 codec_id: s.codec_id,
1337 width: s.width,
1338 height: s.height,
1339 pixel_format: s.pixel_format,
1340 sample_rate: s.sample_rate,
1341 channels: s.channels,
1342 sample_format: s.sample_format,
1343 extradata: s.extradata.clone(),
1344 }
1345}
1346
1347fn process_packet(
1349 packet: Packet,
1350 ops: &mut [StreamOp],
1351 muxer: &mut dyn Muxer,
1352 report: &mut TranscodeReport,
1353) -> Result<()> {
1354 let Some(op) = ops.get_mut(packet.stream_index) else {
1355 return Ok(()); };
1357 match op {
1358 StreamOp::Skip => Ok(()),
1359 StreamOp::Copy(copy) => {
1360 if let Some(packet) = copy.process(packet) {
1361 muxer.write_packet(&packet)?;
1362 report.packets_written += 1;
1363 }
1364 Ok(())
1365 }
1366 StreamOp::Transcode(op) => {
1367 phases::time(&phases::DECODE, || op.decoder.send_packet(&packet))?;
1368 loop {
1369 match phases::time(&phases::DECODE, || op.decoder.receive_frame()) {
1370 Ok(frame) => op.handle_frame(frame, muxer, report)?,
1371 Err(Error::Again) | Err(Error::Eof) => break,
1372 Err(e) => return Err(e),
1373 }
1374 }
1375 Ok(())
1376 }
1377 }
1378}
1379
1380fn flush_streams(
1383 ops: &mut [StreamOp],
1384 muxer: &mut dyn Muxer,
1385 report: &mut TranscodeReport,
1386) -> Result<()> {
1387 for op in ops.iter_mut() {
1388 if let StreamOp::Transcode(op) = op {
1389 op.finish(muxer, report)?;
1390 }
1391 }
1392 Ok(())
1393}
1394
1395fn open_input(engine: &Engine, input: &InputSpec) -> Result<(String, Box<dyn Read + Send>)> {
1399 let path = input
1400 .path
1401 .to_str()
1402 .ok_or_else(|| Error::Option("input path is not valid UTF-8".into()))?;
1403 crate::probe::open_source(engine, path, input.format.as_deref())
1404}
1405
1406fn resolve_output_format(engine: &Engine, output: &OutputSpec) -> Result<String> {
1410 if let Some(forced) = &output.format {
1411 return Ok(forced.clone());
1412 }
1413 if output.path.to_str().is_some_and(rff_io::is_rtmp) {
1414 return Ok("flv".to_string());
1415 }
1416 let ext = output
1417 .path
1418 .extension()
1419 .and_then(|e| e.to_str())
1420 .unwrap_or_default();
1421 engine
1422 .formats
1423 .by_extension(ext)
1424 .map(|f| f.name.to_string())
1425 .ok_or_else(|| {
1426 Error::MuxerNotFound(format!(
1427 "{} (no format for this extension — pass -f)",
1428 output.path.display()
1429 ))
1430 })
1431}
1432
1433#[cfg(test)]
1434mod sample_format_tests {
1435 use super::*;
1436
1437 fn f32_af(samples: &[f32]) -> Frame {
1438 f32_frame(samples.to_vec(), 44_100, 1, Some(0))
1439 }
1440
1441 #[test]
1445 fn pinned_format_converts_f32_to_s16() {
1446 let src = [0.0f32, 0.5, -0.5, 1.0, -1.0];
1447 let out = conform_sample_format(Some(SampleFormat::S16), f32_af(&src)).unwrap();
1448 let Frame::Audio(af) = out else {
1449 unreachable!()
1450 };
1451 assert_eq!(af.format, SampleFormat::S16);
1452 assert_eq!(af.planes[0].len(), src.len() * 2, "s16 is 2 bytes/sample");
1453 let got: Vec<i16> = af.planes[0]
1454 .chunks_exact(2)
1455 .map(|b| i16::from_le_bytes([b[0], b[1]]))
1456 .collect();
1457 assert_eq!(got, vec![0, 16384, -16384, 32767, -32768]);
1460 }
1461
1462 #[test]
1465 fn unpinned_or_matching_format_is_a_passthrough() {
1466 let src = [0.25f32, -0.25];
1467 let Frame::Audio(a) = conform_sample_format(None, f32_af(&src)).unwrap() else {
1468 unreachable!()
1469 };
1470 assert_eq!(a.format, SampleFormat::F32);
1471 let Frame::Audio(b) = conform_sample_format(Some(SampleFormat::F32), f32_af(&src)).unwrap()
1472 else {
1473 unreachable!()
1474 };
1475 assert_eq!(b.format, SampleFormat::F32);
1476 assert_eq!(b.planes[0], a.planes[0]);
1477 }
1478
1479 #[test]
1481 fn codec_name_pins_the_sample_format() {
1482 assert_eq!(
1483 CodecId::sample_format_from_name("pcm_s16le"),
1484 Some(SampleFormat::S16)
1485 );
1486 assert_eq!(
1487 CodecId::sample_format_from_name("pcm_f32le"),
1488 Some(SampleFormat::F32)
1489 );
1490 assert_eq!(CodecId::sample_format_from_name("pcm"), None);
1492 assert_eq!(CodecId::sample_format_from_name("mp3"), None);
1493 }
1494}