1use std::path::PathBuf;
23
24use rff_codec::{CodecParams, Decoder, Encoder};
25use rff_core::{
26 AudioFrame, CodecId, ColorRange, Dictionary, Error, Frame, MediaType, Packet, PixelFormat,
27 Result, SampleFormat, VideoFrame,
28};
29use rff_filter::{FilterChain, FilterComplex};
30
31use rff_format::{Muxer, Stream};
32use rff_resample::Resampler;
33
34use crate::Engine;
35
36mod phases {
43 use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
44 use std::sync::OnceLock;
45
46 pub static OPEN: AtomicU64 = AtomicU64::new(0);
47 pub static DEMUX: AtomicU64 = AtomicU64::new(0);
48 pub static DECODE: AtomicU64 = AtomicU64::new(0);
49 pub static CONFORM: AtomicU64 = AtomicU64::new(0);
50 pub static ENCODE: AtomicU64 = AtomicU64::new(0);
51 pub static MUX: AtomicU64 = AtomicU64::new(0);
52
53 pub fn on() -> bool {
54 static ON: OnceLock<bool> = OnceLock::new();
55 *ON.get_or_init(|| std::env::var_os("RFF_PHASES").is_some())
56 }
57
58 pub fn time<T>(acc: &AtomicU64, f: impl FnOnce() -> T) -> T {
60 if !on() {
61 return f();
62 }
63 let t = std::time::Instant::now();
64 let r = f();
65 acc.fetch_add(t.elapsed().as_nanos() as u64, Relaxed);
66 r
67 }
68
69 pub fn report(total: std::time::Duration) {
70 let total_ns = total.as_nanos().max(1) as f64;
71 eprintln!("--- rff phases (total {:.1} ms) ---", total_ns / 1e6);
72 let mut sum = 0u64;
73 for (name, acc) in [
74 ("open", &OPEN),
75 ("demux", &DEMUX),
76 ("decode", &DECODE),
77 ("conform", &CONFORM),
78 ("encode", &ENCODE),
79 ("mux", &MUX),
80 ] {
81 let ns = acc.swap(0, Relaxed);
82 sum += ns;
83 eprintln!(
84 " {name:<8} {:>9.1} ms {:>5.1}%",
85 ns as f64 / 1e6,
86 100.0 * ns as f64 / total_ns
87 );
88 }
89 let rest = total_ns - sum as f64;
90 eprintln!(
91 " {:<8} {:>9.1} ms {:>5.1}%",
92 "other",
93 rest / 1e6,
94 100.0 * rest / total_ns
95 );
96 }
97}
98
99fn is_full_range_codec(codec: CodecId) -> bool {
103 matches!(
104 codec,
105 CodecId::Jpeg | CodecId::Png | CodecId::Gif | CodecId::Webp
106 )
107}
108
109fn effective_color_range(stream: &Stream) -> ColorRange {
116 match stream.color_range {
117 ColorRange::Unspecified if is_full_range_codec(stream.codec_id) => ColorRange::Full,
118 ColorRange::Unspecified => ColorRange::Limited,
119 explicit => explicit,
120 }
121}
122
123#[derive(Debug, Clone)]
125pub struct InputSpec {
126 pub path: PathBuf,
127 pub format: Option<String>,
130}
131
132#[derive(Debug, Clone)]
134pub struct StreamCodec {
135 pub codec: CodecId,
136 pub options: Dictionary,
138 pub sample_format: Option<SampleFormat>,
142}
143
144#[derive(Debug, Clone)]
146pub enum MapSelector {
147 All,
149 Kind(MediaType),
151 Index(usize),
153}
154
155#[derive(Debug, Clone)]
157pub struct MapSpec {
158 pub input: usize,
159 pub selector: MapSelector,
160}
161
162#[derive(Debug, Clone, Default)]
164pub struct OutputSpec {
165 pub path: PathBuf,
166 pub format: Option<String>,
169 pub video_codec: Option<StreamCodec>,
171 pub audio_codec: Option<StreamCodec>,
173 pub subtitle_codec: Option<CodecId>,
177 pub video_filters: Option<String>,
180 pub filter_complex: Option<String>,
183 pub maps: Vec<MapSpec>,
186 pub overwrite: bool,
188 pub max_video_frames: Option<u64>,
194 pub trim_start: Option<f64>,
198 pub trim_end: Option<f64>,
200 pub frame_rate: Option<(u32, u32)>,
203 pub audio_rate: Option<u32>,
206 pub audio_channels: Option<u16>,
208 pub audio_filters: Option<String>,
210 pub metadata: Dictionary,
212 pub format_options: Dictionary,
215}
216
217#[derive(Debug, Clone, Default)]
219pub struct TranscodeSpec {
220 pub inputs: Vec<InputSpec>,
221 pub outputs: Vec<OutputSpec>,
222}
223
224#[derive(Debug, Clone, Default)]
227pub struct TranscodeReport {
228 pub packets_written: u64,
229 pub frames_decoded: u64,
230}
231
232#[derive(Clone, Copy)]
234struct Trim {
235 start: f64,
236 end: Option<f64>,
237}
238
239impl Trim {
240 fn from_spec(o: &OutputSpec) -> Option<Trim> {
241 if o.trim_start.is_none() && o.trim_end.is_none() {
242 return None;
243 }
244 Some(Trim {
245 start: o.trim_start.unwrap_or(0.0).max(0.0),
246 end: o.trim_end,
247 })
248 }
249
250 fn keeps(&self, t: f64) -> bool {
251 t >= self.start - 1e-9 && self.end.map_or(true, |e| t < e - 1e-9)
252 }
253}
254
255struct FpsConv {
260 num: u32,
261 den: u32,
262 next_slot: i64,
263 last: Option<VideoFrame>,
264}
265
266impl FpsConv {
267 fn new(num: u32, den: u32) -> FpsConv {
268 FpsConv {
269 num: num.max(1),
270 den: den.max(1),
271 next_slot: 0,
272 last: None,
273 }
274 }
275
276 fn slot_time(&self, slot: i64) -> f64 {
277 slot as f64 * self.den as f64 / self.num as f64
278 }
279
280 fn push(&mut self, frame: VideoFrame, t: f64) -> Vec<VideoFrame> {
283 let mut out = Vec::new();
284 while let Some(last) = &self.last {
285 if self.slot_time(self.next_slot) < t - 1e-9 {
286 let mut f = last.clone();
287 f.pts = Some(self.next_slot);
288 out.push(f);
289 self.next_slot += 1;
290 } else {
291 break;
292 }
293 }
294 self.last = Some(frame);
295 out
296 }
297
298 fn finish(&mut self) -> Option<VideoFrame> {
300 self.last.take().map(|mut f| {
301 f.pts = Some(self.next_slot);
302 self.next_slot += 1;
303 f
304 })
305 }
306}
307
308enum StreamOp {
310 Skip,
312 Copy(CopyOp),
314 Transcode(Box<TranscodeOp>),
316}
317
318struct CopyOp {
322 out_index: usize,
323 is_video: bool,
324 start_ticks: i64,
326 end_ticks: Option<i64>,
327 trimming: bool,
328 started: bool,
330}
331
332impl CopyOp {
333 fn process(&mut self, mut packet: Packet) -> Option<Packet> {
335 if self.trimming {
336 let ts = packet.pts.or(packet.dts).unwrap_or(0);
337 if ts < self.start_ticks {
338 return None;
339 }
340 if let Some(end) = self.end_ticks {
341 if ts >= end {
342 return None;
343 }
344 }
345 if self.is_video && !self.started {
346 if !packet.flags.keyframe {
347 return None; }
349 self.started = true;
350 }
351 packet.pts = packet.pts.map(|p| p - self.start_ticks);
352 packet.dts = packet.dts.map(|d| d - self.start_ticks);
353 }
354 packet.stream_index = self.out_index;
355 Some(packet)
356 }
357}
358
359struct TranscodeOp {
361 decoder: Box<dyn Decoder>,
362 encoder: Box<dyn Encoder>,
363 filters: FilterChain,
364 overlay: Option<(VideoFrame, u32, u32)>,
367 in_time_base: rff_core::Rational,
369 is_video: bool,
370 trim: Option<Trim>,
372 shift_ticks: i64,
373 audio_clock_samples: u64,
375 fps: Option<FpsConv>,
377 audio_chain: rff_filter::AudioFilterChain,
379 target_channels: u16,
381 target_rate: u32,
383 target_sample_format: Option<SampleFormat>,
385 resampler: Option<Resampler>,
387 accepted_formats: Option<Vec<PixelFormat>>,
389 pixel_converter: Option<FilterChain>,
391 source_range: ColorRange,
393 out_index: usize,
394 max_video_frames: Option<u64>,
396 video_frames_sent: u64,
397 pending_pts: std::collections::VecDeque<Option<i64>>,
401}
402
403fn apply_overlay(overlay: &Option<(VideoFrame, u32, u32)>, frame: Frame) -> Result<Frame> {
406 match (overlay, frame) {
407 (Some((over, x, y)), Frame::Video(v)) => {
408 Ok(Frame::Video(rff_filter::overlay(v, over, *x, *y)?))
409 }
410 (_, frame) => Ok(frame),
411 }
412}
413
414fn apply_filters(filters: &mut FilterChain, frame: Frame) -> Result<Frame> {
417 if filters.is_empty() {
418 return Ok(frame);
419 }
420 match frame {
421 Frame::Video(v) => Ok(Frame::Video(filters.apply(v)?)),
422 other => Ok(other),
423 }
424}
425
426fn nearest_rate(rates: &[u32], target: u32) -> u32 {
428 rates
429 .iter()
430 .copied()
431 .min_by_key(|r| (*r as i64 - target as i64).abs())
432 .unwrap_or(target)
433}
434
435fn audio_to_f32(af: &AudioFrame) -> Result<Vec<f32>> {
437 match af.format {
438 SampleFormat::F32 => Ok(af.planes[0]
439 .chunks_exact(4)
440 .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
441 .collect()),
442 SampleFormat::S16 => Ok(af.planes[0]
443 .chunks_exact(2)
444 .map(|b| i16::from_le_bytes([b[0], b[1]]) as f32 / 32768.0)
445 .collect()),
446 other => Err(Error::unsupported(format!(
447 "resample: sample format `{}` (need interleaved s16/f32)",
448 other.name()
449 ))),
450 }
451}
452
453fn f32_le_bytes(samples: &[f32]) -> Vec<u8> {
457 let mut bytes = vec![0u8; samples.len() * 4];
458 for (d, s) in bytes.chunks_exact_mut(4).zip(samples) {
459 d.copy_from_slice(&s.to_le_bytes());
460 }
461 bytes
462}
463
464fn f32_to_s16(s: f32) -> i16 {
467 (s * 32768.0).round().clamp(-32768.0, 32767.0) as i16
468}
469
470fn f32_frame(samples: Vec<f32>, rate: u32, channels: u16, pts: Option<i64>) -> Frame {
472 let bytes = f32_le_bytes(&samples);
473 Frame::Audio(AudioFrame {
474 sample_rate: rate,
475 channels,
476 format: SampleFormat::F32,
477 samples: samples.len() / channels.max(1) as usize,
478 planes: vec![bytes],
479 pts,
480 })
481}
482
483fn conform_sample_format(target: Option<SampleFormat>, frame: Frame) -> Result<Frame> {
488 let Some(target) = target else {
489 return Ok(frame);
490 };
491 let Frame::Audio(af) = frame else {
492 return Ok(frame);
493 };
494 if af.format == target {
495 return Ok(Frame::Audio(af));
496 }
497 let src = &af.planes[0];
503 let plane = match (af.format, target) {
504 (SampleFormat::F32, SampleFormat::S16) => {
505 let mut out = vec![0u8; src.len() / 4 * 2];
506 for (d, b) in out.chunks_exact_mut(2).zip(src.chunks_exact(4)) {
507 let s = f32::from_le_bytes([b[0], b[1], b[2], b[3]]);
508 d.copy_from_slice(&f32_to_s16(s).to_le_bytes());
509 }
510 out
511 }
512 (SampleFormat::S16, SampleFormat::F32) => {
513 let mut out = vec![0u8; src.len() / 2 * 4];
514 for (d, b) in out.chunks_exact_mut(4).zip(src.chunks_exact(2)) {
515 let s = i16::from_le_bytes([b[0], b[1]]) as f32 / 32768.0;
516 d.copy_from_slice(&s.to_le_bytes());
517 }
518 out
519 }
520 _ => {
521 audio_to_f32(&af)?;
523 return Err(Error::unsupported(format!(
524 "sample format conversion to `{}` (only interleaved s16/f32)",
525 target.name()
526 )));
527 }
528 };
529 let planes = vec![plane];
530 Ok(Frame::Audio(AudioFrame {
531 sample_rate: af.sample_rate,
532 channels: af.channels,
533 format: target,
534 samples: af.samples,
535 planes,
536 pts: af.pts,
537 }))
538}
539
540fn conform_audio(
543 resampler: &mut Option<Resampler>,
544 target_rate: u32,
545 frame: Frame,
546) -> Result<Frame> {
547 if target_rate == 0 {
548 return Ok(frame);
549 }
550 let Frame::Audio(af) = frame else {
551 return Ok(frame);
552 };
553 if af.sample_rate == target_rate {
554 return Ok(Frame::Audio(af));
555 }
556 let rs =
557 resampler.get_or_insert_with(|| Resampler::new(af.sample_rate, target_rate, af.channels));
558 let out = rs.process(&audio_to_f32(&af)?);
559 let pts = af
560 .pts
561 .map(|p| p * target_rate as i64 / af.sample_rate.max(1) as i64);
562 Ok(f32_frame(out, target_rate, af.channels, pts))
563}
564
565fn conform_video(
573 converter: &mut Option<FilterChain>,
574 accepted: &Option<Vec<PixelFormat>>,
575 source_range: ColorRange,
576 frame: Frame,
577) -> Result<Frame> {
578 let Some(accepted) = accepted else {
579 return Ok(frame);
580 };
581 let Frame::Video(vf) = frame else {
582 return Ok(frame);
583 };
584 if accepted.is_empty() || accepted.contains(&vf.format) {
585 return Ok(Frame::Video(vf));
586 }
587 if converter.is_none() {
588 let target = accepted
590 .iter()
591 .find(|f| matches!(f, PixelFormat::Rgb24 | PixelFormat::Yuv420p))
592 .ok_or_else(|| {
593 Error::unsupported(format!(
594 "no conversion from `{}` to any format this encoder accepts",
595 vf.format.name()
596 ))
597 })?;
598 let range = match source_range {
601 ColorRange::Limited => "limited",
602 _ => "full",
603 };
604 *converter = Some(FilterChain::parse(&format!(
605 "format={}:bt601:{}",
606 target.name(),
607 range
608 ))?);
609 }
610 let chain = converter.as_mut().expect("just built");
611 Ok(Frame::Video(chain.apply(vf)?))
612}
613
614fn conform_channels(target: u16, frame: Frame) -> Result<Frame> {
617 if target == 0 {
618 return Ok(frame);
619 }
620 let Frame::Audio(af) = frame else {
621 return Ok(frame);
622 };
623 if af.channels == target {
624 return Ok(Frame::Audio(af));
625 }
626 let samples = audio_to_f32(&af)?;
627 let mixed: Vec<f32> = match (af.channels, target) {
628 (2, 1) => samples
629 .chunks_exact(2)
630 .map(|p| 0.5 * (p[0] + p[1]))
631 .collect(),
632 (1, 2) => samples.iter().flat_map(|s| [*s, *s]).collect(),
633 (from, to) => {
634 return Err(Error::unsupported(format!(
635 "-ac: only 1<->2 channel mixing supported (input {from} ch, requested {to})"
636 )))
637 }
638 };
639 Ok(f32_frame(mixed, af.sample_rate, target, af.pts))
640}
641
642impl TranscodeOp {
643 fn frame_time(&self, frame: &Frame) -> Result<f64> {
647 let tb = self.in_time_base;
648 let tick = if tb.num > 0 && tb.den > 0 {
649 tb.num as f64 / tb.den as f64
650 } else {
651 0.001
652 };
653 match frame.pts() {
654 Some(p) => Ok(p as f64 * tick),
655 None => match frame {
656 Frame::Audio(af) => {
657 Ok(self.audio_clock_samples as f64 / af.sample_rate.max(1) as f64)
658 }
659 Frame::Video(_) => Err(Error::unsupported(
660 "-ss/-t/-to/-r need timestamps and this video stream has none",
661 )),
662 },
663 }
664 }
665
666 fn handle_frame(
669 &mut self,
670 frame: Frame,
671 muxer: &mut dyn Muxer,
672 report: &mut TranscodeReport,
673 ) -> Result<()> {
674 report.frames_decoded += 1;
675 let needs_time = self.trim.is_some() || (self.is_video && self.fps.is_some());
676 let t = if needs_time {
677 Some(self.frame_time(&frame)?)
678 } else {
679 None
680 };
681 if let Frame::Audio(af) = &frame {
683 self.audio_clock_samples += af.samples as u64;
684 }
685 let mut frame = frame;
686 if let (Some(trim), Some(t)) = (self.trim, t) {
687 match frame {
688 Frame::Audio(af) => {
691 match rff_filter::trim_audio_frame(af, Some(t), Some(trim.start), trim.end)? {
692 Some(af) => frame = Frame::Audio(af),
693 None => return Ok(()),
694 }
695 }
696 Frame::Video(_) => {
697 if !trim.keeps(t) {
698 return Ok(());
699 }
700 }
701 }
702 }
703 if self.shift_ticks != 0 {
705 match &mut frame {
706 Frame::Video(v) => v.pts = v.pts.map(|p| p - self.shift_ticks),
707 Frame::Audio(a) => a.pts = a.pts.map(|p| p - self.shift_ticks),
708 }
709 }
710 match frame {
711 Frame::Video(v) if self.fps.is_some() => {
712 let t_out = t.expect("fps requires time") - self.trim.map_or(0.0, |w| w.start);
713 let mut fps = self.fps.take().expect("checked");
714 let due = fps.push(v, t_out);
715 self.fps = Some(fps);
716 for f in due {
717 self.emit(Frame::Video(f), muxer, report)?;
718 }
719 Ok(())
720 }
721 other => self.emit(other, muxer, report),
722 }
723 }
724
725 fn emit(
728 &mut self,
729 frame: Frame,
730 muxer: &mut dyn Muxer,
731 report: &mut TranscodeReport,
732 ) -> Result<()> {
733 if let Some(limit) = self.max_video_frames {
734 if matches!(frame, Frame::Video(_)) {
735 if self.video_frames_sent >= limit {
736 return Ok(());
737 }
738 self.video_frames_sent += 1;
739 }
740 }
741 let Some(frame) = phases::time(&phases::CONFORM, || self.conform(frame))? else {
742 return Ok(()); };
744 if self.is_video {
745 self.pending_pts.push_back(frame.pts());
746 }
747 phases::time(&phases::ENCODE, || self.encoder.send_frame(&frame))?;
748 self.drain(muxer, report)
749 }
750
751 fn conform(&mut self, frame: Frame) -> Result<Option<Frame>> {
754 let frame = apply_filters(&mut self.filters, frame)?;
755 let frame = apply_overlay(&self.overlay, frame)?;
756 let frame = conform_channels(self.target_channels, frame)?;
757 let frame = match frame {
758 Frame::Audio(af) if !self.audio_chain.is_empty() => {
759 let tb = self.in_time_base;
760 let t = af
761 .pts
762 .map(|p| p as f64 * tb.num as f64 / tb.den.max(1) as f64);
763 match self.audio_chain.apply(af, t)? {
764 Some(af) => Frame::Audio(af),
765 None => return Ok(None),
766 }
767 }
768 other => other,
769 };
770 let frame = conform_audio(&mut self.resampler, self.target_rate, frame)?;
771 let frame = conform_sample_format(self.target_sample_format, frame)?;
772 let frame = conform_video(
773 &mut self.pixel_converter,
774 &self.accepted_formats,
775 self.source_range,
776 frame,
777 )?;
778 Ok(Some(frame))
779 }
780
781 fn drain(&mut self, muxer: &mut dyn Muxer, report: &mut TranscodeReport) -> Result<()> {
786 loop {
787 match phases::time(&phases::ENCODE, || self.encoder.receive_packet()) {
788 Ok(mut packet) => {
789 if self.is_video {
790 let queued = self.pending_pts.pop_front().flatten();
791 if packet.pts.is_none() {
792 packet.pts = queued;
793 }
794 }
795 packet.stream_index = self.out_index;
796 phases::time(&phases::MUX, || muxer.write_packet(&packet))?;
797 report.packets_written += 1;
798 }
799 Err(Error::Again) | Err(Error::Eof) => break,
800 Err(e) => return Err(e),
801 }
802 }
803 Ok(())
804 }
805
806 fn finish(&mut self, muxer: &mut dyn Muxer, report: &mut TranscodeReport) -> Result<()> {
809 self.decoder.flush();
810 loop {
811 match phases::time(&phases::DECODE, || self.decoder.receive_frame()) {
812 Ok(frame) => self.handle_frame(frame, muxer, report)?,
813 Err(Error::Again) | Err(Error::Eof) => break,
814 Err(e) => return Err(e),
815 }
816 }
817 if let Some(mut fps) = self.fps.take() {
818 let held = fps.finish();
819 self.fps = Some(fps);
820 if let Some(f) = held {
821 self.emit(Frame::Video(f), muxer, report)?;
822 }
823 }
824 if let Some(rs) = &mut self.resampler {
825 let tail = rs.finish();
826 if !tail.is_empty() {
827 let frame = f32_frame(tail, rs.out_rate(), rs.channels(), None);
828 let frame = conform_sample_format(self.target_sample_format, frame)?;
829 self.encoder.send_frame(&frame)?;
830 self.drain(muxer, report)?;
831 }
832 }
833 phases::time(&phases::ENCODE, || self.encoder.flush());
834 self.drain(muxer, report)
835 }
836}
837
838pub fn run(engine: &Engine, spec: &TranscodeSpec) -> Result<TranscodeReport> {
844 if spec.inputs.is_empty() {
845 return Err(Error::Option("no input files specified".into()));
846 }
847 if spec.outputs.is_empty() {
848 return Err(Error::Option("no output file specified".into()));
849 }
850 if spec.outputs.len() > 1 {
851 return Err(Error::unsupported("multiple outputs are not supported yet"));
852 }
853 let output = &spec.outputs[0];
854
855 let overlay_xy = output
858 .filter_complex
859 .as_deref()
860 .map(FilterComplex::parse)
861 .transpose()?
862 .and_then(|fc| fc.overlay);
863 let overlay_input = match overlay_xy {
864 Some(_) if spec.inputs.len() >= 2 => Some(spec.inputs.len() - 1),
865 Some(_) => {
866 return Err(Error::Option(
867 "filter_complex overlay needs a second input (the overlay image/video)".into(),
868 ))
869 }
870 None => None,
871 };
872
873 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, source) = open_input(engine, input)?;
883 let mut demuxer = source.open_demuxer(engine, &in_format)?;
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 => match rff_io::create_seekable(out_path_str)? {
1005 Some(file) => engine
1006 .formats
1007 .open_muxer_seekable(&out_format, Box::new(file))?,
1008 None => engine
1009 .formats
1010 .open_muxer(&out_format, rff_io::create(out_path_str)?)?,
1011 },
1012 };
1013 if !output.metadata.is_empty() {
1014 muxer.set_metadata(&output.metadata);
1015 }
1016 muxer.write_header(&out_streams)?;
1017
1018 let mut report = TranscodeReport::default();
1019
1020 for (demuxer, ops) in demuxers.iter_mut().zip(per_input_ops.iter_mut()) {
1022 loop {
1023 match phases::time(&phases::DEMUX, || demuxer.read_packet()) {
1024 Ok(packet) => process_packet(packet, ops, &mut *muxer, &mut report)?,
1025 Err(Error::Eof) => break,
1026 Err(e) => return Err(e),
1027 }
1028 }
1029 flush_streams(ops, &mut *muxer, &mut report)?;
1030 }
1031 phases::time(&phases::MUX, || muxer.write_trailer())?;
1032 if let Some(t) = t_start {
1033 phases::report(t.elapsed());
1034 }
1035
1036 Ok(report)
1037}
1038
1039fn select_streams(inputs: &[Vec<Stream>], output: &OutputSpec) -> Result<Vec<(usize, usize)>> {
1043 let mut selection = Vec::new();
1044 if output.maps.is_empty() {
1045 for (ii, streams) in inputs.iter().enumerate() {
1046 for (i, s) in streams.iter().enumerate() {
1047 if matches!(
1048 s.media_type,
1049 MediaType::Video | MediaType::Audio | MediaType::Subtitle
1050 ) {
1051 selection.push((ii, i));
1052 }
1053 }
1054 }
1055 } else {
1056 for map in &output.maps {
1057 let streams = inputs
1058 .get(map.input)
1059 .ok_or_else(|| Error::Option(format!("-map: no input #{}", map.input)))?;
1060 for (i, s) in streams.iter().enumerate() {
1061 let hit = match &map.selector {
1062 MapSelector::All => true,
1063 MapSelector::Kind(k) => s.media_type == *k,
1064 MapSelector::Index(idx) => i == *idx,
1065 };
1066 if hit {
1067 selection.push((map.input, i));
1068 }
1069 }
1070 }
1071 }
1072 Ok(selection)
1073}
1074
1075fn build_op(
1078 engine: &Engine,
1079 stream: &Stream,
1080 output: &OutputSpec,
1081 out_index: usize,
1082) -> Result<(StreamOp, Stream)> {
1083 let requested = match stream.media_type {
1084 MediaType::Video => output.video_codec.as_ref(),
1085 MediaType::Audio => output.audio_codec.as_ref(),
1086 _ => None,
1087 };
1088 match requested {
1089 Some(target) => {
1091 let mut decoder = engine.codecs.find_decoder(stream.codec_id)?;
1092 decoder.configure(&codec_params(stream))?;
1093 let mut encoder = engine.codecs.find_encoder(target.codec)?;
1094 encoder.configure(&target.options)?; let mut filters = if stream.media_type == MediaType::Video {
1097 FilterChain::parse(output.video_filters.as_deref().unwrap_or(""))?
1098 } else {
1099 FilterChain::default()
1100 };
1101 let mut source_range = ColorRange::Unspecified;
1108 if stream.media_type == MediaType::Video {
1109 source_range = effective_color_range(stream);
1110 if is_full_range_codec(target.codec) && source_range == ColorRange::Limited {
1111 filters.prepend_range_conversion(true);
1112 source_range = ColorRange::Full;
1113 }
1114 filters.set_source_color_range(source_range);
1115 }
1116 let (out_w, out_h) = filters.output_dims(stream.width, stream.height);
1117 let is_video = stream.media_type == MediaType::Video;
1118 let is_audio = stream.media_type == MediaType::Audio;
1119
1120 let audio_chain = if is_audio {
1122 rff_filter::AudioFilterChain::parse(output.audio_filters.as_deref().unwrap_or(""))?
1123 } else {
1124 rff_filter::AudioFilterChain::default()
1125 };
1126
1127 let target_channels = match output.audio_channels {
1129 Some(c) if is_audio => {
1130 if !(1..=2).contains(&c) {
1131 return Err(Error::unsupported(format!(
1132 "-ac {c}: only 1 or 2 channels supported"
1133 )));
1134 }
1135 c
1136 }
1137 _ => 0,
1138 };
1139
1140 let mut target_rate = 0;
1144 let mut out_rate = stream.sample_rate;
1145 if is_audio && stream.sample_rate > 0 {
1146 let desired = output
1147 .audio_rate
1148 .or(audio_chain.resample_target())
1149 .unwrap_or(stream.sample_rate);
1150 let desired = match encoder.accepted_sample_rates() {
1151 Some(rates) if !rates.contains(&desired) => nearest_rate(&rates, desired),
1152 _ => desired,
1153 };
1154 if desired != stream.sample_rate {
1155 target_rate = desired;
1156 }
1157 out_rate = desired;
1158 }
1159
1160 let trim = Trim::from_spec(output);
1162 let tb = stream.time_base;
1163 let shift_ticks = trim
1164 .map(|t| (t.start * tb.den as f64 / tb.num.max(1) as f64).round() as i64)
1165 .unwrap_or(0);
1166
1167 let fps = match output.frame_rate {
1169 Some((num, den)) if is_video => Some(FpsConv::new(num, den)),
1170 _ => None,
1171 };
1172
1173 let mut accepted_formats = encoder.accepted_pixel_formats();
1174
1175 let mut os = Stream::new(out_index, target.codec);
1176 os.media_type = stream.media_type;
1177 os.time_base = stream.time_base;
1178 os.width = out_w;
1179 os.height = out_h;
1180 os.pixel_format = stream.pixel_format;
1181 if target.codec == CodecId::RawVideo && stream.media_type == MediaType::Video {
1188 let pinned = os.pixel_format.unwrap_or(PixelFormat::Yuv420p);
1189 os.pixel_format = Some(pinned);
1190 accepted_formats = Some(vec![pinned]);
1191 }
1192 os.color_range = filters.output_color_range().unwrap_or(source_range);
1201 os.sample_rate = out_rate;
1202 os.channels = if target_channels > 0 {
1203 target_channels
1204 } else {
1205 stream.channels
1206 };
1207 if let Some((num, den)) = output.frame_rate {
1209 if is_video {
1210 os.time_base = rff_core::Rational::new(den as i32, num as i32);
1211 }
1212 }
1213 let target_sample_format = target.sample_format;
1219 os.sample_format = target_sample_format
1220 .or(stream.sample_format)
1221 .or(Some(SampleFormat::F32));
1222 let target_sample_format = target_sample_format.or(os.sample_format);
1227 if stream.media_type == MediaType::Audio && out_rate > 0 {
1230 os.time_base = rff_core::Rational::new(1, out_rate as i32);
1231 }
1232 Ok((
1233 StreamOp::Transcode(Box::new(TranscodeOp {
1234 decoder,
1235 encoder,
1236 filters,
1237 overlay: None,
1238 in_time_base: stream.time_base,
1239 is_video,
1240 trim,
1241 shift_ticks,
1242 audio_clock_samples: 0,
1243 fps,
1244 audio_chain,
1245 target_channels,
1246 target_rate,
1247 target_sample_format,
1248 resampler: None,
1249 accepted_formats,
1250 pixel_converter: None,
1251 source_range,
1252 out_index,
1253 max_video_frames: output.max_video_frames,
1254 video_frames_sent: 0,
1255 pending_pts: std::collections::VecDeque::new(),
1256 })),
1257 os,
1258 ))
1259 }
1260 None => {
1263 let mut os = stream.clone();
1264 os.index = out_index;
1265 if stream.media_type == MediaType::Subtitle {
1268 if let Some(target) = output.subtitle_codec {
1269 if target.media_type() != MediaType::Subtitle {
1270 return Err(Error::unsupported(format!(
1271 "-c:s: `{}` is not a subtitle codec",
1272 target.name()
1273 )));
1274 }
1275 os.codec_id = target;
1276 }
1277 }
1278 let trim = Trim::from_spec(output);
1279 let tb = stream.time_base;
1280 let to_ticks = |s: f64| (s * tb.den as f64 / tb.num.max(1) as f64).round() as i64;
1281 Ok((
1282 StreamOp::Copy(CopyOp {
1283 out_index,
1284 is_video: stream.media_type == MediaType::Video,
1285 start_ticks: trim.map(|t| to_ticks(t.start)).unwrap_or(0),
1286 end_ticks: trim.and_then(|t| t.end).map(to_ticks),
1287 trimming: trim.is_some(),
1288 started: false,
1289 }),
1290 os,
1291 ))
1292 }
1293 }
1294}
1295
1296fn decode_overlay_frame(
1299 engine: &Engine,
1300 demuxer: &mut dyn rff_format::Demuxer,
1301 streams: &[Stream],
1302) -> Result<VideoFrame> {
1303 let vidx = streams
1304 .iter()
1305 .position(|s| s.media_type == MediaType::Video)
1306 .ok_or_else(|| {
1307 Error::Option("filter_complex overlay: overlay input has no video".into())
1308 })?;
1309 let mut decoder = engine.codecs.find_decoder(streams[vidx].codec_id)?;
1310 decoder.configure(&codec_params(&streams[vidx]))?;
1311 let mut to_yuv = FilterChain::parse("format=yuv420p")?;
1312 let mut got_eof = false;
1313 loop {
1314 let frame = match demuxer.read_packet() {
1315 Ok(pkt) if pkt.stream_index as usize != vidx => continue,
1316 Ok(pkt) => {
1317 decoder.send_packet(&pkt)?;
1318 decoder.receive_frame()
1319 }
1320 Err(Error::Eof) if !got_eof => {
1321 got_eof = true;
1322 decoder.flush();
1323 decoder.receive_frame()
1324 }
1325 Err(e) => return Err(e),
1326 };
1327 match frame {
1328 Ok(Frame::Video(v)) => return to_yuv.apply(v),
1329 Ok(_) => continue,
1330 Err(Error::Again) if !got_eof => continue,
1331 Err(Error::Again) | Err(Error::Eof) => {
1332 return Err(Error::Option(
1333 "filter_complex overlay: no decodable frame in the overlay input".into(),
1334 ))
1335 }
1336 Err(e) => return Err(e),
1337 }
1338 }
1339}
1340
1341fn codec_params(s: &Stream) -> CodecParams {
1343 CodecParams {
1344 codec_id: s.codec_id,
1345 width: s.width,
1346 height: s.height,
1347 pixel_format: s.pixel_format,
1348 sample_rate: s.sample_rate,
1349 channels: s.channels,
1350 sample_format: s.sample_format,
1351 extradata: s.extradata.clone(),
1352 }
1353}
1354
1355fn process_packet(
1357 packet: Packet,
1358 ops: &mut [StreamOp],
1359 muxer: &mut dyn Muxer,
1360 report: &mut TranscodeReport,
1361) -> Result<()> {
1362 let Some(op) = ops.get_mut(packet.stream_index) else {
1363 return Ok(()); };
1365 match op {
1366 StreamOp::Skip => Ok(()),
1367 StreamOp::Copy(copy) => {
1368 if let Some(packet) = copy.process(packet) {
1369 muxer.write_packet(&packet)?;
1370 report.packets_written += 1;
1371 }
1372 Ok(())
1373 }
1374 StreamOp::Transcode(op) => {
1375 phases::time(&phases::DECODE, || op.decoder.send_packet(&packet))?;
1376 loop {
1377 match phases::time(&phases::DECODE, || op.decoder.receive_frame()) {
1378 Ok(frame) => op.handle_frame(frame, muxer, report)?,
1379 Err(Error::Again) | Err(Error::Eof) => break,
1380 Err(e) => return Err(e),
1381 }
1382 }
1383 Ok(())
1384 }
1385 }
1386}
1387
1388fn flush_streams(
1391 ops: &mut [StreamOp],
1392 muxer: &mut dyn Muxer,
1393 report: &mut TranscodeReport,
1394) -> Result<()> {
1395 for op in ops.iter_mut() {
1396 if let StreamOp::Transcode(op) = op {
1397 op.finish(muxer, report)?;
1398 }
1399 }
1400 Ok(())
1401}
1402
1403fn open_input(engine: &Engine, input: &InputSpec) -> Result<(String, crate::probe::Source)> {
1407 let path = input
1408 .path
1409 .to_str()
1410 .ok_or_else(|| Error::Option("input path is not valid UTF-8".into()))?;
1411 crate::probe::open_source(engine, path, input.format.as_deref())
1412}
1413
1414fn resolve_output_format(engine: &Engine, output: &OutputSpec) -> Result<String> {
1418 if let Some(forced) = &output.format {
1419 return Ok(forced.clone());
1420 }
1421 if output.path.to_str().is_some_and(rff_io::is_rtmp) {
1422 return Ok("flv".to_string());
1423 }
1424 let ext = output
1425 .path
1426 .extension()
1427 .and_then(|e| e.to_str())
1428 .unwrap_or_default();
1429 engine
1430 .formats
1431 .by_extension(ext)
1432 .map(|f| f.name.to_string())
1433 .ok_or_else(|| {
1434 Error::MuxerNotFound(format!(
1435 "{} (no format for this extension — pass -f)",
1436 output.path.display()
1437 ))
1438 })
1439}
1440
1441#[cfg(test)]
1442mod sample_format_tests {
1443 use super::*;
1444
1445 fn f32_af(samples: &[f32]) -> Frame {
1446 f32_frame(samples.to_vec(), 44_100, 1, Some(0))
1447 }
1448
1449 #[test]
1453 fn pinned_format_converts_f32_to_s16() {
1454 let src = [0.0f32, 0.5, -0.5, 1.0, -1.0];
1455 let out = conform_sample_format(Some(SampleFormat::S16), f32_af(&src)).unwrap();
1456 let Frame::Audio(af) = out else {
1457 unreachable!()
1458 };
1459 assert_eq!(af.format, SampleFormat::S16);
1460 assert_eq!(af.planes[0].len(), src.len() * 2, "s16 is 2 bytes/sample");
1461 let got: Vec<i16> = af.planes[0]
1462 .chunks_exact(2)
1463 .map(|b| i16::from_le_bytes([b[0], b[1]]))
1464 .collect();
1465 assert_eq!(got, vec![0, 16384, -16384, 32767, -32768]);
1468 }
1469
1470 #[test]
1473 fn unpinned_or_matching_format_is_a_passthrough() {
1474 let src = [0.25f32, -0.25];
1475 let Frame::Audio(a) = conform_sample_format(None, f32_af(&src)).unwrap() else {
1476 unreachable!()
1477 };
1478 assert_eq!(a.format, SampleFormat::F32);
1479 let Frame::Audio(b) = conform_sample_format(Some(SampleFormat::F32), f32_af(&src)).unwrap()
1480 else {
1481 unreachable!()
1482 };
1483 assert_eq!(b.format, SampleFormat::F32);
1484 assert_eq!(b.planes[0], a.planes[0]);
1485 }
1486
1487 #[test]
1489 fn codec_name_pins_the_sample_format() {
1490 assert_eq!(
1491 CodecId::sample_format_from_name("pcm_s16le"),
1492 Some(SampleFormat::S16)
1493 );
1494 assert_eq!(
1495 CodecId::sample_format_from_name("pcm_f32le"),
1496 Some(SampleFormat::F32)
1497 );
1498 assert_eq!(CodecId::sample_format_from_name("pcm"), None);
1500 assert_eq!(CodecId::sample_format_from_name("mp3"), None);
1501 }
1502}