1use std::collections::HashMap;
2use std::io::Write;
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5use std::sync::atomic::AtomicBool;
6
7use super::ffmpeg::{Child, Ffmpeg, pix_fmt};
8use super::mux::{Planner, Premix, Sound, gop_mp4, premixed_m4a};
9use super::picture::Blender;
10use super::probe::keyframes;
11use super::route::{Caps, Picked, Prefs, pick_encode};
12use super::video::{Source, frame_at, open, source};
13use crate::media::{AudioBuffer, Codec, Error, Frame, PixelFormat, Result, Rgba, Route};
14
15#[derive(Debug, Clone, PartialEq)]
16pub struct Piece {
17 pub path: PathBuf,
18 pub from: f64,
19 pub to: f64,
20}
21
22const MAX_SECONDS: f64 = 1e7;
23pub(super) const MAX_FPS: u32 = 1000;
24pub(super) const WRITTEN: f32 = 0.95;
25
26pub type Overlay = Box<dyn FnMut(f64, u32, u32) -> Option<Arc<Rgba>> + Send>;
27
28pub struct ExportOptions {
29 pub video: Codec,
30 pub crf: u8,
31 pub audio_kbps: u32,
32 pub audio_gains: Vec<f32>,
33 pub overlay: Option<Overlay>,
34 pub prefs: Prefs,
35}
36
37impl Default for ExportOptions {
38 fn default() -> Self {
39 Self {
40 video: Codec::H264,
41 crf: 18,
42 audio_kbps: 192,
43 audio_gains: Vec::new(),
44 overlay: None,
45 prefs: Prefs::default(),
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq)]
51pub enum Stage {
52 Cutting { piece: usize, pieces: usize },
53 Encoding,
54 Mixing,
55 Joining,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq)]
59pub struct Progress {
60 pub stage: Stage,
61 pub done: f32,
62 pub encoder: Route,
63}
64
65#[derive(Debug, Clone, PartialEq)]
66pub struct Exported {
67 pub path: PathBuf,
68 pub frames: u64,
69 pub copied: u64,
70 pub encoded: u64,
71 pub smart: bool,
72 pub video: Picked,
73 pub reasons: Vec<String>,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum Part {
78 Copy { from: i64, frames: i64 },
79 Encode { from: i64, frames: i64 },
80}
81
82impl Part {
83 pub(super) fn frames(self) -> i64 {
84 match self {
85 Part::Copy { frames, .. } | Part::Encode { frames, .. } => frames,
86 }
87 }
88}
89
90pub fn parts(in_f: i64, out_f: i64, keys: &[i64]) -> Vec<Part> {
91 if out_f <= in_f {
92 return Vec::new();
93 }
94 let Some(a) = keys.iter().copied().find(|k| *k >= in_f && *k < out_f) else {
95 return vec![Part::Encode {
96 from: in_f,
97 frames: out_f - in_f,
98 }];
99 };
100 let b = keys
101 .iter()
102 .copied()
103 .rev()
104 .find(|k| *k <= out_f)
105 .unwrap_or(a)
106 .max(a);
107 let mut out = Vec::new();
108 if a > in_f {
109 out.push(Part::Encode {
110 from: in_f,
111 frames: a - in_f,
112 });
113 }
114 if b > a {
115 out.push(Part::Copy {
116 from: a,
117 frames: b - a,
118 });
119 }
120 if out_f > b {
121 out.push(Part::Encode {
122 from: b,
123 frames: out_f - b,
124 });
125 }
126 out
127}
128
129#[derive(Clone)]
130pub(super) struct Job<'a> {
131 pub(super) pieces: &'a [Piece],
132 pub(super) sources: Vec<Source>,
133 pub(super) index: Vec<usize>,
134 pub(super) fps: (u32, u32),
135 pub(super) size: (u32, u32),
136 pub(super) format: PixelFormat,
137 pub(super) total: u64,
138 pub(super) done: u64,
139 pub(super) work: PathBuf,
140 pub(super) route: Route,
141}
142
143pub(super) type Tell<'a> = &'a mut dyn FnMut(&Progress) -> bool;
144
145pub(super) type Plan = (Vec<(usize, Part)>, Vec<usize>);
146
147impl Job<'_> {
148 pub(super) fn frame_of(&self, seconds: f64) -> i64 {
149 frame_at(seconds, self.fps)
150 }
151
152 pub(super) fn span(&self, piece: &Piece) -> (i64, i64) {
153 (self.frame_of(piece.from), self.frame_of(piece.to))
154 }
155
156 pub(super) fn quiet(&self) -> Self {
157 Job {
158 total: 0,
159 done: 0,
160 ..self.clone()
161 }
162 }
163
164 pub(super) fn plan(
165 &self,
166 prefs: &Prefs,
167 mut split: impl FnMut(usize, Part, &[i64]) -> Result<Vec<Part>>,
168 ) -> Result<Plan> {
169 let mut keys: HashMap<usize, Vec<i64>> = HashMap::new();
170 let mut plan: Vec<(usize, Part)> = Vec::new();
171 let mut order = Vec::new();
172 for (n, piece) in self.pieces.iter().enumerate() {
173 let si = self.index[n];
174 if let std::collections::hash_map::Entry::Vacant(slot) = keys.entry(si) {
175 slot.insert(
176 keyframes(&self.sources[si].path, prefs)?
177 .into_iter()
178 .map(|t| self.frame_of(t))
179 .collect(),
180 );
181 }
182 let (a, b) = self.span(piece);
183 for part in parts(a, b, &keys[&si]) {
184 for p in split(si, part, &keys[&si])? {
185 let i = match plan.iter().position(|q| *q == (si, p)) {
186 Some(i) => i,
187 None => {
188 plan.push((si, p));
189 plan.len() - 1
190 }
191 };
192 order.push(i);
193 }
194 }
195 }
196 Ok((plan, order))
197 }
198
199 pub(super) fn finish(&self, progress: Tell) -> Result<()> {
200 if progress(&Progress {
201 stage: Stage::Joining,
202 done: 1.0,
203 encoder: self.route,
204 }) {
205 Ok(())
206 } else {
207 Err(Error::Cancelled)
208 }
209 }
210
211 pub(super) fn seconds(&self, frames: i64) -> f64 {
212 frames as f64 * self.fps.1 as f64 / self.fps.0 as f64
213 }
214
215 pub(super) fn tell(&self, progress: Tell, stage: Stage) -> Result<()> {
216 let done = if self.total == 0 {
217 0.0
218 } else {
219 (self.done as f32 / self.total as f32).min(1.0) * WRITTEN
220 };
221 let done = match stage {
222 Stage::Mixing | Stage::Joining => WRITTEN,
223 _ => done,
224 };
225 if progress(&Progress {
226 stage,
227 done,
228 encoder: self.route,
229 }) {
230 Ok(())
231 } else {
232 Err(Error::Cancelled)
233 }
234 }
235}
236
237pub fn export(
238 out: &Path,
239 pieces: &[Piece],
240 opts: ExportOptions,
241 progress: impl FnMut(&Progress) -> bool,
242) -> Result<Exported> {
243 export_to(out, pieces, opts, None, progress)
244}
245
246pub fn export_with_audio(
247 out: &Path,
248 pieces: &[Piece],
249 opts: ExportOptions,
250 mut audio: impl FnMut(usize, usize) -> Result<AudioBuffer> + Send,
251 progress: impl FnMut(&Progress) -> bool,
252) -> Result<Exported> {
253 export_to(out, pieces, opts, Some(&mut audio), progress)
254}
255
256fn export_to(
257 out: &Path,
258 pieces: &[Piece],
259 mut opts: ExportOptions,
260 premix: Option<Premix>,
261 mut progress: impl FnMut(&Progress) -> bool,
262) -> Result<Exported> {
263 if pieces.is_empty() {
264 return Err(Error::Invalid("nothing to export".to_string()));
265 }
266 let mut sources: Vec<Source> = Vec::new();
267 let mut by_path: HashMap<PathBuf, usize> = HashMap::new();
268 let mut index = Vec::new();
269 for p in pieces {
270 if p.to.partial_cmp(&p.from) != Some(std::cmp::Ordering::Greater)
271 || !(-MAX_SECONDS..=MAX_SECONDS).contains(&p.from)
272 || !(-MAX_SECONDS..=MAX_SECONDS).contains(&p.to)
273 {
274 return Err(Error::Invalid(format!(
275 "a piece from {} to {}",
276 p.from, p.to
277 )));
278 }
279 let i = match by_path.get(&p.path) {
280 Some(&i) => i,
281 None => {
282 sources.push(source(&p.path, &opts.prefs)?);
283 by_path.insert(p.path.clone(), sources.len() - 1);
284 sources.len() - 1
285 }
286 };
287 index.push(i);
288 }
289 let caps = Caps::detect(&opts.prefs);
290 let mut video = pick_encode(opts.video, &opts.prefs, &caps)?;
291 let first = sources[index[0]].probe.clone();
292 let fps = first.fps;
293 if fps.0 == 0 || fps.1 == 0 || fps.0 / fps.1 > MAX_FPS {
294 return Err(Error::Invalid(format!(
295 "a frame rate of {}/{}",
296 fps.0, fps.1
297 )));
298 }
299 let size = (first.width & !1, first.height & !1);
300 if size.0 == 0 || size.1 == 0 {
301 return Err(Error::Invalid(format!(
302 "a picture of {}x{}",
303 first.width, first.height
304 )));
305 }
306 let format = if first.bit_depth > 8 && video.codec == Codec::Hevc {
307 PixelFormat::Yuv420p10
308 } else {
309 PixelFormat::Yuv420p8
310 };
311 let format = match video.route {
312 Route::Gpu => match super::mux::gpu_check(video.codec, size, fps, format) {
313 Ok(()) => format,
314 Err(e) => {
315 video.reasons.push(format!(
316 "the GPU encoder failed ({e}); encoding H.264 on the CPU instead"
317 ));
318 video.route = Route::Cpu;
319 video.codec = Codec::H264;
320 PixelFormat::Yuv420p8
321 }
322 },
323 Route::Cpu => PixelFormat::Yuv420p8,
324 Route::Ffmpeg => format,
325 };
326 let same = sources.iter().all(|s| {
327 s.probe.video_codec == Some(video.codec)
328 && s.probe.fps == fps
329 && (s.probe.width, s.probe.height) == (first.width, first.height)
330 && s.probe.bit_depth == first.bit_depth
331 && s.probe.rotation == first.rotation
332 });
333 let readable = sources.iter().all(|s| s.demux.is_some());
334 let ffmpeg = match (&caps.ffmpeg, video.route) {
335 (Some(f), Route::Ffmpeg) => Some(f.clone()),
336 (_, Route::Cpu | Route::Gpu) => None,
337 (None, Route::Ffmpeg) => {
338 return Err(Error::Unsupported(
339 "ffmpeg was picked but not found".to_string(),
340 ));
341 }
342 };
343 let why = if opts.overlay.is_some() {
344 Some("an overlay is drawn on every frame, so the whole export is re-encoded".to_string())
345 } else if !same {
346 Some(match sources.iter().find(|s| s.probe.video_codec != Some(video.codec)) {
347 Some(s) => format!(
348 "the source video is {}, the export {}, so the whole export is re-encoded",
349 s.probe.video_codec_name, video.codec
350 ),
351 None => "the sources differ in frame rate, size, depth or rotation, so the whole export is re-encoded".to_string(),
352 })
353 } else if video.codec == Codec::Vp9 && (first.profile != "Profile 0" || first.bit_depth > 8) {
354 Some(format!(
355 "the source VP9 is {} at {} bits, our VP9 encoder writes profile 0 at 8 bits, so the whole export is re-encoded",
356 first.profile, first.bit_depth
357 ))
358 } else if size != (first.width, first.height) {
359 Some(format!(
360 "the picture's odd size {}x{} is evened, so the whole export is re-encoded",
361 first.width, first.height
362 ))
363 } else if ffmpeg.is_none() && !readable {
364 Some("our MP4 and MKV readers can't index a source to copy it, so the whole export is re-encoded".to_string())
365 } else {
366 None
367 };
368 let smart = why.is_none();
369 let mut reasons: Vec<String> = video.reasons.clone();
370 for s in &sources {
371 for r in &s.reasons {
372 if !reasons.contains(r) {
373 reasons.push(r.clone());
374 }
375 }
376 }
377 reasons.extend(why);
378 let work = std::path::absolute(out.with_extension("parts"))?;
379 if work.exists() {
380 std::fs::remove_dir_all(&work).map_err(|e| Error::at(&work, e))?;
381 }
382 std::fs::create_dir_all(&work).map_err(|e| Error::at(&work, e))?;
383 let mut job = Job {
384 pieces,
385 sources,
386 index,
387 fps,
388 size,
389 format,
390 total: 0,
391 done: 0,
392 work: work.clone(),
393 route: video.route,
394 };
395 job.total = pieces
396 .iter()
397 .map(|p| (job.frame_of(p.to) - job.frame_of(p.from)).max(0) as u64)
398 .sum();
399 let result = match &ffmpeg {
400 Some(f) => run(
401 &mut job,
402 f,
403 &video,
404 &mut opts,
405 smart,
406 out,
407 &mut progress,
408 premix,
409 ),
410 None => super::mux::run(
411 &mut job,
412 &video,
413 &mut opts,
414 smart,
415 out,
416 &mut progress,
417 match premix {
418 Some(p) => Sound::Premixed(p),
419 None => Sound::Tracks(caps.ffmpeg.as_ref()),
420 },
421 ),
422 };
423 let _ = std::fs::remove_dir_all(&work);
424 let (copied, encoded, planned) = result?;
425 for r in planned {
426 if !reasons.contains(&r) {
427 reasons.push(r);
428 }
429 }
430 Ok(Exported {
431 path: out.to_path_buf(),
432 frames: copied + encoded,
433 copied,
434 encoded,
435 smart,
436 video,
437 reasons,
438 })
439}
440
441#[allow(clippy::too_many_arguments)]
442fn run(
443 job: &mut Job,
444 f: &Ffmpeg,
445 video: &Picked,
446 opts: &mut ExportOptions,
447 smart: bool,
448 out: &Path,
449 progress: Tell,
450 mut premix: Option<Premix>,
451) -> Result<(u64, u64, Vec<String>)> {
452 let (mut copied, mut encoded) = (0u64, 0u64);
453 let mut list = String::new();
454 let exact = premix.is_some();
455 let kbps = opts.audio_kbps;
456 let mut planner = Planner::default();
457 if smart {
458 let jobref = &*job;
459 let (plan, order) = jobref.plan(&opts.prefs, |si, p, keys| {
460 planner.split(jobref, (si, p), keys, false)
461 })?;
462 let mut at = 0i64;
463 for &i in &order {
464 let file = job.work.join(format!("part-{i:05}.ts"));
465 let frames = plan[i].1.frames();
466 list.push_str(&listed(job, &file, at, frames));
467 at += frames;
468 match plan[i].1 {
469 Part::Copy { frames, .. } => copied += frames as u64,
470 Part::Encode { frames, .. } => encoded += frames as u64,
471 }
472 }
473 let tail = tail(f, job, video, opts.crf, true)?;
474 let mix = job.work.join("mix.m4a");
475 let mix_args = if exact {
476 Vec::new()
477 } else {
478 mix_args(job, opts, &mix)
479 };
480 let total: i64 = plan.iter().map(|(_, p)| p.frames()).sum::<i64>().max(1);
481 let next = std::sync::atomic::AtomicUsize::new(0);
482 let stop = std::sync::atomic::AtomicBool::new(false);
483 let (tx, rx) = std::sync::mpsc::channel::<Result<i64>>();
484 let workers = plan.len().clamp(1, 6);
485 let jobref = &*job;
486 let outcome = std::thread::scope(|s| {
487 {
488 let tx = tx.clone();
489 let (mix_args, mix, stop) = (&mix_args, &mix, &stop);
490 s.spawn(move || {
491 let r = match premix.take() {
492 Some(p) => premixed_m4a(jobref, p, kbps, stop, mix),
493 None => f.run_args(mix_args).map(|_| ()),
494 };
495 let _ = tx.send(r.map(|_| 0));
496 });
497 }
498 for _ in 0..workers {
499 let tx = tx.clone();
500 let (plan, next, stop, tail) = (&plan, &next, &stop, &tail);
501 s.spawn(move || {
502 loop {
503 if stop.load(std::sync::atomic::Ordering::Relaxed) {
504 break;
505 }
506 let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
507 let Some(&(si, part)) = plan.get(i) else {
508 break;
509 };
510 let file = jobref.work.join(format!("part-{i:05}.ts"));
511 let r = match part {
512 Part::Copy { from, frames } => {
513 copy(f, jobref, si, from, frames, &file).map(|_| frames)
514 }
515 Part::Encode { from, frames } => {
516 edge(f, jobref, si, from, frames, tail, &file).map(|_| frames)
517 }
518 };
519 let _ = tx.send(r);
520 }
521 });
522 }
523 drop(tx);
524 let mut done = 0i64;
525 let mut first_err = None;
526 for (k, r) in rx.iter().enumerate() {
527 match r {
528 Ok(frames) => done += frames,
529 Err(e) => {
530 stop.store(true, std::sync::atomic::Ordering::Relaxed);
531 first_err.get_or_insert(e);
532 continue;
533 }
534 }
535 let keep = progress(&Progress {
536 stage: Stage::Cutting {
537 piece: k + 1,
538 pieces: plan.len() + 1,
539 },
540 done: done as f32 / total as f32 * WRITTEN,
541 encoder: Route::Ffmpeg,
542 });
543 if !keep {
544 stop.store(true, std::sync::atomic::Ordering::Relaxed);
545 first_err.get_or_insert(Error::Cancelled);
546 }
547 }
548 match first_err {
549 Some(e) => Err(e),
550 None => Ok(()),
551 }
552 });
553 outcome?;
554 return join(job, f, video, &list, (&mix, exact), out, progress)
555 .map(|_| (copied, encoded, planner.reasons));
556 } else {
557 job.tell(progress, Stage::Encoding)?;
558 let file = job.work.join("whole.ts");
559 let mut enc = Encoder::open(f, job, video, opts.crf, &file)?;
560 let mut at = 0i64;
561 for (n, piece) in job.pieces.iter().enumerate() {
562 let (a, b) = job.span(piece);
563 reencode(
564 job,
565 job.index[n],
566 (a, b - a),
567 at,
568 &mut |f| enc.push(f),
569 &mut opts.overlay,
570 progress,
571 )?;
572 at += b - a;
573 }
574 enc.finish()?;
575 encoded = at as u64;
576 list.push_str(&listed(job, &file, 0, at));
577 }
578 job.tell(progress, Stage::Mixing)?;
579 let mix = job.work.join("mix.m4a");
580 match premix {
581 Some(p) => premixed_m4a(job, p, kbps, &AtomicBool::new(false), &mix)?,
582 None => {
583 f.run_args(&mix_args(job, opts, &mix))?;
584 }
585 }
586 join(job, f, video, &list, (&mix, exact), out, progress)?;
587 Ok((copied, encoded, planner.reasons))
588}
589
590fn micros(job: &Job, frames: i64) -> i128 {
591 let num = frames as i128 * job.fps.1 as i128 * 1_000_000;
592 let den = (job.fps.0 as i128).max(1);
593 (2 * num + den).div_euclid(2 * den)
594}
595
596fn listed(job: &Job, file: &Path, at: i64, frames: i64) -> String {
597 let us = micros(job, at + frames) - micros(job, at);
598 format!(
599 "file '{}'\nduration {}.{:06}\n",
600 file.display(),
601 us.div_euclid(1_000_000),
602 us.rem_euclid(1_000_000)
603 )
604}
605
606fn join(
607 job: &Job,
608 f: &Ffmpeg,
609 video: &Picked,
610 list: &str,
611 (mix, exact): (&Path, bool),
612 out: &Path,
613 progress: Tell,
614) -> Result<()> {
615 job.tell(progress, Stage::Joining)?;
616 let list_path = job.work.join("pieces.txt");
617 std::fs::write(&list_path, list).map_err(|e| Error::at(&list_path, e))?;
618 let part = job.work.join("joined.mp4");
619 let mut join: Vec<String> = vec![
620 "-f".into(),
621 "concat".into(),
622 "-safe".into(),
623 "0".into(),
624 "-i".into(),
625 list_path.display().to_string(),
626 "-i".into(),
627 mix.display().to_string(),
628 "-map".into(),
629 "0:v:0".into(),
630 "-map".into(),
631 "1:a:0".into(),
632 "-c".into(),
633 "copy".into(),
634 ];
635 if video.codec == Codec::Hevc {
636 join.extend(["-tag:v".into(), "hvc1".into()]);
637 }
638 let rotation = job.sources[job.index[0]].probe.rotation;
639 if rotation != 0 {
640 join.extend([
641 "-display_rotation:v:0".into(),
642 format!("{}", -(rotation as i32)),
643 ]);
644 }
645 if exact {
646 join.extend(["-movie_timescale".into(), "48000".into()]);
647 }
648 join.extend([
649 "-movflags".into(),
650 "+faststart".into(),
651 "-f".into(),
652 "mp4".into(),
653 part.display().to_string(),
654 ]);
655 f.run_args(&join)?;
656 std::fs::rename(&part, out).map_err(|e| Error::at(out, e))?;
657 job.finish(progress)
658}
659
660fn copy(f: &Ffmpeg, job: &Job, si: usize, from: i64, frames: i64, file: &Path) -> Result<()> {
661 if job.sources[si].demux.is_some() {
662 let gop = file.with_extension("mp4");
663 gop_mp4(job, si, from, frames, &gop)?;
664 let args: Vec<String> = vec![
665 "-i".into(),
666 gop.display().to_string(),
667 "-map".into(),
668 "0:v:0".into(),
669 "-c:v".into(),
670 "copy".into(),
671 "-f".into(),
672 "mpegts".into(),
673 file.display().to_string(),
674 ];
675 let done = f.run_args(&args);
676 let _ = std::fs::remove_file(&gop);
677 done?;
678 return padded(file);
679 }
680 let seek = job.seconds(from) + job.seconds(1) / 2.0;
681 let args: Vec<String> = vec![
682 "-ss".into(),
683 format!("{seek:.6}"),
684 "-i".into(),
685 job.sources[si].path.display().to_string(),
686 "-map".into(),
687 "0:v:0".into(),
688 "-an".into(),
689 "-sn".into(),
690 "-dn".into(),
691 "-frames:v".into(),
692 frames.to_string(),
693 "-c:v".into(),
694 "copy".into(),
695 "-f".into(),
696 "mpegts".into(),
697 file.display().to_string(),
698 ];
699 f.run_args(&args)?;
700 padded(file)
701}
702
703const TS_PACKET: usize = 188;
704const TS_FLOOR: u64 = 64 * TS_PACKET as u64;
705
706fn padded(file: &Path) -> Result<()> {
707 let len = std::fs::metadata(file)
708 .map_err(|e| Error::at(file, e))?
709 .len();
710 if len >= TS_FLOOR {
711 return Ok(());
712 }
713 let mut null = [0xffu8; TS_PACKET];
714 null[..4].copy_from_slice(&[0x47, 0x1f, 0xff, 0x10]);
715 let count = (TS_FLOOR - len).div_ceil(TS_PACKET as u64) as usize;
716 let mut out = std::fs::OpenOptions::new()
717 .append(true)
718 .open(file)
719 .map_err(|e| Error::at(file, e))?;
720 out.write_all(&null.repeat(count))
721 .map_err(|e| Error::at(file, e))
722}
723
724pub(super) fn reencode(
725 job: &mut Job,
726 si: usize,
727 (from, frames): (i64, i64),
728 at: i64,
729 push: &mut dyn FnMut(&Frame) -> Result<()>,
730 overlay: &mut Option<Overlay>,
731 progress: Tell,
732) -> Result<()> {
733 if frames <= 0 {
734 return Ok(());
735 }
736 let src = &job.sources[si];
737 let size = (src.probe.width, src.probe.height) != job.size;
738 let mut frames_in = open(
739 src,
740 job.seconds(from),
741 Some(frames as u64),
742 size.then_some(job.size),
743 Some(job.fps),
744 )?;
745 let mut n = 0i64;
746 let mut last: Option<Frame> = None;
747 let mut blender = Blender::new();
748 while n < frames {
749 let mut frame = match frames_in.next() {
750 Some(f) => f?,
751 None => match last.clone() {
752 Some(f) => f,
753 None => return Err(Error::Invalid("a piece with no frames".to_string())),
754 },
755 };
756 if frame.format != job.format {
757 frame = convert_depth(&frame, job.format);
758 }
759 frame.pts = at + n;
760 let layer = overlay
761 .as_mut()
762 .and_then(|o| o(job.seconds(at + n), frame.width, frame.height));
763 let keep = match layer {
764 Some(layer) => {
765 let clean = frame.clone();
766 blender.draw(&mut frame, &layer);
767 push(&frame)?;
768 frames_in.recycle(frame);
769 clean
770 }
771 None => {
772 push(&frame)?;
773 frame
774 }
775 };
776 if let Some(old) = last.replace(keep) {
777 frames_in.recycle(old);
778 }
779 n += 1;
780 job.done += 1;
781 if n % 15 == 0 {
782 job.tell(progress, Stage::Encoding)?;
783 }
784 }
785 Ok(())
786}
787
788pub(super) fn convert_depth(frame: &Frame, format: PixelFormat) -> Frame {
789 let mut out = Frame::new(
790 format,
791 frame.width,
792 frame.height,
793 frame.pts,
794 frame.time_base,
795 );
796 for (o, i) in out.planes.iter_mut().zip(&frame.planes) {
797 let rows = o
798 .data
799 .chunks_mut(o.stride.max(1))
800 .zip(i.data.chunks(i.stride.max(1)));
801 for (dst, src) in rows.take(i.height) {
802 match format {
803 PixelFormat::Yuv420p8 => {
804 for (d, s) in dst.iter_mut().zip(src.chunks_exact(2)).take(i.width) {
805 *d = ((u16::from_le_bytes([s[0], s[1]]) + 2) / 4).min(255) as u8;
806 }
807 }
808 PixelFormat::Yuv420p10 => {
809 for (d, &s) in dst.chunks_exact_mut(2).zip(src).take(i.width) {
810 d.copy_from_slice(&(s as u16 * 4).to_le_bytes());
811 }
812 }
813 }
814 }
815 }
816 out
817}
818
819struct Encoder {
820 child: Child,
821}
822
823struct Tail {
824 pre: Vec<String>,
825 post: Vec<String>,
826}
827
828fn tail(f: &Ffmpeg, job: &Job, video: &Picked, crf: u8, edge: bool) -> Result<Tail> {
829 let name = f
830 .encoder_for(video.codec, video.ffmpeg_codec.as_deref())
831 .ok_or_else(|| {
832 Error::Unsupported(format!(
833 "{} has no {} encoder",
834 f.path.display(),
835 video.codec
836 ))
837 })?;
838 let mut pre: Vec<String> = Vec::new();
839 let mut post = encoder_args(&name, video.codec, crf, job, edge);
840 if name.ends_with("_vaapi") {
841 pre.extend([
842 "-init_hw_device".into(),
843 "vaapi=va:/dev/dri/renderD128".into(),
844 "-filter_hw_device".into(),
845 "va".into(),
846 ]);
847 let up = if job.format == PixelFormat::Yuv420p10 {
848 "format=p010,hwupload"
849 } else {
850 "format=nv12,hwupload"
851 };
852 post.extend(["-vf".into(), up.into()]);
853 }
854 if edge {
855 post.extend(["-bf".into(), "0".into(), "-g".into(), "100000".into()]);
856 }
857 Ok(Tail { pre, post })
858}
859
860fn edge(
861 f: &Ffmpeg,
862 job: &Job,
863 si: usize,
864 from: i64,
865 frames: i64,
866 tail: &Tail,
867 file: &Path,
868) -> Result<()> {
869 let src = &job.sources[si];
870 let mut args = tail.pre.clone();
871 args.extend(f.decoder_args(src.ffmpeg_codec.as_deref()));
872 args.extend([
873 "-ss".into(),
874 format!("{:.6}", (job.seconds(from) - job.seconds(1) / 2.0).max(0.0)),
875 "-i".into(),
876 src.path.display().to_string(),
877 "-map".into(),
878 "0:v:0".into(),
879 "-an".into(),
880 "-sn".into(),
881 "-dn".into(),
882 "-frames:v".into(),
883 frames.to_string(),
884 ]);
885 args.extend(tail.post.iter().cloned());
886 args.extend(["-f".into(), "mpegts".into(), file.display().to_string()]);
887 f.run_args(&args)?;
888 padded(file)
889}
890
891impl Encoder {
892 fn open(f: &Ffmpeg, job: &Job, video: &Picked, crf: u8, file: &Path) -> Result<Encoder> {
893 let t = tail(f, job, video, crf, false)?;
894 let pix = pix_fmt(job.format);
895 let mut args = t.pre;
896 args.extend([
897 "-f".into(),
898 "rawvideo".into(),
899 "-pix_fmt".into(),
900 pix.into(),
901 "-s".into(),
902 format!("{}x{}", job.size.0, job.size.1),
903 "-r".into(),
904 format!("{}/{}", job.fps.0, job.fps.1),
905 "-i".into(),
906 "-".into(),
907 ]);
908 args.extend(t.post);
909 args.extend(["-f".into(), "mpegts".into(), file.display().to_string()]);
910 Ok(Encoder {
911 child: f.spawn(&args, true, false)?,
912 })
913 }
914
915 fn push(&mut self, frame: &Frame) -> Result<()> {
916 let Some(input) = self.child.stdin.as_mut() else {
917 return Err(Error::Backend("ffmpeg closed its input".to_string()));
918 };
919 for p in &frame.planes {
920 let row = p.width * frame.format.bytes_per_sample();
921 let ok = if p.stride == row {
922 input.write_all(&p.data)
923 } else {
924 p.data
925 .chunks(p.stride)
926 .take(p.height)
927 .try_for_each(|r| input.write_all(&r[..row.min(r.len())]))
928 };
929 if ok.is_err() {
930 let err = self.child.stderr_now();
931 return Err(Error::Backend(format!("ffmpeg: {err}")));
932 }
933 }
934 Ok(())
935 }
936
937 fn finish(self) -> Result<()> {
938 self.child.finish()
939 }
940}
941
942fn encoder_args(name: &str, codec: Codec, crf: u8, job: &Job, edge: bool) -> Vec<String> {
943 let s = |v: &[&str]| v.iter().map(|x| x.to_string()).collect::<Vec<_>>();
944 let crf_edge = if edge { crf.saturating_sub(2) } else { crf };
945 let q = crf_edge.to_string();
946 let mut a = s(&["-c:v", name]);
947 match name {
948 "libx264" => a.extend(s(&[
949 "-preset",
950 if edge { "veryfast" } else { "medium" },
951 "-crf",
952 &q,
953 "-pix_fmt",
954 "yuv420p",
955 ])),
956 "libx265" => a.extend(s(&[
957 "-preset",
958 if edge { "veryfast" } else { "medium" },
959 "-crf",
960 &q,
961 "-pix_fmt",
962 pix_fmt(job.format),
963 "-x265-params",
964 "log-level=error",
965 ])),
966 n if n.ends_with("_vaapi") => a.extend(s(&[
967 "-rc_mode",
968 "CQP",
969 "-qp",
970 &(crf_edge as u32 + 1).to_string(),
971 ])),
972 n if n.ends_with("_nvenc") => {
973 a.extend(s(&["-preset", "p5", "-rc", "vbr", "-cq", &q, "-b:v", "0"]))
974 }
975 n if n.ends_with("_qsv") => a.extend(s(&["-global_quality", &q])),
976 n if n.ends_with("_videotoolbox") => a.extend(s(&[
977 "-q:v",
978 &(100u32.saturating_sub(crf_edge as u32 * 2)).to_string(),
979 ])),
980 _ => {
981 let pixels = job.size.0 as f64 * job.size.1 as f64;
982 let rate = pixels * job.fps.0 as f64 / job.fps.1 as f64;
983 let bits = rate * 0.15 * 2f64.powf((18.0 - crf_edge as f64) / 6.0);
984 a.extend(s(&["-b:v", &format!("{}", bits.round() as u64)]));
985 }
986 }
987 if codec == Codec::Hevc && !name.ends_with("_vaapi") && !name.starts_with("lib") {
988 a.extend(s(&["-tag:v", "hvc1"]));
989 }
990 a
991}
992
993fn mix_args(job: &Job, opts: &ExportOptions, out: &Path) -> Vec<String> {
994 let mut args: Vec<String> = Vec::new();
995 let mut graph = String::new();
996 let mut joined = String::new();
997 let (mut at, mut first) = (0i64, 0i64);
998 for (i, piece) in job.pieces.iter().enumerate() {
999 let src = &job.sources[job.index[i]];
1000 let (a, b) = job.span(piece);
1001 let start = job.seconds(a);
1002 let dur = job.seconds(b - a);
1003 at += b - a;
1004 let end = (job.seconds(at) * 48_000.0).round() as i64;
1005 let n = (end - first).max(0);
1006 first = end;
1007 args.extend([
1008 "-ss".into(),
1009 format!("{start:.6}"),
1010 "-t".into(),
1011 format!("{:.6}", dur + 0.5),
1012 "-i".into(),
1013 src.path.display().to_string(),
1014 ]);
1015 let tracks: Vec<(usize, f32)> = src
1016 .probe
1017 .audio
1018 .iter()
1019 .map(|t| {
1020 (
1021 t.index,
1022 opts.audio_gains.get(t.index).copied().unwrap_or(1.0),
1023 )
1024 })
1025 .filter(|(_, g)| *g > 0.0)
1026 .collect();
1027 if tracks.is_empty() {
1028 graph.push_str(&format!(
1029 "anullsrc=r=48000:cl=stereo,atrim=end_sample={n}[p{i}];"
1030 ));
1031 } else {
1032 let mut labels = String::new();
1033 for (t, g) in &tracks {
1034 graph.push_str(&format!(
1035 "[{i}:a:{t}]aresample=48000,aformat=sample_fmts=fltp:channel_layouts=stereo,volume={g:.4}[p{i}t{t}];"
1036 ));
1037 labels.push_str(&format!("[p{i}t{t}]"));
1038 }
1039 graph.push_str(&format!(
1040 "{labels}amix=inputs={}:normalize=0:duration=longest,apad=whole_len={n},atrim=end_sample={n},asetpts=N/SR/TB[p{i}];",
1041 tracks.len()
1042 ));
1043 }
1044 joined.push_str(&format!("[p{i}]"));
1045 }
1046 graph.push_str(&format!(
1047 "{joined}concat=n={}:v=0:a=1[mix]",
1048 job.pieces.len()
1049 ));
1050 let script = job.work.join("mix.graph");
1051 let _ = std::fs::write(&script, &graph);
1052 args.extend([
1053 "-/filter_complex".into(),
1054 script.display().to_string(),
1055 "-map".into(),
1056 "[mix]".into(),
1057 "-c:a".into(),
1058 "aac".into(),
1059 "-b:a".into(),
1060 format!("{}k", opts.audio_kbps.max(32)),
1061 "-ar".into(),
1062 "48000".into(),
1063 "-ac".into(),
1064 "2".into(),
1065 out.display().to_string(),
1066 ]);
1067 args
1068}
1069
1070#[cfg(test)]
1071mod tests {
1072 use super::*;
1073
1074 #[test]
1075 fn a_piece_between_keyframes_copies_its_middle_and_encodes_its_edges() {
1076 let keys = [0, 120, 240, 360];
1077 assert_eq!(
1078 parts(100, 300, &keys),
1079 [
1080 Part::Encode {
1081 from: 100,
1082 frames: 20
1083 },
1084 Part::Copy {
1085 from: 120,
1086 frames: 120
1087 },
1088 Part::Encode {
1089 from: 240,
1090 frames: 60
1091 },
1092 ]
1093 );
1094 assert_eq!(
1095 parts(130, 200, &keys),
1096 [Part::Encode {
1097 from: 130,
1098 frames: 70
1099 }]
1100 );
1101 assert_eq!(
1102 parts(100, 200, &keys),
1103 [
1104 Part::Encode {
1105 from: 100,
1106 frames: 20
1107 },
1108 Part::Encode {
1109 from: 120,
1110 frames: 80
1111 },
1112 ]
1113 );
1114 }
1115}