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_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;
166/// Concatenation and joining of multiple media sources.
167pub 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;
175/// Rate-distortion analysis for optimal encoding parameter selection.
176pub 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;
186/// Watermark and graphic overlay embedding during transcoding.
187pub 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/// Errors that can occur during transcoding operations.
250#[derive(Debug, Clone, Error)]
251pub enum TranscodeError {
252    /// Invalid input file or format.
253    #[error("Invalid input: {0}")]
254    InvalidInput(String),
255
256    /// Invalid output configuration.
257    #[error("Invalid output: {0}")]
258    InvalidOutput(String),
259
260    /// Codec error during encoding/decoding.
261    #[error("Codec error: {0}")]
262    CodecError(String),
263
264    /// Container format error.
265    #[error("Container error: {0}")]
266    ContainerError(String),
267
268    /// I/O error during transcoding.
269    #[error("I/O error: {0}")]
270    IoError(String),
271
272    /// Pipeline execution error.
273    #[error("Pipeline error: {0}")]
274    PipelineError(String),
275
276    /// Multi-pass encoding error.
277    #[error("Multi-pass error: {0}")]
278    MultiPassError(String),
279
280    /// Audio normalization error.
281    #[error("Normalization error: {0}")]
282    NormalizationError(String),
283
284    /// Validation error.
285    #[error("Validation error: {0}")]
286    ValidationError(#[from] ValidationError),
287
288    /// Job execution error.
289    #[error("Job error: {0}")]
290    JobError(String),
291
292    /// Unsupported operation or feature.
293    #[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
303/// Result type for transcoding operations.
304pub type Result<T> = std::result::Result<T, TranscodeError>;
305
306/// Main transcoding interface with simple API.
307///
308/// # Example
309///
310/// ```rust,no_run
311/// use oximedia_transcode::Transcoder;
312///
313/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
314/// Transcoder::new()
315///     .input("input.mp4")
316///     .output("output.webm")
317///     .video_codec("vp9")
318///     .audio_codec("opus")
319///     .transcode()
320///     .await?;
321/// # Ok(())
322/// # }
323/// ```
324pub struct Transcoder {
325    config: TranscodeConfig,
326}
327
328/// Transcoding configuration.
329#[derive(Debug, Clone)]
330pub struct TranscodeConfig {
331    /// Input file path.
332    pub input: Option<String>,
333    /// Output file path.
334    pub output: Option<String>,
335    /// Video codec name.
336    pub video_codec: Option<String>,
337    /// Audio codec name.
338    pub audio_codec: Option<String>,
339    /// Target video bitrate in bits per second.
340    pub video_bitrate: Option<u64>,
341    /// Target audio bitrate in bits per second.
342    pub audio_bitrate: Option<u64>,
343    /// Video width in pixels.
344    pub width: Option<u32>,
345    /// Video height in pixels.
346    pub height: Option<u32>,
347    /// Frame rate as a rational number (numerator, denominator).
348    pub frame_rate: Option<(u32, u32)>,
349    /// Multi-pass encoding mode.
350    pub multi_pass: Option<MultiPassMode>,
351    /// Quality mode for encoding.
352    pub quality_mode: Option<QualityMode>,
353    /// Enable audio normalization.
354    pub normalize_audio: bool,
355    /// Loudness normalization standard.
356    pub loudness_standard: Option<LoudnessStandard>,
357    /// Enable hardware acceleration.
358    pub hw_accel: bool,
359    /// Preserve metadata from input.
360    pub preserve_metadata: bool,
361    /// Subtitle handling mode.
362    pub subtitle_mode: Option<SubtitleMode>,
363    /// Chapter handling mode.
364    pub chapter_mode: Option<ChapterMode>,
365    /// Stream copy mode for passthrough without re-encoding.
366    pub stream_copy: Option<StreamCopyMode>,
367    /// Audio channel layout for output.
368    pub audio_channel_layout: Option<audio_channel_map::AudioLayout>,
369}
370
371/// Subtitle handling modes.
372#[derive(Debug, Clone, Copy, PartialEq, Eq)]
373pub enum SubtitleMode {
374    /// Ignore subtitles.
375    Ignore,
376    /// Copy subtitles as separate stream.
377    Copy,
378    /// Burn subtitles into video.
379    BurnIn,
380}
381
382/// Chapter handling modes.
383#[derive(Debug, Clone, Copy, PartialEq, Eq)]
384pub enum ChapterMode {
385    /// Ignore chapters.
386    Ignore,
387    /// Copy chapters from input.
388    Copy,
389    /// Add custom chapters.
390    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    /// Get a reference to the transcoder configuration.
421    #[must_use]
422    pub fn config(&self) -> &TranscodeConfig {
423        &self.config
424    }
425
426    /// Creates a new transcoder with default configuration.
427    #[must_use]
428    pub fn new() -> Self {
429        Self {
430            config: TranscodeConfig::default(),
431        }
432    }
433
434    /// Sets the input file path.
435    #[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    /// Sets the output file path.
442    #[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    /// Sets the video codec.
449    #[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    /// Sets the audio codec.
456    #[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    /// Sets the target video bitrate.
463    #[must_use]
464    pub fn video_bitrate(mut self, bitrate: u64) -> Self {
465        self.config.video_bitrate = Some(bitrate);
466        self
467    }
468
469    /// Sets the target audio bitrate.
470    #[must_use]
471    pub fn audio_bitrate(mut self, bitrate: u64) -> Self {
472        self.config.audio_bitrate = Some(bitrate);
473        self
474    }
475
476    /// Sets the output resolution.
477    #[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    /// Sets the output frame rate.
485    #[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    /// Sets the multi-pass encoding mode.
492    #[must_use]
493    pub fn multi_pass(mut self, mode: MultiPassMode) -> Self {
494        self.config.multi_pass = Some(mode);
495        self
496    }
497
498    /// Sets the quality mode.
499    #[must_use]
500    pub fn quality(mut self, mode: QualityMode) -> Self {
501        self.config.quality_mode = Some(mode);
502        self
503    }
504
505    /// Sets the target bitrate (convenience method for video bitrate).
506    #[must_use]
507    pub fn target_bitrate(mut self, bitrate: u64) -> Self {
508        self.config.video_bitrate = Some(bitrate);
509        self
510    }
511
512    /// Enables or disables audio normalization.
513    #[must_use]
514    pub fn normalize_audio(mut self, enable: bool) -> Self {
515        self.config.normalize_audio = enable;
516        self
517    }
518
519    /// Sets the loudness normalization standard.
520    #[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    /// Enables or disables hardware acceleration.
528    #[must_use]
529    pub fn hw_accel(mut self, enable: bool) -> Self {
530        self.config.hw_accel = enable;
531        self
532    }
533
534    /// Sets the stream copy mode for passthrough without re-encoding.
535    ///
536    /// When codecs match between input and output, stream copy avoids
537    /// re-encoding and preserves the original quality.
538    #[must_use]
539    pub fn stream_copy(mut self, mode: StreamCopyMode) -> Self {
540        self.config.stream_copy = Some(mode);
541        self
542    }
543
544    /// Sets the audio channel layout for the output.
545    #[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    /// Applies a preset configuration.
552    #[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    /// Executes the transcode operation.
585    ///
586    /// # Errors
587    ///
588    /// Returns an error if:
589    /// - Input or output path is not set
590    /// - Input file is invalid or cannot be opened
591    /// - Output configuration is invalid
592    /// - Transcoding fails
593    /// - On wasm32 targets (filesystem-based transcoding is not supported)
594    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            // Validate configuration
606            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            // Create a basic pipeline and execute
614            let mut pipeline = TranscodePipeline::builder()
615                .input(&input)
616                .output(&output)
617                .build()?;
618
619            // Apply configuration to pipeline
620            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            // Execute pipeline
628            pipeline.execute().await
629        }
630    }
631}
632
633impl Default for Transcoder {
634    fn default() -> Self {
635        Self::new()
636    }
637}
638
639/// Preset configuration for common transcoding scenarios.
640#[derive(Debug, Clone, Default)]
641pub struct PresetConfig {
642    /// Video codec name.
643    pub video_codec: Option<String>,
644    /// Audio codec name.
645    pub audio_codec: Option<String>,
646    /// Video bitrate.
647    pub video_bitrate: Option<u64>,
648    /// Audio bitrate.
649    pub audio_bitrate: Option<u64>,
650    /// Video width.
651    pub width: Option<u32>,
652    /// Video height.
653    pub height: Option<u32>,
654    /// Frame rate.
655    pub frame_rate: Option<(u32, u32)>,
656    /// Quality mode.
657    pub quality_mode: Option<QualityMode>,
658    /// Container format.
659    pub container: Option<String>,
660    /// Audio channel layout (mono, stereo, 5.1, 7.1).
661    pub audio_channel_layout: Option<audio_channel_map::AudioLayout>,
662}
663
664/// Output from a successful transcode operation.
665#[derive(Debug, Clone)]
666pub struct TranscodeOutput {
667    /// Output file path.
668    pub output_path: String,
669    /// File size in bytes.
670    pub file_size: u64,
671    /// Duration in seconds.
672    pub duration: f64,
673    /// Video bitrate in bits per second.
674    pub video_bitrate: u64,
675    /// Audio bitrate in bits per second.
676    pub audio_bitrate: u64,
677    /// Actual encoding time in seconds.
678    pub encoding_time: f64,
679    /// Speed factor (input duration / encoding time).
680    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}