Skip to main content

oximedia_transcode/
lib.rs

1//! High-level transcoding pipeline for `OxiMedia`.
2//!
3//! This crate provides a comprehensive, professional-grade transcoding system with:
4//!
5//! # Features
6//!
7//! ## High-Level API
8//!
9//! - **Simple One-Liner Transcoding** - Quick transcoding with sensible defaults
10//! - **Preset Library** - Industry-standard presets for major platforms
11//! - **Fluent Builder API** - Complex workflows with readable code
12//!
13//! ## Professional Features
14//!
15//! - **Multi-Pass Encoding** - 2-pass and 3-pass encoding for optimal quality
16//! - **ABR Ladder Generation** - Adaptive bitrate encoding for HLS/DASH
17//! - **Parallel Encoding** - Encode multiple outputs simultaneously
18//! - **Hardware Acceleration** - Auto-detection and use of GPU encoders
19//! - **Progress Tracking** - Real-time progress with ETA estimation
20//! - **Audio Normalization** - Automatic loudness normalization (EBU R128/ATSC A/85)
21//! - **Quality Control** - CRF, CBR, VBR, and constrained VBR modes
22//! - **Subtitle Support** - Burn-in or soft subtitle embedding
23//! - **Chapter Markers** - Preserve or add chapter information
24//! - **Metadata Preservation** - Copy or map metadata fields
25//!
26//! ## Job Management
27//!
28//! - **Job Queuing** - Queue multiple transcode jobs
29//! - **Priority Scheduling** - High, normal, and low priority jobs
30//! - **Resource Management** - CPU/GPU limits and throttling
31//! - **Error Recovery** - Automatic retry logic with exponential backoff
32//! - **Validation** - Input/output validation before processing
33//!
34//! # Supported Platforms
35//!
36//! ## Streaming Platforms
37//!
38//! - **`YouTube`** - 1080p60, 4K, VP9/H.264 variants
39//! - **Vimeo** - Professional quality presets
40//! - **Twitch** - Live streaming optimized
41//! - **Social Media** - Instagram, `TikTok`, Twitter optimized
42//!
43//! ## Broadcast
44//!
45//! - **`ProRes` Proxy** - High-quality editing proxies
46//! - **`DNxHD` Proxy** - Avid editing proxies
47//! - **Broadcast HD/4K** - Broadcast-ready deliverables
48//!
49//! ## Streaming Protocols
50//!
51//! - **HLS** - HTTP Live Streaming ABR ladders
52//! - **DASH** - MPEG-DASH ABR ladders
53//! - **CMAF** - Common Media Application Format
54//!
55//! ## Archive
56//!
57//! - **Lossless** - FFV1 lossless preservation
58//! - **High Quality** - VP9/AV1 archival encoding
59//!
60//! # Quick Start
61//!
62//! ## Simple Transcoding
63//!
64//! ```rust,no_run
65//! use oximedia_transcode::{Transcoder, presets};
66//!
67//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
68//! // Simple transcode to YouTube 1080p
69//! Transcoder::new()
70//!     .input("input.mp4")
71//!     .output("output.mp4")
72//!     .preset(presets::youtube::youtube_1080p())
73//!     .transcode()
74//!     .await?;
75//! # Ok(())
76//! # }
77//! ```
78//!
79//! ## Complex Pipeline
80//!
81//! ```rust,ignore
82//! use oximedia_transcode::{TranscodePipeline, Quality};
83//! use oximedia_transcode::presets::streaming;
84//!
85//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
86//! // Create HLS ABR ladder with multiple qualities
87//! TranscodePipeline::builder()
88//!     .input("source.mp4")
89//!     .abr_ladder(streaming::hls_ladder())
90//!     .audio_normalize(true)
91//!     .quality(Quality::High)
92//!     .parallel_encode(true)
93//!     .progress(|p| {
94//!         println!("Progress: {}% - ETA: {:?}", p.percent, p.eta);
95//!     })
96//!     .execute()
97//!     .await?;
98//! # Ok(())
99//! # }
100//! ```
101//!
102//! ## Multi-Pass Encoding
103//!
104//! ```rust,no_run
105//! use oximedia_transcode::{Transcoder, MultiPassMode};
106//!
107//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
108//! // 2-pass encoding for optimal quality
109//! Transcoder::new()
110//!     .input("input.mp4")
111//!     .output("output.webm")
112//!     .multi_pass(MultiPassMode::TwoPass)
113//!     .target_bitrate(5_000_000) // 5 Mbps
114//!     .transcode()
115//!     .await?;
116//! # Ok(())
117//! # }
118//! ```
119
120#![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;
163/// Concatenation and joining of multiple media sources.
164pub 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;
172/// Rate-distortion analysis for optimal encoding parameter selection.
173pub 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;
182/// Watermark and graphic overlay embedding during transcoding.
183pub 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/// Errors that can occur during transcoding operations.
242#[derive(Debug, Clone, Error)]
243pub enum TranscodeError {
244    /// Invalid input file or format.
245    #[error("Invalid input: {0}")]
246    InvalidInput(String),
247
248    /// Invalid output configuration.
249    #[error("Invalid output: {0}")]
250    InvalidOutput(String),
251
252    /// Codec error during encoding/decoding.
253    #[error("Codec error: {0}")]
254    CodecError(String),
255
256    /// Container format error.
257    #[error("Container error: {0}")]
258    ContainerError(String),
259
260    /// I/O error during transcoding.
261    #[error("I/O error: {0}")]
262    IoError(String),
263
264    /// Pipeline execution error.
265    #[error("Pipeline error: {0}")]
266    PipelineError(String),
267
268    /// Multi-pass encoding error.
269    #[error("Multi-pass error: {0}")]
270    MultiPassError(String),
271
272    /// Audio normalization error.
273    #[error("Normalization error: {0}")]
274    NormalizationError(String),
275
276    /// Validation error.
277    #[error("Validation error: {0}")]
278    ValidationError(#[from] ValidationError),
279
280    /// Job execution error.
281    #[error("Job error: {0}")]
282    JobError(String),
283
284    /// Unsupported operation or feature.
285    #[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
295/// Result type for transcoding operations.
296pub type Result<T> = std::result::Result<T, TranscodeError>;
297
298/// Main transcoding interface with simple API.
299///
300/// # Example
301///
302/// ```rust,no_run
303/// use oximedia_transcode::Transcoder;
304///
305/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
306/// Transcoder::new()
307///     .input("input.mp4")
308///     .output("output.webm")
309///     .video_codec("vp9")
310///     .audio_codec("opus")
311///     .transcode()
312///     .await?;
313/// # Ok(())
314/// # }
315/// ```
316pub struct Transcoder {
317    config: TranscodeConfig,
318}
319
320/// Transcoding configuration.
321#[derive(Debug, Clone)]
322pub struct TranscodeConfig {
323    /// Input file path.
324    pub input: Option<String>,
325    /// Output file path.
326    pub output: Option<String>,
327    /// Video codec name.
328    pub video_codec: Option<String>,
329    /// Audio codec name.
330    pub audio_codec: Option<String>,
331    /// Target video bitrate in bits per second.
332    pub video_bitrate: Option<u64>,
333    /// Target audio bitrate in bits per second.
334    pub audio_bitrate: Option<u64>,
335    /// Video width in pixels.
336    pub width: Option<u32>,
337    /// Video height in pixels.
338    pub height: Option<u32>,
339    /// Frame rate as a rational number (numerator, denominator).
340    pub frame_rate: Option<(u32, u32)>,
341    /// Multi-pass encoding mode.
342    pub multi_pass: Option<MultiPassMode>,
343    /// Quality mode for encoding.
344    pub quality_mode: Option<QualityMode>,
345    /// Enable audio normalization.
346    pub normalize_audio: bool,
347    /// Loudness normalization standard.
348    pub loudness_standard: Option<LoudnessStandard>,
349    /// Enable hardware acceleration.
350    pub hw_accel: bool,
351    /// Preserve metadata from input.
352    pub preserve_metadata: bool,
353    /// Subtitle handling mode.
354    pub subtitle_mode: Option<SubtitleMode>,
355    /// Chapter handling mode.
356    pub chapter_mode: Option<ChapterMode>,
357    /// Stream copy mode for passthrough without re-encoding.
358    pub stream_copy: Option<StreamCopyMode>,
359    /// Audio channel layout for output.
360    pub audio_channel_layout: Option<audio_channel_map::AudioLayout>,
361}
362
363/// Subtitle handling modes.
364#[derive(Debug, Clone, Copy, PartialEq, Eq)]
365pub enum SubtitleMode {
366    /// Ignore subtitles.
367    Ignore,
368    /// Copy subtitles as separate stream.
369    Copy,
370    /// Burn subtitles into video.
371    BurnIn,
372}
373
374/// Chapter handling modes.
375#[derive(Debug, Clone, Copy, PartialEq, Eq)]
376pub enum ChapterMode {
377    /// Ignore chapters.
378    Ignore,
379    /// Copy chapters from input.
380    Copy,
381    /// Add custom chapters.
382    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    /// Get a reference to the transcoder configuration.
413    #[must_use]
414    pub fn config(&self) -> &TranscodeConfig {
415        &self.config
416    }
417
418    /// Creates a new transcoder with default configuration.
419    #[must_use]
420    pub fn new() -> Self {
421        Self {
422            config: TranscodeConfig::default(),
423        }
424    }
425
426    /// Sets the input file path.
427    #[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    /// Sets the output file path.
434    #[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    /// Sets the video codec.
441    #[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    /// Sets the audio codec.
448    #[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    /// Sets the target video bitrate.
455    #[must_use]
456    pub fn video_bitrate(mut self, bitrate: u64) -> Self {
457        self.config.video_bitrate = Some(bitrate);
458        self
459    }
460
461    /// Sets the target audio bitrate.
462    #[must_use]
463    pub fn audio_bitrate(mut self, bitrate: u64) -> Self {
464        self.config.audio_bitrate = Some(bitrate);
465        self
466    }
467
468    /// Sets the output resolution.
469    #[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    /// Sets the output frame rate.
477    #[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    /// Sets the multi-pass encoding mode.
484    #[must_use]
485    pub fn multi_pass(mut self, mode: MultiPassMode) -> Self {
486        self.config.multi_pass = Some(mode);
487        self
488    }
489
490    /// Sets the quality mode.
491    #[must_use]
492    pub fn quality(mut self, mode: QualityMode) -> Self {
493        self.config.quality_mode = Some(mode);
494        self
495    }
496
497    /// Sets the target bitrate (convenience method for video bitrate).
498    #[must_use]
499    pub fn target_bitrate(mut self, bitrate: u64) -> Self {
500        self.config.video_bitrate = Some(bitrate);
501        self
502    }
503
504    /// Enables or disables audio normalization.
505    #[must_use]
506    pub fn normalize_audio(mut self, enable: bool) -> Self {
507        self.config.normalize_audio = enable;
508        self
509    }
510
511    /// Sets the loudness normalization standard.
512    #[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    /// Enables or disables hardware acceleration.
520    #[must_use]
521    pub fn hw_accel(mut self, enable: bool) -> Self {
522        self.config.hw_accel = enable;
523        self
524    }
525
526    /// Sets the stream copy mode for passthrough without re-encoding.
527    ///
528    /// When codecs match between input and output, stream copy avoids
529    /// re-encoding and preserves the original quality.
530    #[must_use]
531    pub fn stream_copy(mut self, mode: StreamCopyMode) -> Self {
532        self.config.stream_copy = Some(mode);
533        self
534    }
535
536    /// Sets the audio channel layout for the output.
537    #[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    /// Applies a preset configuration.
544    #[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    /// Executes the transcode operation.
577    ///
578    /// # Errors
579    ///
580    /// Returns an error if:
581    /// - Input or output path is not set
582    /// - Input file is invalid or cannot be opened
583    /// - Output configuration is invalid
584    /// - Transcoding fails
585    /// - On wasm32 targets (filesystem-based transcoding is not supported)
586    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            // Validate configuration
598            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            // Create a basic pipeline and execute
606            let mut pipeline = TranscodePipeline::builder()
607                .input(&input)
608                .output(&output)
609                .build()?;
610
611            // Apply configuration to pipeline
612            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            // Execute pipeline
620            pipeline.execute().await
621        }
622    }
623}
624
625impl Default for Transcoder {
626    fn default() -> Self {
627        Self::new()
628    }
629}
630
631/// Preset configuration for common transcoding scenarios.
632#[derive(Debug, Clone, Default)]
633pub struct PresetConfig {
634    /// Video codec name.
635    pub video_codec: Option<String>,
636    /// Audio codec name.
637    pub audio_codec: Option<String>,
638    /// Video bitrate.
639    pub video_bitrate: Option<u64>,
640    /// Audio bitrate.
641    pub audio_bitrate: Option<u64>,
642    /// Video width.
643    pub width: Option<u32>,
644    /// Video height.
645    pub height: Option<u32>,
646    /// Frame rate.
647    pub frame_rate: Option<(u32, u32)>,
648    /// Quality mode.
649    pub quality_mode: Option<QualityMode>,
650    /// Container format.
651    pub container: Option<String>,
652    /// Audio channel layout (mono, stereo, 5.1, 7.1).
653    pub audio_channel_layout: Option<audio_channel_map::AudioLayout>,
654}
655
656/// Output from a successful transcode operation.
657#[derive(Debug, Clone)]
658pub struct TranscodeOutput {
659    /// Output file path.
660    pub output_path: String,
661    /// File size in bytes.
662    pub file_size: u64,
663    /// Duration in seconds.
664    pub duration: f64,
665    /// Video bitrate in bits per second.
666    pub video_bitrate: u64,
667    /// Audio bitrate in bits per second.
668    pub audio_bitrate: u64,
669    /// Actual encoding time in seconds.
670    pub encoding_time: f64,
671    /// Speed factor (input duration / encoding time).
672    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}