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 running_stats;
184pub mod scene_cut;
185pub mod stage_graph;
186pub mod stream_copy;
188pub mod transcode_metrics;
189pub mod transcode_preset;
190pub mod transcode_profile;
191pub mod transcode_session;
192pub mod utils;
193pub mod watch_folder;
194pub mod watermark_overlay;
195pub use codec_config::{
196 codec_config_from_quality, Av1Config, Av1Usage, CodecConfig, Ffv1Coder, Ffv1Config, Ffv1Level,
197 FlacConfig, H264Config, H264Profile, JxlConfig, JxlEffort, OpusApplication, OpusConfig,
198 Vp9Config,
199};
200pub use codec_dispatch::{make_video_encoder, VideoEncoderParams};
201pub use codec_profile::CodecTunePreset;
202pub use filters::{AudioFilter, FilterNode, VideoFilter};
203pub use hw_accel::{
204 detect_available_hw_accel, detect_best_hw_accel_for_codec, detect_hw_accel_caps,
205 detect_hw_accel_with_probe, get_available_encoders, HwAccelCapabilities, HwAccelConfig,
206 HwAccelDevice, HwAccelType, HwEncoder, HwFeature, HwKind, HwProbe, MockProbe, SystemProbe,
207};
208pub use stream_copy::{
209 CopyDecision, StreamCopyConfig, StreamCopyDetector, StreamCopyMode, StreamInfo, StreamType,
210};
211
212pub use abr::{AbrLadder, AbrLadderBuilder, AbrRung, AbrStrategy};
213pub use builder::TranscodeBuilder;
214pub use concat_transcode::{
215 AnnotatedSegment, ConcatPlan, ConcatStep, MixedSourceConcatenator, SourceProperties,
216};
217#[cfg(not(target_arch = "wasm32"))]
218pub use frame_pipeline::{
219 wire_hdr_into_pipeline, AudioFrameOp, FramePipelineConfig, FramePipelineExecutor,
220 FramePipelineResult, VideoFrameOp,
221};
222#[cfg(not(target_arch = "wasm32"))]
223pub use multi_track::{MultiTrackExecutor, MultiTrackStats, PerTrack};
224pub use multipass::{MultiPassConfig, MultiPassEncoder, MultiPassMode};
225pub use normalization::{AudioNormalizer, LoudnessStandard, LoudnessTarget, NormalizationConfig};
226pub use parallel::{
227 assemble_av1_tile_bitstream, Av1TileConfig, Av1TileParallelEncoder, Av1TileStats,
228 ParallelConfig, ParallelEncodeBuilder, ParallelEncoder,
229};
230#[cfg(not(target_arch = "wasm32"))]
231pub use pipeline::{Pipeline, PipelineStage, TranscodePipeline};
232#[cfg(not(target_arch = "wasm32"))]
233pub use pipeline_context::{
234 FilterGraph, Frame, FrameDecoder, FrameEncoder, HdrPassthroughConfig, HdrSeiInjector,
235 PassStats, TranscodeContext, TranscodeStats,
236};
237pub use progress::{ProgressCallback, ProgressInfo, ProgressTracker};
238pub use quality::{QualityConfig, QualityMode, QualityPreset, RateControlMode, TuneMode};
239pub use segment_encoder::{
240 ParallelSegmentEncoder, ParallelSegmentResult, ParallelSegmentStats, SegmentSpec,
241};
242pub use thumbnail::{format_vtt_time, SpriteSheet, SpriteSheetConfig};
243pub use transcode_job::{JobPriority, JobQueue, TranscodeJob, TranscodeJobConfig, TranscodeStatus};
244pub use transcode_preset::{TranscodeEstimator, TranscodePreset};
245pub use validation::{InputValidator, OutputValidator, ValidationError};
246
247use thiserror::Error;
248
249#[derive(Debug, Clone, Error)]
251pub enum TranscodeError {
252 #[error("Invalid input: {0}")]
254 InvalidInput(String),
255
256 #[error("Invalid output: {0}")]
258 InvalidOutput(String),
259
260 #[error("Codec error: {0}")]
262 CodecError(String),
263
264 #[error("Container error: {0}")]
266 ContainerError(String),
267
268 #[error("I/O error: {0}")]
270 IoError(String),
271
272 #[error("Pipeline error: {0}")]
274 PipelineError(String),
275
276 #[error("Multi-pass error: {0}")]
278 MultiPassError(String),
279
280 #[error("Normalization error: {0}")]
282 NormalizationError(String),
283
284 #[error("Validation error: {0}")]
286 ValidationError(#[from] ValidationError),
287
288 #[error("Job error: {0}")]
290 JobError(String),
291
292 #[error("Unsupported: {0}")]
294 Unsupported(String),
295}
296
297impl From<std::io::Error> for TranscodeError {
298 fn from(err: std::io::Error) -> Self {
299 TranscodeError::IoError(err.to_string())
300 }
301}
302
303pub type Result<T> = std::result::Result<T, TranscodeError>;
305
306pub struct Transcoder {
325 config: TranscodeConfig,
326}
327
328#[derive(Debug, Clone)]
330pub struct TranscodeConfig {
331 pub input: Option<String>,
333 pub output: Option<String>,
335 pub video_codec: Option<String>,
337 pub audio_codec: Option<String>,
339 pub video_bitrate: Option<u64>,
341 pub audio_bitrate: Option<u64>,
343 pub width: Option<u32>,
345 pub height: Option<u32>,
347 pub frame_rate: Option<(u32, u32)>,
349 pub multi_pass: Option<MultiPassMode>,
351 pub quality_mode: Option<QualityMode>,
353 pub normalize_audio: bool,
355 pub loudness_standard: Option<LoudnessStandard>,
357 pub hw_accel: bool,
359 pub preserve_metadata: bool,
361 pub subtitle_mode: Option<SubtitleMode>,
363 pub chapter_mode: Option<ChapterMode>,
365 pub stream_copy: Option<StreamCopyMode>,
367 pub audio_channel_layout: Option<audio_channel_map::AudioLayout>,
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq)]
373pub enum SubtitleMode {
374 Ignore,
376 Copy,
378 BurnIn,
380}
381
382#[derive(Debug, Clone, Copy, PartialEq, Eq)]
384pub enum ChapterMode {
385 Ignore,
387 Copy,
389 Custom,
391}
392
393impl Default for TranscodeConfig {
394 fn default() -> Self {
395 Self {
396 input: None,
397 output: None,
398 video_codec: None,
399 audio_codec: None,
400 video_bitrate: None,
401 audio_bitrate: None,
402 width: None,
403 height: None,
404 frame_rate: None,
405 multi_pass: None,
406 quality_mode: None,
407 normalize_audio: false,
408 loudness_standard: None,
409 hw_accel: true,
410 preserve_metadata: true,
411 subtitle_mode: None,
412 chapter_mode: None,
413 stream_copy: None,
414 audio_channel_layout: None,
415 }
416 }
417}
418
419impl Transcoder {
420 #[must_use]
422 pub fn config(&self) -> &TranscodeConfig {
423 &self.config
424 }
425
426 #[must_use]
428 pub fn new() -> Self {
429 Self {
430 config: TranscodeConfig::default(),
431 }
432 }
433
434 #[must_use]
436 pub fn input(mut self, path: impl Into<String>) -> Self {
437 self.config.input = Some(path.into());
438 self
439 }
440
441 #[must_use]
443 pub fn output(mut self, path: impl Into<String>) -> Self {
444 self.config.output = Some(path.into());
445 self
446 }
447
448 #[must_use]
450 pub fn video_codec(mut self, codec: impl Into<String>) -> Self {
451 self.config.video_codec = Some(codec.into());
452 self
453 }
454
455 #[must_use]
457 pub fn audio_codec(mut self, codec: impl Into<String>) -> Self {
458 self.config.audio_codec = Some(codec.into());
459 self
460 }
461
462 #[must_use]
464 pub fn video_bitrate(mut self, bitrate: u64) -> Self {
465 self.config.video_bitrate = Some(bitrate);
466 self
467 }
468
469 #[must_use]
471 pub fn audio_bitrate(mut self, bitrate: u64) -> Self {
472 self.config.audio_bitrate = Some(bitrate);
473 self
474 }
475
476 #[must_use]
478 pub fn resolution(mut self, width: u32, height: u32) -> Self {
479 self.config.width = Some(width);
480 self.config.height = Some(height);
481 self
482 }
483
484 #[must_use]
486 pub fn frame_rate(mut self, num: u32, den: u32) -> Self {
487 self.config.frame_rate = Some((num, den));
488 self
489 }
490
491 #[must_use]
493 pub fn multi_pass(mut self, mode: MultiPassMode) -> Self {
494 self.config.multi_pass = Some(mode);
495 self
496 }
497
498 #[must_use]
500 pub fn quality(mut self, mode: QualityMode) -> Self {
501 self.config.quality_mode = Some(mode);
502 self
503 }
504
505 #[must_use]
507 pub fn target_bitrate(mut self, bitrate: u64) -> Self {
508 self.config.video_bitrate = Some(bitrate);
509 self
510 }
511
512 #[must_use]
514 pub fn normalize_audio(mut self, enable: bool) -> Self {
515 self.config.normalize_audio = enable;
516 self
517 }
518
519 #[must_use]
521 pub fn loudness_standard(mut self, standard: LoudnessStandard) -> Self {
522 self.config.loudness_standard = Some(standard);
523 self.config.normalize_audio = true;
524 self
525 }
526
527 #[must_use]
529 pub fn hw_accel(mut self, enable: bool) -> Self {
530 self.config.hw_accel = enable;
531 self
532 }
533
534 #[must_use]
539 pub fn stream_copy(mut self, mode: StreamCopyMode) -> Self {
540 self.config.stream_copy = Some(mode);
541 self
542 }
543
544 #[must_use]
546 pub fn audio_channel_layout(mut self, layout: audio_channel_map::AudioLayout) -> Self {
547 self.config.audio_channel_layout = Some(layout);
548 self
549 }
550
551 #[must_use]
553 pub fn preset(mut self, preset: PresetConfig) -> Self {
554 if let Some(codec) = preset.video_codec {
555 self.config.video_codec = Some(codec);
556 }
557 if let Some(codec) = preset.audio_codec {
558 self.config.audio_codec = Some(codec);
559 }
560 if let Some(bitrate) = preset.video_bitrate {
561 self.config.video_bitrate = Some(bitrate);
562 }
563 if let Some(bitrate) = preset.audio_bitrate {
564 self.config.audio_bitrate = Some(bitrate);
565 }
566 if let Some(width) = preset.width {
567 self.config.width = Some(width);
568 }
569 if let Some(height) = preset.height {
570 self.config.height = Some(height);
571 }
572 if let Some(fps) = preset.frame_rate {
573 self.config.frame_rate = Some(fps);
574 }
575 if let Some(mode) = preset.quality_mode {
576 self.config.quality_mode = Some(mode);
577 }
578 if let Some(layout) = preset.audio_channel_layout {
579 self.config.audio_channel_layout = Some(layout);
580 }
581 self
582 }
583
584 pub async fn transcode(self) -> Result<TranscodeOutput> {
595 #[cfg(target_arch = "wasm32")]
596 {
597 let _ = self;
598 return Err(TranscodeError::Unsupported(
599 "Filesystem-based transcoding is not supported on wasm32".to_string(),
600 ));
601 }
602
603 #[cfg(not(target_arch = "wasm32"))]
604 {
605 let input = self.config.input.ok_or_else(|| {
607 TranscodeError::InvalidInput("No input file specified".to_string())
608 })?;
609 let output = self.config.output.ok_or_else(|| {
610 TranscodeError::InvalidOutput("No output file specified".to_string())
611 })?;
612
613 let mut pipeline = TranscodePipeline::builder()
615 .input(&input)
616 .output(&output)
617 .build()?;
618
619 if let Some(codec) = &self.config.video_codec {
621 pipeline.set_video_codec(codec);
622 }
623 if let Some(codec) = &self.config.audio_codec {
624 pipeline.set_audio_codec(codec);
625 }
626
627 pipeline.execute().await
629 }
630 }
631}
632
633impl Default for Transcoder {
634 fn default() -> Self {
635 Self::new()
636 }
637}
638
639#[derive(Debug, Clone, Default)]
641pub struct PresetConfig {
642 pub video_codec: Option<String>,
644 pub audio_codec: Option<String>,
646 pub video_bitrate: Option<u64>,
648 pub audio_bitrate: Option<u64>,
650 pub width: Option<u32>,
652 pub height: Option<u32>,
654 pub frame_rate: Option<(u32, u32)>,
656 pub quality_mode: Option<QualityMode>,
658 pub container: Option<String>,
660 pub audio_channel_layout: Option<audio_channel_map::AudioLayout>,
662}
663
664#[derive(Debug, Clone)]
666pub struct TranscodeOutput {
667 pub output_path: String,
669 pub file_size: u64,
671 pub duration: f64,
673 pub video_bitrate: u64,
675 pub audio_bitrate: u64,
677 pub encoding_time: f64,
679 pub speed_factor: f64,
681}
682
683#[cfg(test)]
684mod tests {
685 use super::*;
686
687 #[test]
688 fn test_transcoder_builder() {
689 let transcoder = Transcoder::new()
690 .input("input.mp4")
691 .output("output.webm")
692 .video_codec("vp9")
693 .audio_codec("opus")
694 .resolution(1920, 1080)
695 .frame_rate(30, 1);
696
697 assert_eq!(transcoder.config.input, Some("input.mp4".to_string()));
698 assert_eq!(transcoder.config.output, Some("output.webm".to_string()));
699 assert_eq!(transcoder.config.video_codec, Some("vp9".to_string()));
700 assert_eq!(transcoder.config.audio_codec, Some("opus".to_string()));
701 assert_eq!(transcoder.config.width, Some(1920));
702 assert_eq!(transcoder.config.height, Some(1080));
703 assert_eq!(transcoder.config.frame_rate, Some((30, 1)));
704 }
705
706 #[test]
707 fn test_default_config() {
708 let config = TranscodeConfig::default();
709 assert!(config.input.is_none());
710 assert!(config.output.is_none());
711 assert!(config.hw_accel);
712 assert!(config.preserve_metadata);
713 assert!(!config.normalize_audio);
714 }
715
716 #[test]
717 fn test_preset_application() {
718 let preset = PresetConfig {
719 video_codec: Some("vp9".to_string()),
720 audio_codec: Some("opus".to_string()),
721 video_bitrate: Some(5_000_000),
722 audio_bitrate: Some(128_000),
723 width: Some(1920),
724 height: Some(1080),
725 frame_rate: Some((60, 1)),
726 quality_mode: Some(QualityMode::High),
727 container: Some("webm".to_string()),
728 audio_channel_layout: None,
729 };
730
731 let transcoder = Transcoder::new().preset(preset);
732
733 assert_eq!(transcoder.config.video_codec, Some("vp9".to_string()));
734 assert_eq!(transcoder.config.audio_codec, Some("opus".to_string()));
735 assert_eq!(transcoder.config.video_bitrate, Some(5_000_000));
736 assert_eq!(transcoder.config.audio_bitrate, Some(128_000));
737 assert_eq!(transcoder.config.width, Some(1920));
738 assert_eq!(transcoder.config.height, Some(1080));
739 assert_eq!(transcoder.config.frame_rate, Some((60, 1)));
740 assert_eq!(transcoder.config.quality_mode, Some(QualityMode::High));
741 }
742
743 #[test]
744 fn test_stream_copy_mode() {
745 let transcoder = Transcoder::new()
746 .input("input.mp4")
747 .output("output.mp4")
748 .stream_copy(StreamCopyMode::CopyVideo);
749
750 assert_eq!(
751 transcoder.config.stream_copy,
752 Some(StreamCopyMode::CopyVideo)
753 );
754 }
755
756 #[test]
757 fn test_audio_channel_layout_on_transcoder() {
758 let transcoder =
759 Transcoder::new().audio_channel_layout(audio_channel_map::AudioLayout::FivePointOne);
760
761 assert_eq!(
762 transcoder.config.audio_channel_layout,
763 Some(audio_channel_map::AudioLayout::FivePointOne)
764 );
765 }
766
767 #[test]
768 fn test_preset_with_audio_channel_layout() {
769 let preset = PresetConfig {
770 audio_codec: Some("opus".to_string()),
771 audio_bitrate: Some(384_000),
772 audio_channel_layout: Some(audio_channel_map::AudioLayout::FivePointOne),
773 ..PresetConfig::default()
774 };
775
776 let transcoder = Transcoder::new().preset(preset);
777 assert_eq!(
778 transcoder.config.audio_channel_layout,
779 Some(audio_channel_map::AudioLayout::FivePointOne)
780 );
781 assert_eq!(transcoder.config.audio_bitrate, Some(384_000));
782 }
783
784 #[test]
785 fn test_preset_config_default_has_no_channel_layout() {
786 let preset = PresetConfig::default();
787 assert!(preset.audio_channel_layout.is_none());
788 }
789
790 #[test]
791 fn test_config_default_has_no_stream_copy() {
792 let config = TranscodeConfig::default();
793 assert!(config.stream_copy.is_none());
794 assert!(config.audio_channel_layout.is_none());
795 }
796
797 #[test]
798 fn test_subtitle_modes() {
799 assert_eq!(SubtitleMode::Ignore, SubtitleMode::Ignore);
800 assert_ne!(SubtitleMode::Ignore, SubtitleMode::Copy);
801 assert_ne!(SubtitleMode::Copy, SubtitleMode::BurnIn);
802 }
803
804 #[test]
805 fn test_chapter_modes() {
806 assert_eq!(ChapterMode::Ignore, ChapterMode::Ignore);
807 assert_ne!(ChapterMode::Ignore, ChapterMode::Copy);
808 assert_ne!(ChapterMode::Copy, ChapterMode::Custom);
809 }
810}