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