1use std::path::{Path, PathBuf};
16use std::sync::Arc;
17use std::time::{Duration, Instant};
18
19use anyhow::{Context, Result, bail};
20use bytes::Bytes;
21
22use codec::encode::EncoderConfig;
23use container::streaming::{self, DemuxHeader};
24
25use crate::decode_pump::{ClipSource, DecodePumpConfig};
26use crate::multigpu;
27use crate::progress::{JobEvent, ProgressSink, RungProgress, RungStatus};
28use crate::spec::{OutputMode, OutputSpec, Rung};
29use crate::validate::needs_chroma_downsample;
30
31mod audio;
32mod pump;
33mod run;
34mod splice;
35#[cfg(test)]
36mod tests;
37
38pub use splice::Clip;
39
40use self::audio::{PreparedAudio, prepare_audio};
41use self::pump::run_hls;
42use self::run::{run_serial_single_file, run_single_file};
43use self::splice::{trim_audio, trim_frame};
44
45pub(super) const FRAME_CHANNEL_CAPACITY: usize = 8;
47
48#[derive(Debug)]
50pub enum RungArtifact {
51 File(Vec<u8>),
53 HlsRendition {
55 dir: PathBuf,
56 relative_dir: String,
57 },
58}
59
60#[derive(Debug)]
62pub struct RungOutput {
63 pub label: String,
64 pub width: u32,
65 pub height: u32,
66 pub frames: u64,
67 pub bytes: u64,
68 pub artifact: RungArtifact,
69}
70
71#[derive(Debug)]
73pub struct JobOutput {
74 pub rungs: Vec<RungOutput>,
77 pub hls_root: Option<PathBuf>,
79 pub master_playlist: Option<PathBuf>,
81 pub source_codec: String,
82 pub source_dims: (u32, u32),
83 pub source_frame_rate: f64,
84 pub audio_handling: String,
86 pub elapsed: Duration,
87}
88
89pub async fn run_job(
96 input: Bytes,
97 spec: &OutputSpec,
98 output_dir: Option<&Path>,
99 sink: Arc<dyn ProgressSink>,
100) -> Result<JobOutput> {
101 let started = Instant::now();
102 spec.validate().context("invalid OutputSpec")?;
103
104 let (header, audio_track) = {
105 let demuxer = streaming::demux_streaming(&input).context("demux")?;
106 (demuxer.header().clone(), demuxer.audio().cloned())
107 };
108 let source_codec = header.codec.to_ascii_lowercase();
109 let source_dims = (header.info.width, header.info.height);
110 let source_frame_rate = header.info.frame_rate;
111
112 let resolved_spec;
118 let spec = if spec.decode_policy.is_fastest() {
119 let candidates = codec::decode::decode_capable_gpu_indices(&source_codec);
120 if candidates.len() > 1 {
121 match crate::decode_pump::fastest_decode_gpu(
122 &source_codec,
123 &header.info,
124 &input,
125 &candidates,
126 crate::decode_pump::DECODE_BENCH_FRAMES,
127 ) {
128 Some(gpu) => {
129 let mut s = spec.clone();
130 s.decode_policy = crate::spec::DecodePolicy::SpecificGpu(gpu);
131 resolved_spec = s;
132 &resolved_spec
133 }
134 None => spec,
135 }
136 } else {
137 tracing::info!(
138 candidates = candidates.len(),
139 "decode-with-fastest: fewer than two decode-capable GPUs; nothing to benchmark"
140 );
141 spec
142 }
143 } else {
144 spec
145 };
146
147 sink.on_event(JobEvent::Started { rungs: spec.rungs.len() });
148 sink.on_event(JobEvent::Probed {
149 codec: source_codec.clone(),
150 width: header.info.width,
151 height: header.info.height,
152 frame_rate: header.info.frame_rate,
153 audio_codec: audio_track.as_ref().map(|t| t.codec.to_ascii_lowercase()),
154 });
155
156 let frame_rate = {
157 let mut fr = if header.info.frame_rate > 0.0 { header.info.frame_rate } else { 30.0 };
158 if let Some(cap) = spec.max_frame_rate {
159 fr = fr.min(cap);
160 }
161 fr
162 };
163 let frames_total = if header.info.total_frames > 0 {
164 Some(header.info.total_frames)
165 } else {
166 None
167 };
168
169 let prepared_audio = prepare_audio(audio_track.as_ref(), spec.audio).context("preparing audio")?;
170 let audio_handling = prepared_audio
171 .as_ref()
172 .map(|a| a.handling.clone())
173 .unwrap_or_else(|| "none".to_string());
174
175 let filter_chain = Arc::new(
178 codec::filter::FilterChain::prepare(&spec.filters).context("preparing video filters")?,
179 );
180
181 let (rungs, hls_root, master_playlist) = match &spec.mode {
182 OutputMode::SingleFile => {
183 let rungs = run_single_file(
184 input.clone(),
185 spec,
186 &header,
187 frame_rate,
188 frames_total,
189 prepared_audio.as_ref(),
190 Arc::clone(&filter_chain),
191 Arc::clone(&sink),
192 )
193 .await?;
194 (rungs, None, None)
195 }
196 OutputMode::Hls { segment_seconds } => {
197 run_hls(
198 input.clone(),
199 spec,
200 *segment_seconds,
201 &header,
202 frame_rate,
203 prepared_audio.as_ref(),
204 Arc::clone(&filter_chain),
205 output_dir,
206 Arc::clone(&sink),
207 Vec::new(),
210 None,
211 )
212 .await?
213 }
214 };
215
216 let completed = rungs.len();
217 sink.on_event(JobEvent::Finished {
218 rungs_completed: completed,
219 rungs_failed: spec.rungs.len().saturating_sub(completed),
220 });
221
222 Ok(JobOutput {
223 rungs,
224 hls_root,
225 master_playlist,
226 source_codec,
227 source_dims,
228 source_frame_rate,
229 audio_handling,
230 elapsed: started.elapsed(),
231 })
232}
233
234pub fn run_job_blocking(
236 input: &[u8],
237 spec: &OutputSpec,
238 output_dir: Option<&Path>,
239 sink: Arc<dyn ProgressSink>,
240) -> Result<JobOutput> {
241 let rt = tokio::runtime::Builder::new_multi_thread()
242 .enable_all()
243 .build()
244 .context("building Tokio runtime")?;
245 rt.block_on(run_job(Bytes::copy_from_slice(input), spec, output_dir, sink))
246}
247
248pub async fn run_splice_job(
262 clips: Vec<Clip>,
263 spec: &OutputSpec,
264 output_dir: Option<&Path>,
265 sink: Arc<dyn ProgressSink>,
266) -> Result<JobOutput> {
267 let started = Instant::now();
268 spec.validate().context("invalid OutputSpec")?;
269 if clips.is_empty() {
270 bail!("splice requires at least one clip");
271 }
272
273 struct ClipPrep {
275 header: DemuxHeader,
276 audio: Option<PreparedAudio>,
277 src_audio_codec: Option<String>,
278 }
279 let mut preps = Vec::with_capacity(clips.len());
280 for (i, clip) in clips.iter().enumerate() {
281 let demuxer = streaming::demux_streaming(&clip.input)
282 .with_context(|| format!("demuxing splice clip {i}"))?;
283 let header = demuxer.header().clone();
284 let src_audio_codec = demuxer.audio().map(|t| t.codec.to_ascii_lowercase());
285 let audio = prepare_audio(demuxer.audio(), spec.audio)
286 .with_context(|| format!("preparing audio for splice clip {i}"))?;
287 preps.push(ClipPrep { header, audio, src_audio_codec });
288 }
289
290 let primary = preps[0].header.clone();
291 let source_codec = primary.codec.to_ascii_lowercase();
292 let source_dims = (primary.info.width, primary.info.height);
293 let source_frame_rate = primary.info.frame_rate;
294 let frame_rate = {
295 let mut fr = if primary.info.frame_rate > 0.0 { primary.info.frame_rate } else { 30.0 };
296 if let Some(cap) = spec.max_frame_rate {
297 fr = fr.min(cap);
298 }
299 fr
300 };
301
302 sink.on_event(JobEvent::Started { rungs: spec.rungs.len() });
303 sink.on_event(JobEvent::Probed {
304 codec: source_codec.clone(),
305 width: primary.info.width,
306 height: primary.info.height,
307 frame_rate: primary.info.frame_rate,
308 audio_codec: preps[0].src_audio_codec.clone(),
309 });
310
311 for (i, prep) in preps.iter().enumerate().skip(1) {
317 let dims = (prep.header.info.width, prep.header.info.height);
318 let fps = prep.header.info.frame_rate;
319 let fps_differs = fps > 0.0
320 && primary.info.frame_rate > 0.0
321 && (fps - primary.info.frame_rate).abs() > 0.5;
322 if dims != source_dims || fps_differs {
323 tracing::warn!(
324 clip_index = i,
325 clip = %format!("{}x{} @ {:.3} fps", dims.0, dims.1, fps),
326 output = %format!(
327 "{}x{} @ {:.3} fps",
328 source_dims.0, source_dims.1, primary.info.frame_rate
329 ),
330 fps_differs,
331 "splice clip differs from the first clip: resolution is scaled to \
332 the output; frame rate is NOT converted (a differing fps shifts \
333 this clip's timing)"
334 );
335 }
336 }
337
338 let filter_chain = Arc::new(
339 codec::filter::FilterChain::prepare(&spec.filters).context("preparing video filters")?,
340 );
341 let encode_gpu = multigpu::serial_gpu_for_policy(spec.encode_policy);
342 let fastest_decode = if spec.decode_policy.is_fastest() {
346 let candidates = codec::decode::decode_capable_gpu_indices(&primary.codec);
347 if candidates.len() > 1 {
348 crate::decode_pump::fastest_decode_gpu(
349 &primary.codec,
350 &primary.info,
351 &clips[0].input,
352 &candidates,
353 crate::decode_pump::DECODE_BENCH_FRAMES,
354 )
355 } else {
356 None
357 }
358 } else {
359 None
360 };
361 let decode_gpu = spec.decode_policy.gpu_index().or(fastest_decode).or(encode_gpu);
362 let (output_color_metadata, output_pixel_format) =
363 spec.resolve_output(primary.info.color_metadata, primary.info.pixel_format);
364 let base_cfg = EncoderConfig {
365 frame_rate,
366 pixel_format: output_pixel_format,
367 color_metadata: output_color_metadata,
368 gpu_index: encode_gpu,
369 codec: spec.video_codec.codec(),
370 ..EncoderConfig::default()
371 };
372
373 let mut clip_sources = Vec::with_capacity(clips.len());
376 let mut combined_audio: Option<PreparedAudio> = None;
377 let mut effective_total: u64 = 0;
378 let mut total_known = true;
379 for (clip, prep) in clips.iter().zip(preps.iter()) {
380 let cfps = if prep.header.info.frame_rate > 0.0 {
381 prep.header.info.frame_rate
382 } else {
383 frame_rate
384 };
385 let start_frame = trim_frame(clip.start, cfps).unwrap_or(0);
386 let end_frame = trim_frame(clip.end, cfps);
387 match end_frame {
388 Some(e) => effective_total += e.saturating_sub(start_frame),
389 None if prep.header.info.total_frames > 0 => {
390 effective_total += prep.header.info.total_frames.saturating_sub(start_frame)
391 }
392 None => total_known = false,
393 }
394 if let Some(a) = trim_audio(prep.audio.as_ref(), clip.start, clip.end) {
395 if let Some(c) = combined_audio.as_mut() {
396 c.extend(&a);
397 } else {
398 combined_audio = Some(a);
399 }
400 }
401 let pump_cfg = DecodePumpConfig {
402 codec_name: prep.header.codec.clone(),
403 info_for_decoder: prep.header.info.clone(),
404 source_color_metadata: prep.header.info.color_metadata,
405 source_pixel_format: prep.header.info.pixel_format,
406 needs_downsample: needs_chroma_downsample(prep.header.info.pixel_format),
407 tonemap_to_sdr: spec.tonemaps(),
408 gpu_index: decode_gpu,
409 filters: Arc::clone(&filter_chain),
410 };
411 clip_sources.push(ClipSource {
412 cfg: pump_cfg,
413 input: clip.input.clone(),
414 start_frame,
415 end_frame,
416 });
417 }
418 let effective_total = total_known.then_some(effective_total);
419 let audio_handling = combined_audio
420 .as_ref()
421 .map(|a| a.handling.clone())
422 .unwrap_or_else(|| "none".to_string());
423
424 let (rungs, hls_root, master_playlist) = match &spec.mode {
425 OutputMode::SingleFile => {
426 let rungs = run_serial_single_file(
427 clip_sources,
428 spec,
429 base_cfg,
430 frame_rate,
431 effective_total,
432 combined_audio,
433 Arc::clone(&sink),
434 )
435 .await?;
436 (rungs, None, None)
437 }
438 OutputMode::Hls { segment_seconds } => {
439 run_hls(
443 clips[0].input.clone(),
444 spec,
445 *segment_seconds,
446 &primary,
447 frame_rate,
448 combined_audio.as_ref(),
449 Arc::clone(&filter_chain),
450 output_dir,
451 Arc::clone(&sink),
452 clip_sources,
453 effective_total,
454 )
455 .await?
456 }
457 };
458
459 let completed = rungs.len();
460 sink.on_event(JobEvent::Finished {
461 rungs_completed: completed,
462 rungs_failed: spec.rungs.len().saturating_sub(completed),
463 });
464 Ok(JobOutput {
465 rungs,
466 hls_root,
467 master_playlist,
468 source_codec,
469 source_dims,
470 source_frame_rate,
471 audio_handling,
472 elapsed: started.elapsed(),
473 })
474}
475
476pub fn run_splice_job_blocking(
478 clips: Vec<Clip>,
479 spec: &OutputSpec,
480 output_dir: Option<&Path>,
481 sink: Arc<dyn ProgressSink>,
482) -> Result<JobOutput> {
483 let rt = tokio::runtime::Builder::new_multi_thread()
484 .enable_all()
485 .build()
486 .context("building Tokio runtime")?;
487 rt.block_on(run_splice_job(clips, spec, output_dir, sink))
488}
489
490pub(super) fn report_failed(sink: &dyn ProgressSink, rung_index: usize, rung: &Rung, message: &str) {
495 sink.on_rung(RungProgress {
496 rung_index,
497 label: rung.label.clone(),
498 width: rung.width,
499 height: rung.height,
500 status: RungStatus::Failed,
501 percent: 0.0,
502 frames_done: 0,
503 frames_total: None,
504 segments_written: 0,
505 bytes_out: 0,
506 message: Some(message.to_string()),
507 });
508}