1#![forbid(unsafe_code)]
121#![warn(missing_docs)]
122#![allow(clippy::module_name_repetitions)]
123#![allow(clippy::missing_errors_doc)]
124#![allow(clippy::missing_panics_doc)]
125#![allow(clippy::too_many_arguments)]
126
127mod abr;
128pub mod adaptive_bitrate;
129pub mod audio_transcode;
130pub mod bitrate_estimator;
131mod builder;
132mod codec_config;
133pub mod codec_mapping;
134pub mod crf_optimizer;
135mod filters;
136mod hw_accel;
137mod multipass;
138mod normalization;
139mod parallel;
140mod pipeline;
141mod progress;
142mod quality;
143pub mod segment_encoder;
144pub mod segment_transcoder;
145pub mod thumbnail;
146mod transcode_job;
147pub mod two_pass;
148pub mod validation;
149
150pub mod ab_compare;
151pub mod abr_ladder;
152pub mod audio_channel_map;
153pub mod bitrate_control;
154pub mod burn_subs;
155pub mod codec_profile;
156pub mod concat_transcode;
158pub mod crop_scale;
159pub mod encoding_log;
160pub mod examples;
161pub mod frame_stats;
162pub mod output_verify;
163pub mod presets;
164pub mod rate_distortion;
166pub mod resolution_select;
167pub mod scene_cut;
168pub mod stage_graph;
169pub mod transcode_metrics;
170pub mod transcode_session;
171pub mod utils;
172pub mod watermark_overlay;
174pub use codec_config::{
175 codec_config_from_quality, Av1Config, Av1Usage, CodecConfig, H264Config, H264Profile,
176 OpusApplication, OpusConfig, Vp9Config,
177};
178pub use filters::{AudioFilter, FilterNode, VideoFilter};
179pub use hw_accel::{
180 detect_available_hw_accel, detect_best_hw_accel_for_codec, get_available_encoders,
181 HwAccelConfig, HwAccelType, HwEncoder, HwFeature,
182};
183
184pub use abr::{AbrLadder, AbrLadderBuilder, AbrRung, AbrStrategy};
185pub use builder::TranscodeBuilder;
186pub use multipass::{MultiPassConfig, MultiPassEncoder, MultiPassMode};
187pub use normalization::{AudioNormalizer, LoudnessStandard, LoudnessTarget, NormalizationConfig};
188pub use parallel::{ParallelConfig, ParallelEncodeBuilder, ParallelEncoder};
189pub use pipeline::{Pipeline, PipelineStage, TranscodePipeline};
190pub use progress::{ProgressCallback, ProgressInfo, ProgressTracker};
191pub use quality::{QualityConfig, QualityMode, QualityPreset, RateControlMode, TuneMode};
192pub use transcode_job::{JobPriority, JobQueue, TranscodeJob, TranscodeJobConfig, TranscodeStatus};
193pub use validation::{InputValidator, OutputValidator, ValidationError};
194
195use thiserror::Error;
196
197#[derive(Debug, Clone, Error)]
199pub enum TranscodeError {
200 #[error("Invalid input: {0}")]
202 InvalidInput(String),
203
204 #[error("Invalid output: {0}")]
206 InvalidOutput(String),
207
208 #[error("Codec error: {0}")]
210 CodecError(String),
211
212 #[error("Container error: {0}")]
214 ContainerError(String),
215
216 #[error("I/O error: {0}")]
218 IoError(String),
219
220 #[error("Pipeline error: {0}")]
222 PipelineError(String),
223
224 #[error("Multi-pass error: {0}")]
226 MultiPassError(String),
227
228 #[error("Normalization error: {0}")]
230 NormalizationError(String),
231
232 #[error("Validation error: {0}")]
234 ValidationError(#[from] ValidationError),
235
236 #[error("Job error: {0}")]
238 JobError(String),
239
240 #[error("Unsupported: {0}")]
242 Unsupported(String),
243}
244
245impl From<std::io::Error> for TranscodeError {
246 fn from(err: std::io::Error) -> Self {
247 TranscodeError::IoError(err.to_string())
248 }
249}
250
251pub type Result<T> = std::result::Result<T, TranscodeError>;
253
254pub struct Transcoder {
273 config: TranscodeConfig,
274}
275
276#[derive(Debug, Clone)]
278pub struct TranscodeConfig {
279 pub input: Option<String>,
281 pub output: Option<String>,
283 pub video_codec: Option<String>,
285 pub audio_codec: Option<String>,
287 pub video_bitrate: Option<u64>,
289 pub audio_bitrate: Option<u64>,
291 pub width: Option<u32>,
293 pub height: Option<u32>,
295 pub frame_rate: Option<(u32, u32)>,
297 pub multi_pass: Option<MultiPassMode>,
299 pub quality_mode: Option<QualityMode>,
301 pub normalize_audio: bool,
303 pub loudness_standard: Option<LoudnessStandard>,
305 pub hw_accel: bool,
307 pub preserve_metadata: bool,
309 pub subtitle_mode: Option<SubtitleMode>,
311 pub chapter_mode: Option<ChapterMode>,
313}
314
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub enum SubtitleMode {
318 Ignore,
320 Copy,
322 BurnIn,
324}
325
326#[derive(Debug, Clone, Copy, PartialEq, Eq)]
328pub enum ChapterMode {
329 Ignore,
331 Copy,
333 Custom,
335}
336
337impl Default for TranscodeConfig {
338 fn default() -> Self {
339 Self {
340 input: None,
341 output: None,
342 video_codec: None,
343 audio_codec: None,
344 video_bitrate: None,
345 audio_bitrate: None,
346 width: None,
347 height: None,
348 frame_rate: None,
349 multi_pass: None,
350 quality_mode: None,
351 normalize_audio: false,
352 loudness_standard: None,
353 hw_accel: true,
354 preserve_metadata: true,
355 subtitle_mode: None,
356 chapter_mode: None,
357 }
358 }
359}
360
361impl Transcoder {
362 #[must_use]
364 pub fn config(&self) -> &TranscodeConfig {
365 &self.config
366 }
367
368 #[must_use]
370 pub fn new() -> Self {
371 Self {
372 config: TranscodeConfig::default(),
373 }
374 }
375
376 #[must_use]
378 pub fn input(mut self, path: impl Into<String>) -> Self {
379 self.config.input = Some(path.into());
380 self
381 }
382
383 #[must_use]
385 pub fn output(mut self, path: impl Into<String>) -> Self {
386 self.config.output = Some(path.into());
387 self
388 }
389
390 #[must_use]
392 pub fn video_codec(mut self, codec: impl Into<String>) -> Self {
393 self.config.video_codec = Some(codec.into());
394 self
395 }
396
397 #[must_use]
399 pub fn audio_codec(mut self, codec: impl Into<String>) -> Self {
400 self.config.audio_codec = Some(codec.into());
401 self
402 }
403
404 #[must_use]
406 pub fn video_bitrate(mut self, bitrate: u64) -> Self {
407 self.config.video_bitrate = Some(bitrate);
408 self
409 }
410
411 #[must_use]
413 pub fn audio_bitrate(mut self, bitrate: u64) -> Self {
414 self.config.audio_bitrate = Some(bitrate);
415 self
416 }
417
418 #[must_use]
420 pub fn resolution(mut self, width: u32, height: u32) -> Self {
421 self.config.width = Some(width);
422 self.config.height = Some(height);
423 self
424 }
425
426 #[must_use]
428 pub fn frame_rate(mut self, num: u32, den: u32) -> Self {
429 self.config.frame_rate = Some((num, den));
430 self
431 }
432
433 #[must_use]
435 pub fn multi_pass(mut self, mode: MultiPassMode) -> Self {
436 self.config.multi_pass = Some(mode);
437 self
438 }
439
440 #[must_use]
442 pub fn quality(mut self, mode: QualityMode) -> Self {
443 self.config.quality_mode = Some(mode);
444 self
445 }
446
447 #[must_use]
449 pub fn target_bitrate(mut self, bitrate: u64) -> Self {
450 self.config.video_bitrate = Some(bitrate);
451 self
452 }
453
454 #[must_use]
456 pub fn normalize_audio(mut self, enable: bool) -> Self {
457 self.config.normalize_audio = enable;
458 self
459 }
460
461 #[must_use]
463 pub fn loudness_standard(mut self, standard: LoudnessStandard) -> Self {
464 self.config.loudness_standard = Some(standard);
465 self.config.normalize_audio = true;
466 self
467 }
468
469 #[must_use]
471 pub fn hw_accel(mut self, enable: bool) -> Self {
472 self.config.hw_accel = enable;
473 self
474 }
475
476 #[must_use]
478 pub fn preset(mut self, preset: PresetConfig) -> Self {
479 if let Some(codec) = preset.video_codec {
480 self.config.video_codec = Some(codec);
481 }
482 if let Some(codec) = preset.audio_codec {
483 self.config.audio_codec = Some(codec);
484 }
485 if let Some(bitrate) = preset.video_bitrate {
486 self.config.video_bitrate = Some(bitrate);
487 }
488 if let Some(bitrate) = preset.audio_bitrate {
489 self.config.audio_bitrate = Some(bitrate);
490 }
491 if let Some(width) = preset.width {
492 self.config.width = Some(width);
493 }
494 if let Some(height) = preset.height {
495 self.config.height = Some(height);
496 }
497 if let Some(fps) = preset.frame_rate {
498 self.config.frame_rate = Some(fps);
499 }
500 if let Some(mode) = preset.quality_mode {
501 self.config.quality_mode = Some(mode);
502 }
503 self
504 }
505
506 pub async fn transcode(self) -> Result<TranscodeOutput> {
516 let input = self
518 .config
519 .input
520 .ok_or_else(|| TranscodeError::InvalidInput("No input file specified".to_string()))?;
521 let output = self
522 .config
523 .output
524 .ok_or_else(|| TranscodeError::InvalidOutput("No output file specified".to_string()))?;
525
526 let mut pipeline = TranscodePipeline::builder()
528 .input(&input)
529 .output(&output)
530 .build()?;
531
532 if let Some(codec) = &self.config.video_codec {
534 pipeline.set_video_codec(codec);
535 }
536 if let Some(codec) = &self.config.audio_codec {
537 pipeline.set_audio_codec(codec);
538 }
539
540 pipeline.execute().await
542 }
543}
544
545impl Default for Transcoder {
546 fn default() -> Self {
547 Self::new()
548 }
549}
550
551#[derive(Debug, Clone, Default)]
553pub struct PresetConfig {
554 pub video_codec: Option<String>,
556 pub audio_codec: Option<String>,
558 pub video_bitrate: Option<u64>,
560 pub audio_bitrate: Option<u64>,
562 pub width: Option<u32>,
564 pub height: Option<u32>,
566 pub frame_rate: Option<(u32, u32)>,
568 pub quality_mode: Option<QualityMode>,
570 pub container: Option<String>,
572}
573
574#[derive(Debug, Clone)]
576pub struct TranscodeOutput {
577 pub output_path: String,
579 pub file_size: u64,
581 pub duration: f64,
583 pub video_bitrate: u64,
585 pub audio_bitrate: u64,
587 pub encoding_time: f64,
589 pub speed_factor: f64,
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596
597 #[test]
598 fn test_transcoder_builder() {
599 let transcoder = Transcoder::new()
600 .input("input.mp4")
601 .output("output.webm")
602 .video_codec("vp9")
603 .audio_codec("opus")
604 .resolution(1920, 1080)
605 .frame_rate(30, 1);
606
607 assert_eq!(transcoder.config.input, Some("input.mp4".to_string()));
608 assert_eq!(transcoder.config.output, Some("output.webm".to_string()));
609 assert_eq!(transcoder.config.video_codec, Some("vp9".to_string()));
610 assert_eq!(transcoder.config.audio_codec, Some("opus".to_string()));
611 assert_eq!(transcoder.config.width, Some(1920));
612 assert_eq!(transcoder.config.height, Some(1080));
613 assert_eq!(transcoder.config.frame_rate, Some((30, 1)));
614 }
615
616 #[test]
617 fn test_default_config() {
618 let config = TranscodeConfig::default();
619 assert!(config.input.is_none());
620 assert!(config.output.is_none());
621 assert!(config.hw_accel);
622 assert!(config.preserve_metadata);
623 assert!(!config.normalize_audio);
624 }
625
626 #[test]
627 fn test_preset_application() {
628 let preset = PresetConfig {
629 video_codec: Some("vp9".to_string()),
630 audio_codec: Some("opus".to_string()),
631 video_bitrate: Some(5_000_000),
632 audio_bitrate: Some(128_000),
633 width: Some(1920),
634 height: Some(1080),
635 frame_rate: Some((60, 1)),
636 quality_mode: Some(QualityMode::High),
637 container: Some("webm".to_string()),
638 };
639
640 let transcoder = Transcoder::new().preset(preset);
641
642 assert_eq!(transcoder.config.video_codec, Some("vp9".to_string()));
643 assert_eq!(transcoder.config.audio_codec, Some("opus".to_string()));
644 assert_eq!(transcoder.config.video_bitrate, Some(5_000_000));
645 assert_eq!(transcoder.config.audio_bitrate, Some(128_000));
646 assert_eq!(transcoder.config.width, Some(1920));
647 assert_eq!(transcoder.config.height, Some(1080));
648 assert_eq!(transcoder.config.frame_rate, Some((60, 1)));
649 assert_eq!(transcoder.config.quality_mode, Some(QualityMode::High));
650 }
651
652 #[test]
653 fn test_subtitle_modes() {
654 assert_eq!(SubtitleMode::Ignore, SubtitleMode::Ignore);
655 assert_ne!(SubtitleMode::Ignore, SubtitleMode::Copy);
656 assert_ne!(SubtitleMode::Copy, SubtitleMode::BurnIn);
657 }
658
659 #[test]
660 fn test_chapter_modes() {
661 assert_eq!(ChapterMode::Ignore, ChapterMode::Ignore);
662 assert_ne!(ChapterMode::Ignore, ChapterMode::Copy);
663 assert_ne!(ChapterMode::Copy, ChapterMode::Custom);
664 }
665}