#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::too_many_arguments)]
mod abr;
pub mod adaptive_bitrate;
pub mod audio_transcode;
pub mod bitrate_estimator;
mod builder;
mod codec_config;
pub mod codec_mapping;
pub mod crf_optimizer;
mod filters;
#[cfg(not(target_arch = "wasm32"))]
pub mod frame_pipeline;
mod hw_accel;
mod multipass;
mod normalization;
mod parallel;
#[cfg(not(target_arch = "wasm32"))]
mod pipeline;
#[cfg(not(target_arch = "wasm32"))]
pub mod pipeline_context;
mod progress;
mod quality;
pub mod segment_encoder;
pub mod segment_transcoder;
pub mod thumbnail;
mod transcode_job;
pub mod two_pass;
pub mod validation;
pub mod ab_compare;
pub mod abr_ladder;
pub mod audio_channel_map;
pub mod audio_only;
pub mod benchmark;
pub mod bitrate_control;
pub mod burn_subs;
pub mod codec_profile;
pub mod concat_transcode;
pub mod crop_scale;
pub mod encoding_log;
#[cfg(not(target_arch = "wasm32"))]
pub mod examples;
pub mod frame_stats;
pub mod frame_trim;
pub mod hdr_passthrough;
pub mod hwaccel;
pub mod output_verify;
pub mod per_scene_encode;
pub mod presets;
pub mod quality_ladder_gen;
pub mod rate_distortion;
pub mod resolution_select;
pub mod scene_cut;
pub mod stage_graph;
pub mod stream_copy;
pub mod transcode_metrics;
pub mod transcode_preset;
pub mod transcode_profile;
pub mod transcode_session;
pub mod utils;
pub mod watch_folder;
pub mod watermark_overlay;
pub use codec_config::{
codec_config_from_quality, Av1Config, Av1Usage, CodecConfig, Ffv1Coder, Ffv1Config, Ffv1Level,
FlacConfig, H264Config, H264Profile, JxlConfig, JxlEffort, OpusApplication, OpusConfig,
Vp9Config,
};
pub use codec_profile::CodecTunePreset;
pub use filters::{AudioFilter, FilterNode, VideoFilter};
pub use hw_accel::{
detect_available_hw_accel, detect_best_hw_accel_for_codec, get_available_encoders,
HwAccelConfig, HwAccelType, HwEncoder, HwFeature,
};
pub use stream_copy::{
CopyDecision, StreamCopyConfig, StreamCopyDetector, StreamCopyMode, StreamInfo, StreamType,
};
pub use abr::{AbrLadder, AbrLadderBuilder, AbrRung, AbrStrategy};
pub use builder::TranscodeBuilder;
pub use concat_transcode::{
AnnotatedSegment, ConcatPlan, ConcatStep, MixedSourceConcatenator, SourceProperties,
};
#[cfg(not(target_arch = "wasm32"))]
pub use frame_pipeline::{
wire_hdr_into_pipeline, AudioFrameOp, FramePipelineConfig, FramePipelineExecutor,
FramePipelineResult, VideoFrameOp,
};
pub use multipass::{MultiPassConfig, MultiPassEncoder, MultiPassMode};
pub use normalization::{AudioNormalizer, LoudnessStandard, LoudnessTarget, NormalizationConfig};
pub use parallel::{
assemble_av1_tile_bitstream, Av1TileConfig, Av1TileParallelEncoder, Av1TileStats,
ParallelConfig, ParallelEncodeBuilder, ParallelEncoder,
};
#[cfg(not(target_arch = "wasm32"))]
pub use pipeline::{Pipeline, PipelineStage, TranscodePipeline};
#[cfg(not(target_arch = "wasm32"))]
pub use pipeline_context::{
FilterGraph, Frame, FrameDecoder, FrameEncoder, HdrPassthroughConfig, HdrSeiInjector,
PassStats, TranscodeContext, TranscodeStats,
};
pub use progress::{ProgressCallback, ProgressInfo, ProgressTracker};
pub use quality::{QualityConfig, QualityMode, QualityPreset, RateControlMode, TuneMode};
pub use segment_encoder::{
ParallelSegmentEncoder, ParallelSegmentResult, ParallelSegmentStats, SegmentSpec,
};
pub use thumbnail::{format_vtt_time, SpriteSheet, SpriteSheetConfig};
pub use transcode_job::{JobPriority, JobQueue, TranscodeJob, TranscodeJobConfig, TranscodeStatus};
pub use transcode_preset::{TranscodeEstimator, TranscodePreset};
pub use validation::{InputValidator, OutputValidator, ValidationError};
use thiserror::Error;
#[derive(Debug, Clone, Error)]
pub enum TranscodeError {
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Invalid output: {0}")]
InvalidOutput(String),
#[error("Codec error: {0}")]
CodecError(String),
#[error("Container error: {0}")]
ContainerError(String),
#[error("I/O error: {0}")]
IoError(String),
#[error("Pipeline error: {0}")]
PipelineError(String),
#[error("Multi-pass error: {0}")]
MultiPassError(String),
#[error("Normalization error: {0}")]
NormalizationError(String),
#[error("Validation error: {0}")]
ValidationError(#[from] ValidationError),
#[error("Job error: {0}")]
JobError(String),
#[error("Unsupported: {0}")]
Unsupported(String),
}
impl From<std::io::Error> for TranscodeError {
fn from(err: std::io::Error) -> Self {
TranscodeError::IoError(err.to_string())
}
}
pub type Result<T> = std::result::Result<T, TranscodeError>;
pub struct Transcoder {
config: TranscodeConfig,
}
#[derive(Debug, Clone)]
pub struct TranscodeConfig {
pub input: Option<String>,
pub output: Option<String>,
pub video_codec: Option<String>,
pub audio_codec: Option<String>,
pub video_bitrate: Option<u64>,
pub audio_bitrate: Option<u64>,
pub width: Option<u32>,
pub height: Option<u32>,
pub frame_rate: Option<(u32, u32)>,
pub multi_pass: Option<MultiPassMode>,
pub quality_mode: Option<QualityMode>,
pub normalize_audio: bool,
pub loudness_standard: Option<LoudnessStandard>,
pub hw_accel: bool,
pub preserve_metadata: bool,
pub subtitle_mode: Option<SubtitleMode>,
pub chapter_mode: Option<ChapterMode>,
pub stream_copy: Option<StreamCopyMode>,
pub audio_channel_layout: Option<audio_channel_map::AudioLayout>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubtitleMode {
Ignore,
Copy,
BurnIn,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChapterMode {
Ignore,
Copy,
Custom,
}
impl Default for TranscodeConfig {
fn default() -> Self {
Self {
input: None,
output: None,
video_codec: None,
audio_codec: None,
video_bitrate: None,
audio_bitrate: None,
width: None,
height: None,
frame_rate: None,
multi_pass: None,
quality_mode: None,
normalize_audio: false,
loudness_standard: None,
hw_accel: true,
preserve_metadata: true,
subtitle_mode: None,
chapter_mode: None,
stream_copy: None,
audio_channel_layout: None,
}
}
}
impl Transcoder {
#[must_use]
pub fn config(&self) -> &TranscodeConfig {
&self.config
}
#[must_use]
pub fn new() -> Self {
Self {
config: TranscodeConfig::default(),
}
}
#[must_use]
pub fn input(mut self, path: impl Into<String>) -> Self {
self.config.input = Some(path.into());
self
}
#[must_use]
pub fn output(mut self, path: impl Into<String>) -> Self {
self.config.output = Some(path.into());
self
}
#[must_use]
pub fn video_codec(mut self, codec: impl Into<String>) -> Self {
self.config.video_codec = Some(codec.into());
self
}
#[must_use]
pub fn audio_codec(mut self, codec: impl Into<String>) -> Self {
self.config.audio_codec = Some(codec.into());
self
}
#[must_use]
pub fn video_bitrate(mut self, bitrate: u64) -> Self {
self.config.video_bitrate = Some(bitrate);
self
}
#[must_use]
pub fn audio_bitrate(mut self, bitrate: u64) -> Self {
self.config.audio_bitrate = Some(bitrate);
self
}
#[must_use]
pub fn resolution(mut self, width: u32, height: u32) -> Self {
self.config.width = Some(width);
self.config.height = Some(height);
self
}
#[must_use]
pub fn frame_rate(mut self, num: u32, den: u32) -> Self {
self.config.frame_rate = Some((num, den));
self
}
#[must_use]
pub fn multi_pass(mut self, mode: MultiPassMode) -> Self {
self.config.multi_pass = Some(mode);
self
}
#[must_use]
pub fn quality(mut self, mode: QualityMode) -> Self {
self.config.quality_mode = Some(mode);
self
}
#[must_use]
pub fn target_bitrate(mut self, bitrate: u64) -> Self {
self.config.video_bitrate = Some(bitrate);
self
}
#[must_use]
pub fn normalize_audio(mut self, enable: bool) -> Self {
self.config.normalize_audio = enable;
self
}
#[must_use]
pub fn loudness_standard(mut self, standard: LoudnessStandard) -> Self {
self.config.loudness_standard = Some(standard);
self.config.normalize_audio = true;
self
}
#[must_use]
pub fn hw_accel(mut self, enable: bool) -> Self {
self.config.hw_accel = enable;
self
}
#[must_use]
pub fn stream_copy(mut self, mode: StreamCopyMode) -> Self {
self.config.stream_copy = Some(mode);
self
}
#[must_use]
pub fn audio_channel_layout(mut self, layout: audio_channel_map::AudioLayout) -> Self {
self.config.audio_channel_layout = Some(layout);
self
}
#[must_use]
pub fn preset(mut self, preset: PresetConfig) -> Self {
if let Some(codec) = preset.video_codec {
self.config.video_codec = Some(codec);
}
if let Some(codec) = preset.audio_codec {
self.config.audio_codec = Some(codec);
}
if let Some(bitrate) = preset.video_bitrate {
self.config.video_bitrate = Some(bitrate);
}
if let Some(bitrate) = preset.audio_bitrate {
self.config.audio_bitrate = Some(bitrate);
}
if let Some(width) = preset.width {
self.config.width = Some(width);
}
if let Some(height) = preset.height {
self.config.height = Some(height);
}
if let Some(fps) = preset.frame_rate {
self.config.frame_rate = Some(fps);
}
if let Some(mode) = preset.quality_mode {
self.config.quality_mode = Some(mode);
}
if let Some(layout) = preset.audio_channel_layout {
self.config.audio_channel_layout = Some(layout);
}
self
}
pub async fn transcode(self) -> Result<TranscodeOutput> {
#[cfg(target_arch = "wasm32")]
{
let _ = self;
return Err(TranscodeError::Unsupported(
"Filesystem-based transcoding is not supported on wasm32".to_string(),
));
}
#[cfg(not(target_arch = "wasm32"))]
{
let input = self.config.input.ok_or_else(|| {
TranscodeError::InvalidInput("No input file specified".to_string())
})?;
let output = self.config.output.ok_or_else(|| {
TranscodeError::InvalidOutput("No output file specified".to_string())
})?;
let mut pipeline = TranscodePipeline::builder()
.input(&input)
.output(&output)
.build()?;
if let Some(codec) = &self.config.video_codec {
pipeline.set_video_codec(codec);
}
if let Some(codec) = &self.config.audio_codec {
pipeline.set_audio_codec(codec);
}
pipeline.execute().await
}
}
}
impl Default for Transcoder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct PresetConfig {
pub video_codec: Option<String>,
pub audio_codec: Option<String>,
pub video_bitrate: Option<u64>,
pub audio_bitrate: Option<u64>,
pub width: Option<u32>,
pub height: Option<u32>,
pub frame_rate: Option<(u32, u32)>,
pub quality_mode: Option<QualityMode>,
pub container: Option<String>,
pub audio_channel_layout: Option<audio_channel_map::AudioLayout>,
}
#[derive(Debug, Clone)]
pub struct TranscodeOutput {
pub output_path: String,
pub file_size: u64,
pub duration: f64,
pub video_bitrate: u64,
pub audio_bitrate: u64,
pub encoding_time: f64,
pub speed_factor: f64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transcoder_builder() {
let transcoder = Transcoder::new()
.input("input.mp4")
.output("output.webm")
.video_codec("vp9")
.audio_codec("opus")
.resolution(1920, 1080)
.frame_rate(30, 1);
assert_eq!(transcoder.config.input, Some("input.mp4".to_string()));
assert_eq!(transcoder.config.output, Some("output.webm".to_string()));
assert_eq!(transcoder.config.video_codec, Some("vp9".to_string()));
assert_eq!(transcoder.config.audio_codec, Some("opus".to_string()));
assert_eq!(transcoder.config.width, Some(1920));
assert_eq!(transcoder.config.height, Some(1080));
assert_eq!(transcoder.config.frame_rate, Some((30, 1)));
}
#[test]
fn test_default_config() {
let config = TranscodeConfig::default();
assert!(config.input.is_none());
assert!(config.output.is_none());
assert!(config.hw_accel);
assert!(config.preserve_metadata);
assert!(!config.normalize_audio);
}
#[test]
fn test_preset_application() {
let preset = PresetConfig {
video_codec: Some("vp9".to_string()),
audio_codec: Some("opus".to_string()),
video_bitrate: Some(5_000_000),
audio_bitrate: Some(128_000),
width: Some(1920),
height: Some(1080),
frame_rate: Some((60, 1)),
quality_mode: Some(QualityMode::High),
container: Some("webm".to_string()),
audio_channel_layout: None,
};
let transcoder = Transcoder::new().preset(preset);
assert_eq!(transcoder.config.video_codec, Some("vp9".to_string()));
assert_eq!(transcoder.config.audio_codec, Some("opus".to_string()));
assert_eq!(transcoder.config.video_bitrate, Some(5_000_000));
assert_eq!(transcoder.config.audio_bitrate, Some(128_000));
assert_eq!(transcoder.config.width, Some(1920));
assert_eq!(transcoder.config.height, Some(1080));
assert_eq!(transcoder.config.frame_rate, Some((60, 1)));
assert_eq!(transcoder.config.quality_mode, Some(QualityMode::High));
}
#[test]
fn test_stream_copy_mode() {
let transcoder = Transcoder::new()
.input("input.mp4")
.output("output.mp4")
.stream_copy(StreamCopyMode::CopyVideo);
assert_eq!(
transcoder.config.stream_copy,
Some(StreamCopyMode::CopyVideo)
);
}
#[test]
fn test_audio_channel_layout_on_transcoder() {
let transcoder =
Transcoder::new().audio_channel_layout(audio_channel_map::AudioLayout::FivePointOne);
assert_eq!(
transcoder.config.audio_channel_layout,
Some(audio_channel_map::AudioLayout::FivePointOne)
);
}
#[test]
fn test_preset_with_audio_channel_layout() {
let preset = PresetConfig {
audio_codec: Some("opus".to_string()),
audio_bitrate: Some(384_000),
audio_channel_layout: Some(audio_channel_map::AudioLayout::FivePointOne),
..PresetConfig::default()
};
let transcoder = Transcoder::new().preset(preset);
assert_eq!(
transcoder.config.audio_channel_layout,
Some(audio_channel_map::AudioLayout::FivePointOne)
);
assert_eq!(transcoder.config.audio_bitrate, Some(384_000));
}
#[test]
fn test_preset_config_default_has_no_channel_layout() {
let preset = PresetConfig::default();
assert!(preset.audio_channel_layout.is_none());
}
#[test]
fn test_config_default_has_no_stream_copy() {
let config = TranscodeConfig::default();
assert!(config.stream_copy.is_none());
assert!(config.audio_channel_layout.is_none());
}
#[test]
fn test_subtitle_modes() {
assert_eq!(SubtitleMode::Ignore, SubtitleMode::Ignore);
assert_ne!(SubtitleMode::Ignore, SubtitleMode::Copy);
assert_ne!(SubtitleMode::Copy, SubtitleMode::BurnIn);
}
#[test]
fn test_chapter_modes() {
assert_eq!(ChapterMode::Ignore, ChapterMode::Ignore);
assert_ne!(ChapterMode::Ignore, ChapterMode::Copy);
assert_ne!(ChapterMode::Copy, ChapterMode::Custom);
}
}