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;
136#[cfg(not(target_arch = "wasm32"))]
137pub mod frame_pipeline;
138mod hw_accel;
139mod multipass;
140mod normalization;
141mod parallel;
142#[cfg(not(target_arch = "wasm32"))]
143mod pipeline;
144#[cfg(not(target_arch = "wasm32"))]
145pub mod pipeline_context;
146mod progress;
147mod quality;
148pub mod segment_encoder;
149pub mod segment_transcoder;
150pub mod thumbnail;
151mod transcode_job;
152pub mod two_pass;
153pub mod validation;
154
155pub mod ab_compare;
156pub mod abr_ladder;
157pub mod audio_channel_map;
158pub mod audio_only;
159pub mod benchmark;
160pub mod bitrate_control;
161pub mod burn_subs;
162pub mod codec_profile;
163pub mod concat_transcode;
165pub mod crop_scale;
166pub mod encoding_log;
167#[cfg(not(target_arch = "wasm32"))]
168pub mod examples;
169pub mod frame_stats;
170pub mod frame_trim;
171pub mod hdr_passthrough;
172pub mod hwaccel;
174pub mod output_verify;
175pub mod per_scene_encode;
176pub mod presets;
177pub mod quality_ladder_gen;
178pub mod rate_distortion;
179pub mod resolution_select;
180pub mod scene_cut;
181pub mod stage_graph;
182pub mod stream_copy;
184pub mod transcode_metrics;
185pub mod transcode_preset;
186pub mod transcode_profile;
187pub mod transcode_session;
188pub mod utils;
189pub mod watch_folder;
190pub mod watermark_overlay;
191pub use codec_config::{
192 codec_config_from_quality, Av1Config, Av1Usage, CodecConfig, Ffv1Coder, Ffv1Config, Ffv1Level,
193 FlacConfig, H264Config, H264Profile, JxlConfig, JxlEffort, OpusApplication, OpusConfig,
194 Vp9Config,
195};
196pub use codec_profile::CodecTunePreset;
197pub use filters::{AudioFilter, FilterNode, VideoFilter};
198pub use hw_accel::{
199 detect_available_hw_accel, detect_best_hw_accel_for_codec, get_available_encoders,
200 HwAccelConfig, HwAccelType, HwEncoder, HwFeature,
201};
202pub use stream_copy::{
203 CopyDecision, StreamCopyConfig, StreamCopyDetector, StreamCopyMode, StreamInfo, StreamType,
204};
205
206pub use abr::{AbrLadder, AbrLadderBuilder, AbrRung, AbrStrategy};
207pub use builder::TranscodeBuilder;
208pub use concat_transcode::{
209 AnnotatedSegment, ConcatPlan, ConcatStep, MixedSourceConcatenator, SourceProperties,
210};
211#[cfg(not(target_arch = "wasm32"))]
212pub use frame_pipeline::{
213 wire_hdr_into_pipeline, AudioFrameOp, FramePipelineConfig, FramePipelineExecutor,
214 FramePipelineResult, VideoFrameOp,
215};
216pub use multipass::{MultiPassConfig, MultiPassEncoder, MultiPassMode};
217pub use normalization::{AudioNormalizer, LoudnessStandard, LoudnessTarget, NormalizationConfig};
218pub use parallel::{
219 assemble_av1_tile_bitstream, Av1TileConfig, Av1TileParallelEncoder, Av1TileStats,
220 ParallelConfig, ParallelEncodeBuilder, ParallelEncoder,
221};
222#[cfg(not(target_arch = "wasm32"))]
223pub use pipeline::{Pipeline, PipelineStage, TranscodePipeline};
224#[cfg(not(target_arch = "wasm32"))]
225pub use pipeline_context::{
226 FilterGraph, Frame, FrameDecoder, FrameEncoder, HdrPassthroughConfig, HdrSeiInjector,
227 PassStats, TranscodeContext, TranscodeStats,
228};
229pub use progress::{ProgressCallback, ProgressInfo, ProgressTracker};
230pub use quality::{QualityConfig, QualityMode, QualityPreset, RateControlMode, TuneMode};
231pub use segment_encoder::{
232 ParallelSegmentEncoder, ParallelSegmentResult, ParallelSegmentStats, SegmentSpec,
233};
234pub use thumbnail::{format_vtt_time, SpriteSheet, SpriteSheetConfig};
235pub use transcode_job::{JobPriority, JobQueue, TranscodeJob, TranscodeJobConfig, TranscodeStatus};
236pub use transcode_preset::{TranscodeEstimator, TranscodePreset};
237pub use validation::{InputValidator, OutputValidator, ValidationError};
238
239use thiserror::Error;
240
241#[derive(Debug, Clone, Error)]
243pub enum TranscodeError {
244 #[error("Invalid input: {0}")]
246 InvalidInput(String),
247
248 #[error("Invalid output: {0}")]
250 InvalidOutput(String),
251
252 #[error("Codec error: {0}")]
254 CodecError(String),
255
256 #[error("Container error: {0}")]
258 ContainerError(String),
259
260 #[error("I/O error: {0}")]
262 IoError(String),
263
264 #[error("Pipeline error: {0}")]
266 PipelineError(String),
267
268 #[error("Multi-pass error: {0}")]
270 MultiPassError(String),
271
272 #[error("Normalization error: {0}")]
274 NormalizationError(String),
275
276 #[error("Validation error: {0}")]
278 ValidationError(#[from] ValidationError),
279
280 #[error("Job error: {0}")]
282 JobError(String),
283
284 #[error("Unsupported: {0}")]
286 Unsupported(String),
287}
288
289impl From<std::io::Error> for TranscodeError {
290 fn from(err: std::io::Error) -> Self {
291 TranscodeError::IoError(err.to_string())
292 }
293}
294
295pub type Result<T> = std::result::Result<T, TranscodeError>;
297
298pub struct Transcoder {
317 config: TranscodeConfig,
318}
319
320#[derive(Debug, Clone)]
322pub struct TranscodeConfig {
323 pub input: Option<String>,
325 pub output: Option<String>,
327 pub video_codec: Option<String>,
329 pub audio_codec: Option<String>,
331 pub video_bitrate: Option<u64>,
333 pub audio_bitrate: Option<u64>,
335 pub width: Option<u32>,
337 pub height: Option<u32>,
339 pub frame_rate: Option<(u32, u32)>,
341 pub multi_pass: Option<MultiPassMode>,
343 pub quality_mode: Option<QualityMode>,
345 pub normalize_audio: bool,
347 pub loudness_standard: Option<LoudnessStandard>,
349 pub hw_accel: bool,
351 pub preserve_metadata: bool,
353 pub subtitle_mode: Option<SubtitleMode>,
355 pub chapter_mode: Option<ChapterMode>,
357 pub stream_copy: Option<StreamCopyMode>,
359 pub audio_channel_layout: Option<audio_channel_map::AudioLayout>,
361}
362
363#[derive(Debug, Clone, Copy, PartialEq, Eq)]
365pub enum SubtitleMode {
366 Ignore,
368 Copy,
370 BurnIn,
372}
373
374#[derive(Debug, Clone, Copy, PartialEq, Eq)]
376pub enum ChapterMode {
377 Ignore,
379 Copy,
381 Custom,
383}
384
385impl Default for TranscodeConfig {
386 fn default() -> Self {
387 Self {
388 input: None,
389 output: None,
390 video_codec: None,
391 audio_codec: None,
392 video_bitrate: None,
393 audio_bitrate: None,
394 width: None,
395 height: None,
396 frame_rate: None,
397 multi_pass: None,
398 quality_mode: None,
399 normalize_audio: false,
400 loudness_standard: None,
401 hw_accel: true,
402 preserve_metadata: true,
403 subtitle_mode: None,
404 chapter_mode: None,
405 stream_copy: None,
406 audio_channel_layout: None,
407 }
408 }
409}
410
411impl Transcoder {
412 #[must_use]
414 pub fn config(&self) -> &TranscodeConfig {
415 &self.config
416 }
417
418 #[must_use]
420 pub fn new() -> Self {
421 Self {
422 config: TranscodeConfig::default(),
423 }
424 }
425
426 #[must_use]
428 pub fn input(mut self, path: impl Into<String>) -> Self {
429 self.config.input = Some(path.into());
430 self
431 }
432
433 #[must_use]
435 pub fn output(mut self, path: impl Into<String>) -> Self {
436 self.config.output = Some(path.into());
437 self
438 }
439
440 #[must_use]
442 pub fn video_codec(mut self, codec: impl Into<String>) -> Self {
443 self.config.video_codec = Some(codec.into());
444 self
445 }
446
447 #[must_use]
449 pub fn audio_codec(mut self, codec: impl Into<String>) -> Self {
450 self.config.audio_codec = Some(codec.into());
451 self
452 }
453
454 #[must_use]
456 pub fn video_bitrate(mut self, bitrate: u64) -> Self {
457 self.config.video_bitrate = Some(bitrate);
458 self
459 }
460
461 #[must_use]
463 pub fn audio_bitrate(mut self, bitrate: u64) -> Self {
464 self.config.audio_bitrate = Some(bitrate);
465 self
466 }
467
468 #[must_use]
470 pub fn resolution(mut self, width: u32, height: u32) -> Self {
471 self.config.width = Some(width);
472 self.config.height = Some(height);
473 self
474 }
475
476 #[must_use]
478 pub fn frame_rate(mut self, num: u32, den: u32) -> Self {
479 self.config.frame_rate = Some((num, den));
480 self
481 }
482
483 #[must_use]
485 pub fn multi_pass(mut self, mode: MultiPassMode) -> Self {
486 self.config.multi_pass = Some(mode);
487 self
488 }
489
490 #[must_use]
492 pub fn quality(mut self, mode: QualityMode) -> Self {
493 self.config.quality_mode = Some(mode);
494 self
495 }
496
497 #[must_use]
499 pub fn target_bitrate(mut self, bitrate: u64) -> Self {
500 self.config.video_bitrate = Some(bitrate);
501 self
502 }
503
504 #[must_use]
506 pub fn normalize_audio(mut self, enable: bool) -> Self {
507 self.config.normalize_audio = enable;
508 self
509 }
510
511 #[must_use]
513 pub fn loudness_standard(mut self, standard: LoudnessStandard) -> Self {
514 self.config.loudness_standard = Some(standard);
515 self.config.normalize_audio = true;
516 self
517 }
518
519 #[must_use]
521 pub fn hw_accel(mut self, enable: bool) -> Self {
522 self.config.hw_accel = enable;
523 self
524 }
525
526 #[must_use]
531 pub fn stream_copy(mut self, mode: StreamCopyMode) -> Self {
532 self.config.stream_copy = Some(mode);
533 self
534 }
535
536 #[must_use]
538 pub fn audio_channel_layout(mut self, layout: audio_channel_map::AudioLayout) -> Self {
539 self.config.audio_channel_layout = Some(layout);
540 self
541 }
542
543 #[must_use]
545 pub fn preset(mut self, preset: PresetConfig) -> Self {
546 if let Some(codec) = preset.video_codec {
547 self.config.video_codec = Some(codec);
548 }
549 if let Some(codec) = preset.audio_codec {
550 self.config.audio_codec = Some(codec);
551 }
552 if let Some(bitrate) = preset.video_bitrate {
553 self.config.video_bitrate = Some(bitrate);
554 }
555 if let Some(bitrate) = preset.audio_bitrate {
556 self.config.audio_bitrate = Some(bitrate);
557 }
558 if let Some(width) = preset.width {
559 self.config.width = Some(width);
560 }
561 if let Some(height) = preset.height {
562 self.config.height = Some(height);
563 }
564 if let Some(fps) = preset.frame_rate {
565 self.config.frame_rate = Some(fps);
566 }
567 if let Some(mode) = preset.quality_mode {
568 self.config.quality_mode = Some(mode);
569 }
570 if let Some(layout) = preset.audio_channel_layout {
571 self.config.audio_channel_layout = Some(layout);
572 }
573 self
574 }
575
576 pub async fn transcode(self) -> Result<TranscodeOutput> {
587 #[cfg(target_arch = "wasm32")]
588 {
589 let _ = self;
590 return Err(TranscodeError::Unsupported(
591 "Filesystem-based transcoding is not supported on wasm32".to_string(),
592 ));
593 }
594
595 #[cfg(not(target_arch = "wasm32"))]
596 {
597 let input = self.config.input.ok_or_else(|| {
599 TranscodeError::InvalidInput("No input file specified".to_string())
600 })?;
601 let output = self.config.output.ok_or_else(|| {
602 TranscodeError::InvalidOutput("No output file specified".to_string())
603 })?;
604
605 let mut pipeline = TranscodePipeline::builder()
607 .input(&input)
608 .output(&output)
609 .build()?;
610
611 if let Some(codec) = &self.config.video_codec {
613 pipeline.set_video_codec(codec);
614 }
615 if let Some(codec) = &self.config.audio_codec {
616 pipeline.set_audio_codec(codec);
617 }
618
619 pipeline.execute().await
621 }
622 }
623}
624
625impl Default for Transcoder {
626 fn default() -> Self {
627 Self::new()
628 }
629}
630
631#[derive(Debug, Clone, Default)]
633pub struct PresetConfig {
634 pub video_codec: Option<String>,
636 pub audio_codec: Option<String>,
638 pub video_bitrate: Option<u64>,
640 pub audio_bitrate: Option<u64>,
642 pub width: Option<u32>,
644 pub height: Option<u32>,
646 pub frame_rate: Option<(u32, u32)>,
648 pub quality_mode: Option<QualityMode>,
650 pub container: Option<String>,
652 pub audio_channel_layout: Option<audio_channel_map::AudioLayout>,
654}
655
656#[derive(Debug, Clone)]
658pub struct TranscodeOutput {
659 pub output_path: String,
661 pub file_size: u64,
663 pub duration: f64,
665 pub video_bitrate: u64,
667 pub audio_bitrate: u64,
669 pub encoding_time: f64,
671 pub speed_factor: f64,
673}
674
675#[cfg(test)]
676mod tests {
677 use super::*;
678
679 #[test]
680 fn test_transcoder_builder() {
681 let transcoder = Transcoder::new()
682 .input("input.mp4")
683 .output("output.webm")
684 .video_codec("vp9")
685 .audio_codec("opus")
686 .resolution(1920, 1080)
687 .frame_rate(30, 1);
688
689 assert_eq!(transcoder.config.input, Some("input.mp4".to_string()));
690 assert_eq!(transcoder.config.output, Some("output.webm".to_string()));
691 assert_eq!(transcoder.config.video_codec, Some("vp9".to_string()));
692 assert_eq!(transcoder.config.audio_codec, Some("opus".to_string()));
693 assert_eq!(transcoder.config.width, Some(1920));
694 assert_eq!(transcoder.config.height, Some(1080));
695 assert_eq!(transcoder.config.frame_rate, Some((30, 1)));
696 }
697
698 #[test]
699 fn test_default_config() {
700 let config = TranscodeConfig::default();
701 assert!(config.input.is_none());
702 assert!(config.output.is_none());
703 assert!(config.hw_accel);
704 assert!(config.preserve_metadata);
705 assert!(!config.normalize_audio);
706 }
707
708 #[test]
709 fn test_preset_application() {
710 let preset = PresetConfig {
711 video_codec: Some("vp9".to_string()),
712 audio_codec: Some("opus".to_string()),
713 video_bitrate: Some(5_000_000),
714 audio_bitrate: Some(128_000),
715 width: Some(1920),
716 height: Some(1080),
717 frame_rate: Some((60, 1)),
718 quality_mode: Some(QualityMode::High),
719 container: Some("webm".to_string()),
720 audio_channel_layout: None,
721 };
722
723 let transcoder = Transcoder::new().preset(preset);
724
725 assert_eq!(transcoder.config.video_codec, Some("vp9".to_string()));
726 assert_eq!(transcoder.config.audio_codec, Some("opus".to_string()));
727 assert_eq!(transcoder.config.video_bitrate, Some(5_000_000));
728 assert_eq!(transcoder.config.audio_bitrate, Some(128_000));
729 assert_eq!(transcoder.config.width, Some(1920));
730 assert_eq!(transcoder.config.height, Some(1080));
731 assert_eq!(transcoder.config.frame_rate, Some((60, 1)));
732 assert_eq!(transcoder.config.quality_mode, Some(QualityMode::High));
733 }
734
735 #[test]
736 fn test_stream_copy_mode() {
737 let transcoder = Transcoder::new()
738 .input("input.mp4")
739 .output("output.mp4")
740 .stream_copy(StreamCopyMode::CopyVideo);
741
742 assert_eq!(
743 transcoder.config.stream_copy,
744 Some(StreamCopyMode::CopyVideo)
745 );
746 }
747
748 #[test]
749 fn test_audio_channel_layout_on_transcoder() {
750 let transcoder =
751 Transcoder::new().audio_channel_layout(audio_channel_map::AudioLayout::FivePointOne);
752
753 assert_eq!(
754 transcoder.config.audio_channel_layout,
755 Some(audio_channel_map::AudioLayout::FivePointOne)
756 );
757 }
758
759 #[test]
760 fn test_preset_with_audio_channel_layout() {
761 let preset = PresetConfig {
762 audio_codec: Some("opus".to_string()),
763 audio_bitrate: Some(384_000),
764 audio_channel_layout: Some(audio_channel_map::AudioLayout::FivePointOne),
765 ..PresetConfig::default()
766 };
767
768 let transcoder = Transcoder::new().preset(preset);
769 assert_eq!(
770 transcoder.config.audio_channel_layout,
771 Some(audio_channel_map::AudioLayout::FivePointOne)
772 );
773 assert_eq!(transcoder.config.audio_bitrate, Some(384_000));
774 }
775
776 #[test]
777 fn test_preset_config_default_has_no_channel_layout() {
778 let preset = PresetConfig::default();
779 assert!(preset.audio_channel_layout.is_none());
780 }
781
782 #[test]
783 fn test_config_default_has_no_stream_copy() {
784 let config = TranscodeConfig::default();
785 assert!(config.stream_copy.is_none());
786 assert!(config.audio_channel_layout.is_none());
787 }
788
789 #[test]
790 fn test_subtitle_modes() {
791 assert_eq!(SubtitleMode::Ignore, SubtitleMode::Ignore);
792 assert_ne!(SubtitleMode::Ignore, SubtitleMode::Copy);
793 assert_ne!(SubtitleMode::Copy, SubtitleMode::BurnIn);
794 }
795
796 #[test]
797 fn test_chapter_modes() {
798 assert_eq!(ChapterMode::Ignore, ChapterMode::Ignore);
799 assert_ne!(ChapterMode::Ignore, ChapterMode::Copy);
800 assert_ne!(ChapterMode::Copy, ChapterMode::Custom);
801 }
802}