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