//! Asynchronous media normalization jobs producing bounded mono WebM/Opus output. use std::collections::HashMap; use std::convert::Infallible; use std::fs::{self, File}; use std::io::{Read, Seek, SeekFrom, Write}; use std::path::{Path as FilePath, PathBuf}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex, RwLock}; use std::time::{Duration as StdDuration, Instant}; use axum::extract::Path; use axum::http::StatusCode; use axum::response::sse::{Event, KeepAlive, Sse}; use axum::response::IntoResponse; use axum::Json; use file_format::{FileFormat, Kind}; use futures::Stream; use once_cell::sync::Lazy; use ropus::{Application, Bitrate, Channels as OpusChannels, DecodeMode, Decoder as OpusDecoder, Encoder as OpusEncoder}; use rubato::audioadapter_buffers::direct::SequentialSliceOfVecs; use rubato::{Fft, FixedSync, Indexing, Resampler}; use serde::{Deserialize, Serialize}; use symphonia::core::audio::sample::Sample; use symphonia::core::codecs::audio::{well_known::CODEC_ID_OPUS, AudioDecoder, AudioDecoderOptions}; use symphonia::core::codecs::CodecParameters; use symphonia::core::errors::Error as SymphoniaError; use symphonia::core::formats::{FormatOptions, Track, TrackFlags, TrackType}; use symphonia::core::io::{MediaSource, MediaSourceStream}; use symphonia::core::meta::MetadataOptions; use symphonia::core::formats::probe::Hint; use symphonia::core::units::{TimeBase, Timestamp}; use tokio::sync::broadcast; use tokio_stream::wrappers::BroadcastStream; use tokio_stream::StreamExt; use webm_iterable::matroska_spec::{Master, MatroskaSpec, SimpleBlock}; use webm_iterable::{WebmIterator, WebmWriter, WriteOptions}; use crate::api_token::APIToken; /// Sample rate required by the normalized WebM/Opus output contract. const OUTPUT_SAMPLE_RATE: u32 = 48_000; /// Number of samples in one 20 ms Opus frame at 48 kHz. const OPUS_FRAME_SAMPLES: usize = 960; /// Target bitrate for mono speech-oriented Opus output. const OPUS_BITRATE: u32 = 32_000; /// Stable normalized container name returned to upload clients. const OUTPUT_FORMAT: &str = "webm"; /// Stable normalized codec name returned to upload clients. const OUTPUT_CODEC: &str = "opus"; /// Maximum duration of a WebM cluster before rotating it. const CLUSTER_DURATION_MS: u64 = 30_000; /// Encoder look-ahead advertised as the output track's codec delay. const OPUS_PRE_SKIP: u16 = 312; /// Default size ceiling for copying an already-normalized file unchanged. const DEFAULT_MAX_PASS_THROUGH_BYTES: u64 = 25 * 1024 * 1024; /// Bounded input block used for streaming resampling. const RESAMPLE_INPUT_BLOCK_SAMPLES: usize = 2_048; /// Bounded block used by the cancellation-aware pass-through copy. const COPY_BLOCK_BYTES: usize = 64 * 1024; /// Minimum interval between non-terminal progress events in one phase. const PROGRESS_EVENT_INTERVAL: StdDuration = StdDuration::from_secs(6); /// Timestamp differences above this threshold are recorded as discontinuities. const LARGE_DISCONTINUITY_MS: i64 = 1_000; /// Maximum full-scale peak still treated as practical silence. const SILENCE_MAX_PEAK_DBFS: f32 = -60.0; /// Time a terminal job remains available for late SSE subscribers. const TERMINAL_JOB_RETENTION: std::time::Duration = std::time::Duration::from_secs(10 * 60); /// In-memory registry of running and recently completed media jobs. static JOBS: Lazy>>> = Lazy::new(|| RwLock::new(HashMap::new())); /// Request body for starting a media normalization job. #[derive(Debug, Deserialize)] pub struct CreateMediaJobRequest { /// Absolute path of the source media file. pub input_path: String, /// Optional absolute output path; a sibling WebM path is derived when omitted. pub output_path: Option, /// Optional size ceiling for pass-through files. pub max_pass_through_bytes: Option, } /// Response returned immediately after a media job has been registered. #[derive(Debug, Serialize)] pub struct CreateMediaJobResponse { /// Opaque identifier used by the event and cancellation routes. pub job_id: String, } /// Observable lifecycle phases of a media job. #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub enum MediaJobPhase { /// The runtime is identifying the container and selecting an audio track. Probing, /// The runtime is decoding and normalizing the selected track. Transcoding, /// The normalized output was committed atomically. Completed, /// The job ended with a stable media error. Failed, /// Cancellation completed and temporary output has been removed. Cancelled, } /// Stable, machine-readable failure categories returned by the media API. #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub enum MediaErrorCode { /// The requested input file does not exist. FileNotFound, /// The file type could not be identified. UnknownFormat, /// Executable input was rejected. UnsafeFile, /// The identified input is not audio or video. NotMedia, /// The input file could not be opened. FileOpenFailed, /// Symphonia does not support the container. UnsupportedContainer, /// The container has no audio track. NoAudioTrack, /// No audio track has a supported decoder. UnsupportedCodec, /// Required decoded stream parameters are absent or changed. InvalidAudioParameters, /// The Opus identification header is invalid. InvalidOpusHeader, /// The Opus mapping requires unsupported multistream decoding. UnsupportedOpusMapping, /// The decoder could not be initialized. DecoderInitFailed, /// The encoder could not be initialized. EncoderInitFailed, /// The stream requested a decoder reset. StreamReset, /// The media container is truncated or malformed. DamagedContainer, /// Audio decoding failed. DecodeFailed, /// Audio resampling failed. ResampleFailed, /// Opus encoding failed. EncodeFailed, /// The output directory could not be created. OutputCreateFailed, /// Output bytes could not be written. OutputWriteFailed, /// The partial output could not be committed. OutputCommitFailed, /// A WebM relative timestamp exceeded its safe range. WebmTimestampOverflow, /// WebM output serialization failed. WebmWriteFailed, /// The job was cancelled. Cancelled, /// The worker task terminated unexpectedly. InternalError, } /// Snapshot delivered through the media job SSE stream. #[derive(Clone, Debug, Serialize)] pub struct MediaJobEvent { /// Current lifecycle phase. pub phase: MediaJobPhase, /// Optional progress fraction between zero and one. pub progress: Option, /// Terminal result, present only for completed jobs. pub result: Option, /// Terminal diagnostic, present only for failed jobs. pub error: Option, } /// Successful normalized-media result. #[derive(Clone, Debug, Serialize)] pub struct MediaJobResult { /// Path at which the normalized output was committed. pub output_path: String, /// Stable container produced for provider uploads. pub output_format: String, /// Stable audio codec produced for provider uploads. pub output_codec: String, /// Human-readable detected container description for diagnostics. pub detected_format: String, /// Human-readable selected codec description for diagnostics. pub detected_codec: String, /// Duration of the normalized playable audio. pub duration_ms: u64, /// Whether the input was copied unchanged. pub pass_through: bool, /// Whether the normalized audio exceeds the practical-silence threshold. pub has_audible_signal: bool, } /// Stable error code plus an English diagnostic intended for logs. #[derive(Clone, Debug, Serialize)] pub struct MediaError { /// Machine-readable failure category used for localization by the client. pub code: MediaErrorCode, /// US-English diagnostic detail for logging and support. pub message: String, } impl MediaError { /// Creates a media error without exposing free-form codes on the wire. fn new(code: MediaErrorCode, message: impl Into) -> Self { Self { code, message: message.into() } } } /// Mutable state shared by the request routes and blocking worker. struct MediaJob { /// Cooperative cancellation flag checked at bounded intervals. cancelled: Arc, /// The latest snapshot replayed to a newly connected SSE subscriber. current: Mutex, /// Fan-out channel for live state changes. events: broadcast::Sender, /// Last running progress publication, used to protect Blazor from render storms. last_progress: Mutex>, } impl MediaJob { /// Creates a job in the probing phase before its worker is scheduled. fn new() -> Self { let initial = MediaJobEvent { phase: MediaJobPhase::Probing, progress: Some(0.0), result: None, error: None, }; let (events, _) = broadcast::channel(32); Self { cancelled: Arc::new(AtomicBool::new(false)), current: Mutex::new(initial), events, last_progress: Mutex::new(None), } } /// Replaces the replay snapshot and notifies all live subscribers. fn publish(&self, event: MediaJobEvent) { *self.current.lock().unwrap() = event.clone(); let _ = self.events.send(event); } /// Publishes running progress no more than once per interval and always on a phase change. fn publish_progress(&self, phase: MediaJobPhase, progress: Option) { let now = Instant::now(); let mut last = self.last_progress.lock().unwrap(); if last.is_some_and(|(last_phase, last_at)| last_phase == phase && now.duration_since(last_at) < PROGRESS_EVENT_INTERVAL) { return; } *last = Some((phase, now)); drop(last); self.publish(MediaJobEvent { phase, progress, result: None, error: None }); } /// Returns whether cooperative cancellation was requested. fn is_cancelled(&self) -> bool { self.cancelled.load(Ordering::Relaxed) } } /// Registers and immediately schedules a media normalization job. pub async fn create_job( _token: APIToken, Json(request): Json, ) -> Result, (StatusCode, Json)> { let input_path = PathBuf::from(&request.input_path); if !input_path.is_file() { return Err((StatusCode::BAD_REQUEST, Json(MediaError::new(MediaErrorCode::FileNotFound, "The selected media file does not exist.")))); } let output_path = request.output_path.map(PathBuf::from).unwrap_or_else(|| { let parent = input_path.parent().unwrap_or_else(|| FilePath::new(".")); let stem = input_path.file_stem().and_then(|value| value.to_str()).unwrap_or("media"); parent.join(format!("{stem}-normalized.webm")) }); let job_id = format!("{}-{}", std::process::id(), rand::random::()); let job = Arc::new(MediaJob::new()); JOBS.write().unwrap().insert(job_id.clone(), Arc::clone(&job)); let completed_job_id = job_id.clone(); tauri::async_runtime::spawn(async move { let started_at = Instant::now(); log::info!("media job registered: job_id={completed_job_id}"); let max_pass_through_bytes = request.max_pass_through_bytes.unwrap_or(DEFAULT_MAX_PASS_THROUGH_BYTES); let task_job = Arc::clone(&job); let result = tokio::task::spawn_blocking(move || normalize_media(&input_path, &output_path, max_pass_through_bytes, &task_job)).await; match result { Ok(Ok(result)) => { log::info!("media job completed: job_id={completed_job_id}, elapsed_ms={}", started_at.elapsed().as_millis()); job.publish(MediaJobEvent { phase: MediaJobPhase::Completed, progress: Some(1.0), result: Some(result), error: None, }); } Ok(Err(error)) if error.code == MediaErrorCode::Cancelled => { log::info!("media job cancelled: job_id={completed_job_id}, elapsed_ms={}", started_at.elapsed().as_millis()); job.publish(MediaJobEvent { phase: MediaJobPhase::Cancelled, progress: None, result: None, error: None, }); } Ok(Err(error)) => { log::error!("media job failed: job_id={completed_job_id}, code={:?}, diagnostic={}, elapsed_ms={}", error.code, error.message, started_at.elapsed().as_millis()); job.publish(MediaJobEvent { phase: MediaJobPhase::Failed, progress: None, result: None, error: Some(error), }); } Err(error) => job.publish(MediaJobEvent { phase: MediaJobPhase::Failed, progress: None, result: None, error: Some(MediaError::new(MediaErrorCode::InternalError, format!("The media worker failed: {error}"))), }), } retain_terminal_job(completed_job_id).await; }); Ok(Json(CreateMediaJobResponse { job_id })) } /// Retains a terminal job for late SSE subscribers, then removes it asynchronously. /// /// Retention starts only after the worker has published a terminal event. Sleeping here neither /// blocks the originating request nor the blocking media worker. async fn retain_terminal_job(job_id: String) { tokio::time::sleep(TERMINAL_JOB_RETENTION).await; JOBS.write().unwrap().remove(&job_id); } /// Streams the current snapshot followed by live media job events. pub async fn get_job_events( _token: APIToken, Path(job_id): Path, ) -> Result>>, StatusCode> { let job = JOBS.read().unwrap().get(&job_id).cloned().ok_or(StatusCode::NOT_FOUND)?; let current = job.current.lock().unwrap().clone(); let initial = tokio_stream::once(current); let updates = BroadcastStream::new(job.events.subscribe()).filter_map(|event| event.ok()); let stream = initial.chain(updates).map(|event| { let data = serde_json::to_string(&event).unwrap_or_else(|_| "{}".to_string()); Ok(Event::default().event(phase_name(&event.phase)).data(data)) }); Ok(Sse::new(stream).keep_alive(KeepAlive::default())) } /// Requests cooperative cancellation of a running media job. pub async fn cancel_job(_token: APIToken, Path(job_id): Path) -> impl IntoResponse { match JOBS.read().unwrap().get(&job_id) { Some(job) => { job.cancelled.store(true, Ordering::Relaxed); StatusCode::NO_CONTENT } None => StatusCode::NOT_FOUND, } } /// Maps a phase to the corresponding SSE event name. fn phase_name(phase: &MediaJobPhase) -> &'static str { match phase { MediaJobPhase::Probing => "probing", MediaJobPhase::Transcoding => "transcoding", MediaJobPhase::Completed => "completed", MediaJobPhase::Failed => "failed", MediaJobPhase::Cancelled => "cancelled", } } /// Shared byte position retained after the source is moved into Symphonia. #[derive(Clone)] struct SourceProgress { bytes_read: Arc, length: u64, } impl SourceProgress { /// Returns monotonically clamped sequential read progress. fn fraction(&self) -> Option { (self.length > 0).then(|| (self.bytes_read.load(Ordering::Relaxed) as f64 / self.length as f64).clamp(0.0, 0.99)) } } /// File source that checks cancellation inside every read and seek operation. struct CancellationMediaSource { file: File, cancelled: Arc, bytes_read: Arc, length: u64, } impl CancellationMediaSource { /// Wraps a regular file and exposes a progress handle to the transcoder. fn new(file: File, cancelled: Arc) -> std::io::Result<(Self, SourceProgress)> { let length = file.metadata()?.len(); let bytes_read = Arc::new(AtomicU64::new(0)); let progress = SourceProgress { bytes_read: Arc::clone(&bytes_read), length }; Ok((Self { file, cancelled, bytes_read, length }, progress)) } /// Converts cancellation into an interrupted I/O operation understood by the reader. fn check_cancelled(&self) -> std::io::Result<()> { if self.cancelled.load(Ordering::Relaxed) { Err(std::io::Error::new(std::io::ErrorKind::Interrupted, "media job cancelled")) } else { Ok(()) } } } impl Read for CancellationMediaSource { fn read(&mut self, buffer: &mut [u8]) -> std::io::Result { self.check_cancelled()?; let count = self.file.read(buffer)?; self.bytes_read.fetch_add(count as u64, Ordering::Relaxed); self.check_cancelled()?; Ok(count) } } impl Seek for CancellationMediaSource { fn seek(&mut self, position: SeekFrom) -> std::io::Result { self.check_cancelled()?; let position = self.file.seek(position)?; self.check_cancelled()?; Ok(position) } } impl MediaSource for CancellationMediaSource { fn is_seekable(&self) -> bool { true } fn byte_len(&self) -> Option { Some(self.length) } } /// Probes, normalizes, and atomically commits one media file. fn normalize_media(input_path: &FilePath, output_path: &FilePath, max_pass_through_bytes: u64, job: &MediaJob) -> Result { check_cancelled(job)?; let detected = FileFormat::from_file(input_path) .map_err(|error| MediaError::new(MediaErrorCode::UnknownFormat, format!("The file type could not be identified: {error}")))?; if detected.kind() == Kind::Executable { return Err(MediaError::new(MediaErrorCode::UnsafeFile, "The selected file contains executable data and cannot be processed as media.")); } if !matches!(detected.kind(), Kind::Audio | Kind::Video) && !matches!( detected, FileFormat::ExtensibleBinaryMetaLanguage | FileFormat::Id3v2 | FileFormat::Mpeg4Part14 | FileFormat::Mpeg4Part14Audio | FileFormat::Mpeg4Part14Video ) && !has_supported_media_extension(input_path) { return Err(MediaError::new(MediaErrorCode::NotMedia, format!("The selected file is not supported media (detected as {detected:?})."))); } let file_size = input_path.metadata().map(|metadata| metadata.len()).unwrap_or(0); log::info!("media job started: input='{}', output='{}', size_bytes={}, detected_file_format={detected:?}", input_path.display(), output_path.display(), file_size); let file = File::open(input_path).map_err(|error| MediaError::new(MediaErrorCode::FileOpenFailed, error.to_string()))?; let (source, source_progress) = CancellationMediaSource::new(file, Arc::clone(&job.cancelled)) .map_err(|error| MediaError::new(MediaErrorCode::FileOpenFailed, error.to_string()))?; let mss = MediaSourceStream::new(Box::new(source), Default::default()); let mut hint = Hint::new(); if let Some(extension) = input_path.extension().and_then(|value| value.to_str()) { hint.with_extension(extension); } let mut format = match symphonia::default::get_probe() .probe(&hint, mss, FormatOptions::default(), MetadataOptions::default()) { Ok(format) => format, Err(_) if job.is_cancelled() => return Err(MediaError::new(MediaErrorCode::Cancelled, "The media job was cancelled.")), Err(error) => return Err(map_probe_error(error)), }; let detected_format = format!("{detected:?} / {}", format.format_info().long_name); let tracks = format.tracks(); for track in tracks { if let Some(params) = track.codec_params.as_ref().and_then(CodecParameters::audio) { log::info!( "media track: id={}, type={:?}, default={}, codec={}, sample_rate={:?}, channels={:?}, duration={:?}, time_base={:?}", track.id, track.track_type(), track.flags.contains(TrackFlags::DEFAULT), params.codec, params.sample_rate, params.channels.as_ref().map(|channels| channels.count()), track.duration, track.time_base, ); } else { log::info!("media track: id={}, type={:?}, default={}, non_audio=true", track.id, track.track_type(), track.flags.contains(TrackFlags::DEFAULT)); } } if !tracks.iter().any(is_audio_track) { return Err(MediaError::new(MediaErrorCode::NoAudioTrack, "The selected media file does not contain an audio track.")); } let selected = select_audio_track(tracks) .ok_or_else(|| MediaError::new(MediaErrorCode::UnsupportedCodec, "None of the audio tracks uses a supported codec."))?; let track_id = selected.id; let track_delay = selected.delay.unwrap_or(0); let track_padding = selected.padding.unwrap_or(0); let track_time_base = selected.time_base; let params = selected.codec_params.as_ref().and_then(CodecParameters::audio).unwrap().clone(); let detected_codec = if params.codec == CODEC_ID_OPUS { "opus".to_string() } else { format!("{}", params.codec) }; let track_duration_ms = selected.num_frames.zip(params.sample_rate) .map(|(frames, rate)| frames.saturating_mul(1_000) / u64::from(rate)) .or_else(|| selected.time_base.zip(selected.duration).and_then(|(time_base, duration)| { let timestamp = Timestamp::new(i64::try_from(duration.get()).ok()?); let time = time_base.calc_time(timestamp)?; Some((time.as_secs_f64() * 1000.0).max(0.0).round() as u64) })); let container_duration_ms = format.media_info().time_base.zip(format.media_info().duration).and_then(|(time_base, duration)| { let timestamp = Timestamp::new(i64::try_from(duration.get()).ok()?); let time = time_base.calc_time(timestamp)?; Some((time.as_secs_f64() * 1000.0).max(0.0).round() as u64) }); let duration_ms = track_duration_ms.or(container_duration_ms).unwrap_or(0); log::info!( "media audio selection: track_id={}, default={}, track_duration_ms={:?}, container_duration_ms={:?}, progress_duration_ms={}", track_id, selected.flags.contains(TrackFlags::DEFAULT), track_duration_ms, container_duration_ms, duration_ms, ); let channels = params.channels.as_ref().map(|value| value.count()).unwrap_or(0); let pass_through = is_webm_container(input_path) && tracks.len() == 1 && selected.track_type() == Some(TrackType::Audio) && params.codec == CODEC_ID_OPUS && params.sample_rate == Some(OUTPUT_SAMPLE_RATE) && channels == 1 && input_path.metadata().map(|metadata| metadata.len() <= max_pass_through_bytes).unwrap_or(false); log::info!("media normalization decision: track_id={track_id}, pass_through={pass_through}, codec={detected_codec}, channels={channels}"); let partial_path = partial_path(output_path); if let Some(parent) = partial_path.parent() { fs::create_dir_all(parent).map_err(|error| MediaError::new(MediaErrorCode::OutputCreateFailed, error.to_string()))?; } let result = if pass_through { job.publish_progress(MediaJobPhase::Transcoding, Some(0.0)); let analysis_context = SignalAnalysisContext { track_id, params: ¶ms, track_delay, track_padding, expected_duration_ms: duration_ms, time_base: track_time_base, source_progress: &source_progress, job, }; let signal = analyze_audio_signal(&mut *format, analysis_context)?; copy_with_cancellation(input_path, &partial_path, job)?; Ok(MediaJobResult { output_path: output_path.to_string_lossy().into_owned(), output_format: OUTPUT_FORMAT.to_string(), output_codec: OUTPUT_CODEC.to_string(), detected_format: detected_format.clone(), detected_codec, duration_ms, pass_through: true, has_audible_signal: signal.has_audible_signal(), }) } else { job.publish_progress(MediaJobPhase::Transcoding, Some(0.0)); let context = TranscodeContext { track_id, track_delay, track_padding, params, partial_path: &partial_path, output_path, detected_format, detected_codec, expected_duration_ms: duration_ms, time_base: track_time_base, source_progress, job, }; transcode(&mut *format, context) }; match result { Ok(result) => { if let Err(error) = fs::rename(&partial_path, output_path) { let _ = fs::remove_file(&partial_path); return Err(MediaError::new(MediaErrorCode::OutputCommitFailed, error.to_string())); } Ok(result) } Err(error) => { let _ = fs::remove_file(&partial_path); Err(error) } } } /// Recognizes extensions for containers supported by the configured Symphonia readers. fn has_supported_media_extension(path: &FilePath) -> bool { path.extension() .and_then(|extension| extension.to_str()) .is_some_and(|extension| matches!( extension.to_ascii_lowercase().as_str(), "aac" | "aif" | "aiff" | "caf" | "flac" | "m4a" | "mka" | "mkv" | "mov" | "mp1" | "mp2" | "mp3" | "mp4" | "oga" | "ogg" | "opus" | "wav" | "webm" )) } /// Returns whether a track explicitly contains audio codec parameters. fn is_audio_track(track: &Track) -> bool { matches!(track.codec_params, Some(CodecParameters::Audio(_))) } /// Selects the decodable default audio track, falling back to the first decodable audio track. fn select_audio_track(tracks: &[Track]) -> Option<&Track> { tracks .iter() .filter(|track| is_audio_track(track) && is_decodable(track)) .min_by_key(|track| !track.flags.contains(TrackFlags::DEFAULT)) } /// Checks whether the runtime can construct a decoder for the track. fn is_decodable(track: &Track) -> bool { let Some(params) = track.codec_params.as_ref().and_then(CodecParameters::audio) else { return false; }; params.codec == CODEC_ID_OPUS || symphonia::default::get_codecs().make_audio_decoder(params, &AudioDecoderOptions::default()).is_ok() } /// Streaming peak measurement over normalized full-scale floating-point samples. #[derive(Default)] struct AudioPeakDetector { /// Highest absolute sample observed so far. max_amplitude: f32, } impl AudioPeakDetector { /// Includes one bounded sample block in the maximum-peak measurement. fn observe(&mut self, samples: &[f32]) { for sample in samples { let amplitude = sample.abs(); self.max_amplitude = if amplitude.is_finite() { self.max_amplitude.max(amplitude) } else { f32::INFINITY }; } } /// Returns whether any retained sample exceeds the configured silence ceiling. fn has_audible_signal(&self) -> bool { self.max_amplitude > 10.0_f32.powf(SILENCE_MAX_PEAK_DBFS / 20.0) } /// Returns the measured full-scale peak for diagnostics. fn max_peak_dbfs(&self) -> f32 { if self.max_amplitude == 0.0 { f32::NEG_INFINITY } else { 20.0 * self.max_amplitude.log10() } } } /// Inputs required to scan an otherwise pass-through-compatible audio track. struct SignalAnalysisContext<'a> { /// Selected track identifier. track_id: u32, /// Selected track codec parameters. params: &'a symphonia::core::codecs::audio::AudioCodecParameters, /// Leading decoded frames to discard. track_delay: u32, /// Trailing decoded frames to discard. track_padding: u32, /// Container duration used for progress reporting. expected_duration_ms: u64, /// Selected track timebase used for progress reporting. time_base: Option, /// Sequential byte progress fallback when the track has no duration. source_progress: &'a SourceProgress, /// Cancellation and progress state for the job. job: &'a MediaJob, } /// Decodes an otherwise pass-through-compatible track solely to classify practical silence. fn analyze_audio_signal( format: &mut dyn symphonia::core::formats::FormatReader, context: SignalAnalysisContext<'_>, ) -> Result { let mut decoder = StreamDecoder::new(context.params, context.track_delay)?; let mut detector = AudioPeakDetector::default(); let mut decoded_tail = Vec::::new(); let mut first_packet_pts = None::; let mut decoded_packets = 0u64; let mut last_progress = 0.0f64; loop { check_cancelled(context.job)?; let packet = match format.next_packet() { Ok(Some(packet)) => packet, Ok(None) => break, Err(SymphoniaError::ResetRequired) => return Err(MediaError::new(MediaErrorCode::StreamReset, "The media stream changed unexpectedly.")), Err(SymphoniaError::IoError(error)) if error.kind() == std::io::ErrorKind::Interrupted && context.job.is_cancelled() => { return Err(MediaError::new(MediaErrorCode::Cancelled, "The media job was cancelled.")); } Err(SymphoniaError::IoError(error)) if error.kind() == std::io::ErrorKind::UnexpectedEof => break, Err(error) => return Err(MediaError::new(MediaErrorCode::DamagedContainer, format!("The media container is damaged: {error}"))), }; if packet.track_id != context.track_id { continue; } let packet_pts = packet.pts.get(); first_packet_pts.get_or_insert(packet_pts); let Some((mono, _)) = decoder.decode(&packet)? else { continue; }; decoded_packets += 1; decoded_tail.extend_from_slice(&mono); let emit_len = decoded_tail.len().saturating_sub(context.track_padding as usize); if emit_len > 0 { detector.observe(&decoded_tail[..emit_len]); drop(decoded_tail.drain(..emit_len)); } let timestamp_ms = packet_timestamp_ms(packet_pts, first_packet_pts, context.time_base); let progress = if context.expected_duration_ms > 0 { timestamp_ms.map(|current_ms| (current_ms as f64 / context.expected_duration_ms as f64).clamp(0.0, 0.99)) } else { context.source_progress.fraction() }; if let Some(progress) = progress { last_progress = last_progress.max(progress); } context.job.publish_progress(MediaJobPhase::Transcoding, progress.map(|_| last_progress)); } if decoded_packets == 0 { return Err(MediaError::new(MediaErrorCode::InvalidAudioParameters, "The selected audio track did not yield decoded audio samples.")); } log::info!( "media signal analysis completed: track_id={}, max_peak_dbfs={}, silence_threshold_dbfs={}, has_audible_signal={}", context.track_id, detector.max_peak_dbfs(), SILENCE_MAX_PEAK_DBFS, detector.has_audible_signal(), ); Ok(detector) } /// Immutable inputs shared across a single transcoding operation. struct TranscodeContext<'a> { /// Selected input track identifier. track_id: u32, /// Leading decoded frames to discard. track_delay: u32, /// Trailing decoded frames to discard. track_padding: u32, /// Selected track codec parameters. params: symphonia::core::codecs::audio::AudioCodecParameters, /// Temporary output path used until the job succeeds. partial_path: &'a FilePath, /// Final output path returned in the result. output_path: &'a FilePath, /// Detected container diagnostic. detected_format: String, /// Detected codec diagnostic. detected_codec: String, /// Container duration used for progress reporting. expected_duration_ms: u64, /// Selected track timebase used to align packet presentation timestamps. time_base: Option, /// Sequential byte progress fallback when the selected track has no duration. source_progress: SourceProgress, /// Cancellation and progress state for the job. job: &'a MediaJob, } /// Decodes a selected track and writes timestamp-aligned 20 ms mono Opus frames. fn transcode( format: &mut dyn symphonia::core::formats::FormatReader, context: TranscodeContext<'_>, ) -> Result { let mut decoder = StreamDecoder::new(&context.params, context.track_delay)?; let mut opus_encoder = OpusEncoder::builder(OUTPUT_SAMPLE_RATE, OpusChannels::Mono, Application::Audio) .bitrate(Bitrate::Bits(OPUS_BITRATE)) .vbr(true) .build() .map_err(|error| MediaError::new(MediaErrorCode::EncoderInitFailed, error.to_string()))?; let file = File::create(context.partial_path).map_err(|error| MediaError::new(MediaErrorCode::OutputCreateFailed, error.to_string()))?; let mut writer = WebmOpusWriter::new(file)?; let mut pending = Vec::::with_capacity(OPUS_FRAME_SAMPLES * 3); let mut signal = AudioPeakDetector::default(); let mut resampler: Option = None; let mut decoded_tail = Vec::::new(); let mut encoded = [0u8; 4_000]; let mut produced_samples = 0u64; let mut first_packet_pts = None::; let mut last_packet_pts = None::; let mut decoded_packets = 0u64; let mut discarded_packets = 0u64; let mut decode_errors = 0u64; let mut discontinuities = 0u64; let mut last_progress = 0.0f64; loop { check_cancelled(context.job)?; let packet = match format.next_packet() { Ok(Some(packet)) => packet, Ok(None) => break, Err(SymphoniaError::ResetRequired) => return Err(MediaError::new(MediaErrorCode::StreamReset, "The media stream changed unexpectedly.")), Err(SymphoniaError::IoError(error)) if error.kind() == std::io::ErrorKind::Interrupted && context.job.is_cancelled() => { return Err(MediaError::new(MediaErrorCode::Cancelled, "The media job was cancelled.")); } Err(SymphoniaError::IoError(error)) if error.kind() == std::io::ErrorKind::UnexpectedEof => break, Err(error) => return Err(MediaError::new(MediaErrorCode::DamagedContainer, format!("The media container is damaged: {error}"))), }; if packet.track_id != context.track_id { discarded_packets += 1; continue; } let packet_pts = packet.pts.get(); first_packet_pts.get_or_insert(packet_pts); last_packet_pts = Some(packet_pts); let Some((mono, sample_rate)) = decoder.decode(&packet)? else { decode_errors += 1; continue; }; decoded_packets += 1; let stream_resampler = match resampler.as_mut() { Some(existing) if existing.input_rate() == sample_rate => existing, Some(_) => return Err(MediaError::new(MediaErrorCode::InvalidAudioParameters, "The decoded audio sample rate changed during the stream.")), None => { log::info!("media resampling: input_rate={sample_rate}, output_rate={OUTPUT_SAMPLE_RATE}, enabled={}", sample_rate != OUTPUT_SAMPLE_RATE); resampler.insert(StreamResampler::new(sample_rate)?) } }; // Retain only the possible end padding so it can never be emitted prematurely. decoded_tail.extend_from_slice(&mono); let padding = context.track_padding as usize; let emit_len = decoded_tail.len().saturating_sub(padding); if emit_len > 0 { let emit: Vec<_> = decoded_tail.drain(..emit_len).collect(); let resampled = stream_resampler.push(&emit)?; // FFT resamplers buffer across packet boundaries, so their returned samples no longer // begin at the current packet PTS. Native 48-kHz streams retain exact packet alignment. let desired_start = (sample_rate == OUTPUT_SAMPLE_RATE) .then(|| packet_output_start(packet_pts, first_packet_pts, context.time_base)) .flatten(); let current_start = produced_samples.saturating_add(pending.len() as u64); let previous_pending_len = pending.len(); append_timestamp_aligned( &mut pending, &resampled, current_start, desired_start, context.track_id, packet_pts, &mut discontinuities, ); signal.observe(&pending[previous_pending_len..]); } encode_complete_frames(&mut pending, &mut opus_encoder, &mut writer, &mut encoded, &mut produced_samples, context.job)?; let timestamp_ms = packet_timestamp_ms(packet_pts, first_packet_pts, context.time_base); let progress = if context.expected_duration_ms > 0 { timestamp_ms.map(|current_ms| (current_ms as f64 / context.expected_duration_ms as f64).clamp(0.0, 0.99)) } else { context.source_progress.fraction() }; if let Some(progress) = progress { last_progress = last_progress.max(progress); } context.job.publish_progress(MediaJobPhase::Transcoding, progress.map(|_| last_progress)); } if let Some(stream_resampler) = resampler.as_mut() { // Discard the retained decoded tail (container padding), then flush the filter delay. let flushed = stream_resampler.finish()?; signal.observe(&flushed); pending.extend_from_slice(&flushed); } else { return Err(MediaError::new(MediaErrorCode::InvalidAudioParameters, "The selected audio track did not yield decoded audio parameters.")); } encode_complete_frames(&mut pending, &mut opus_encoder, &mut writer, &mut encoded, &mut produced_samples, context.job)?; if !pending.is_empty() { pending.resize(OPUS_FRAME_SAMPLES, 0.0); let length = opus_encoder.encode_float(&pending, &mut encoded) .map_err(|error| MediaError::new(MediaErrorCode::EncodeFailed, error.to_string()))?; writer.write_packet(&encoded[..length], produced_samples)?; produced_samples += OPUS_FRAME_SAMPLES as u64; } writer.finish()?; check_cancelled(context.job)?; let output_size = fs::metadata(context.partial_path).map(|metadata| metadata.len()).unwrap_or(0); let output_duration_ms = produced_samples.saturating_mul(1000) / u64::from(OUTPUT_SAMPLE_RATE); if context.expected_duration_ms > 0 && output_duration_ms.abs_diff(context.expected_duration_ms) > 2_000 { log::warn!( "media duration mismatch: track_id={}, expected_duration_ms={}, decoded_duration_ms={}", context.track_id, context.expected_duration_ms, output_duration_ms, ); } log::info!( "media transcode completed: track_id={}, decoded_packets={}, discarded_packets={}, recoverable_decode_errors={}, first_pts={:?}, last_pts={:?}, discontinuities={}, output_bytes={}, duration_ms={}, max_peak_dbfs={}, silence_threshold_dbfs={}, has_audible_signal={}", context.track_id, decoded_packets, discarded_packets, decode_errors, first_packet_pts, last_packet_pts, discontinuities, output_size, output_duration_ms, signal.max_peak_dbfs(), SILENCE_MAX_PEAK_DBFS, signal.has_audible_signal(), ); Ok(MediaJobResult { output_path: context.output_path.to_string_lossy().into_owned(), output_format: OUTPUT_FORMAT.to_string(), output_codec: OUTPUT_CODEC.to_string(), detected_format: context.detected_format, detected_codec: context.detected_codec, duration_ms: output_duration_ms, pass_through: false, has_audible_signal: signal.has_audible_signal(), }) } /// Converts a packet PTS to its output sample offset relative to the first audio packet. fn packet_output_start(packet_pts: i64, first_packet_pts: Option, time_base: Option) -> Option { packet_timestamp_ms(packet_pts, first_packet_pts, time_base) .map(|milliseconds| milliseconds.saturating_mul(u64::from(OUTPUT_SAMPLE_RATE)) / 1_000) } /// Converts a packet PTS to milliseconds relative to the first selected-track packet. fn packet_timestamp_ms(packet_pts: i64, first_packet_pts: Option, time_base: Option) -> Option { let delta = packet_pts.checked_sub(first_packet_pts?)?; if delta < 0 { return Some(0); } let time = time_base?.calc_time(Timestamp::new(delta))?; Some((time.as_secs_f64() * 1_000.0).max(0.0).round() as u64) } /// Inserts silence for forward timestamp gaps and trims overlapping decoded samples. fn append_timestamp_aligned( pending: &mut Vec, samples: &[f32], current_start: u64, desired_start: Option, track_id: u32, packet_pts: i64, discontinuities: &mut u64, ) { let Some(desired_start) = desired_start else { pending.extend_from_slice(samples); return; }; let delta = i128::from(desired_start) - i128::from(current_start); let delta_ms = delta.saturating_mul(1_000) / i128::from(OUTPUT_SAMPLE_RATE); if delta_ms.unsigned_abs() >= LARGE_DISCONTINUITY_MS as u128 { *discontinuities += 1; log::warn!("audio timestamp discontinuity: track_id={track_id}, pts={packet_pts}, delta_ms={delta_ms}"); } if delta > 0 { let silence = usize::try_from(delta).unwrap_or(usize::MAX); pending.resize(pending.len().saturating_add(silence), 0.0); pending.extend_from_slice(samples); } else { let overlap = usize::try_from(delta.unsigned_abs()).unwrap_or(usize::MAX).min(samples.len()); pending.extend_from_slice(&samples[overlap..]); } } /// Downmixes interleaved PCM to mono using a deterministic arithmetic mean. fn downmix_to_mono(samples: &[f32], channels: usize) -> Vec { if channels <= 1 { return samples.to_vec(); } samples.chunks_exact(channels).map(|frame| frame.iter().copied().sum::() / channels as f32).collect() } /// Parsed subset of an Opus identification header supported by the single-stream decoder. #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct OpusHeader { /// Mono or stereo channel count. channels: usize, /// Number of leading decoded frames to discard at 48 kHz. pre_skip: u16, } impl OpusHeader { /// Parses and validates a mono/stereo, mapping-family-zero `OpusHead` packet. fn parse(data: &[u8]) -> Result { if data.len() < 19 || &data[..8] != b"OpusHead" || data[8] > 15 || !matches!(data[9], 1 | 2) { return Err(MediaError::new(MediaErrorCode::InvalidOpusHeader, "The Opus identification header is invalid.")); } if data[18] != 0 { return Err(MediaError::new(MediaErrorCode::UnsupportedOpusMapping, "The Opus channel mapping requires unsupported multistream decoding.")); } Ok(Self { channels: usize::from(data[9]), pre_skip: u16::from_le_bytes([data[10], data[11]]), }) } } /// Ropus adapter that validates Symphonia parameters and applies Opus pre-skip exactly once. struct RopusOpusDecoder { /// Underlying libopus single-stream decoder. decoder: OpusDecoder, /// Number of interleaved channels produced by the decoder. channels: usize, /// Remaining leading frames to discard. delay_remaining: usize, /// Reused bounded PCM output buffer. pcm: Vec, } impl RopusOpusDecoder { /// Builds a mono/stereo decoder from the codec's `OpusHead` private data. fn new(params: &symphonia::core::codecs::audio::AudioCodecParameters, track_delay: u32) -> Result { let header = OpusHeader::parse(params.extra_data.as_deref().unwrap_or_default())?; let declared_channels = params.channels.as_ref().map(|channels| channels.count()) .ok_or_else(|| MediaError::new(MediaErrorCode::InvalidAudioParameters, "The Opus stream does not declare its channel count."))?; if declared_channels != header.channels || params.sample_rate != Some(OUTPUT_SAMPLE_RATE) { return Err(MediaError::new(MediaErrorCode::InvalidAudioParameters, "The Opus stream parameters do not match its identification header.")); } let opus_channels = if header.channels == 1 { OpusChannels::Mono } else { OpusChannels::Stereo }; let decoder = OpusDecoder::new(OUTPUT_SAMPLE_RATE, opus_channels) .map_err(|error| MediaError::new(MediaErrorCode::DecoderInitFailed, error.to_string()))?; let delay = if track_delay == 0 { u32::from(header.pre_skip) } else { track_delay }; Ok(Self { decoder, channels: header.channels, delay_remaining: delay as usize, pcm: vec![0; 5_760 * header.channels], }) } /// Decodes one Opus packet, downmixes it, and removes leading codec delay. fn decode(&mut self, packet: &[u8]) -> Result, MediaError> { let frames = self.decoder.decode(packet, &mut self.pcm, DecodeMode::Normal) .map_err(|error| MediaError::new(MediaErrorCode::DecodeFailed, error.to_string()))?; let skip = self.delay_remaining.min(frames); self.delay_remaining -= skip; let samples = &self.pcm[skip * self.channels..frames * self.channels]; let interleaved: Vec<_> = samples.iter().map(|sample| f32::from(*sample) / 32_768.0).collect(); Ok(downmix_to_mono(&interleaved, self.channels)) } } /// Decoder abstraction allowing Symphonia demuxing with either its native decoders or Ropus. enum StreamDecoder { /// Decoder supplied by Symphonia for non-Opus codecs. Symphonia { /// Stateful codec decoder. decoder: Box, /// Reused interleaved PCM buffer. interleaved: Vec, /// First observed decoded sample rate. sample_rate: Option, /// First observed decoded channel count. channels: Option, /// Remaining track delay to discard. delay_remaining: usize, }, /// Symphonia-compatible Opus packet adapter backed by Ropus. Opus(Box), } impl StreamDecoder { /// Creates the appropriate decoder without guessing missing stream parameters. fn new(params: &symphonia::core::codecs::audio::AudioCodecParameters, track_delay: u32) -> Result { if params.codec == CODEC_ID_OPUS { return Ok(Self::Opus(Box::new(RopusOpusDecoder::new(params, track_delay)?))); } let decoder = symphonia::default::get_codecs().make_audio_decoder(params, &AudioDecoderOptions::default()) .map_err(|_| MediaError::new(MediaErrorCode::UnsupportedCodec, "The selected audio codec is not supported."))?; Ok(Self::Symphonia { decoder, interleaved: Vec::new(), sample_rate: None, channels: None, delay_remaining: track_delay as usize, }) } /// Decodes one packet and returns mono PCM plus the actual decoder sample rate. fn decode(&mut self, packet: &symphonia::core::packet::Packet) -> Result, u32)>, MediaError> { match self { Self::Opus(decoder) => Ok(Some((decoder.decode(&packet.data)?, OUTPUT_SAMPLE_RATE))), Self::Symphonia { decoder, interleaved, sample_rate, channels, delay_remaining } => { let decoded = match decoder.decode(packet) { Ok(decoded) => decoded, Err(SymphoniaError::DecodeError(_)) => return Ok(None), Err(error) => return Err(MediaError::new(MediaErrorCode::DecodeFailed, format!("Audio decoding failed: {error}"))), }; let actual_rate = decoded.spec().rate(); let actual_channels = decoded.spec().channels().count(); if actual_rate == 0 || actual_channels == 0 { return Err(MediaError::new(MediaErrorCode::InvalidAudioParameters, "The decoder returned invalid audio parameters.")); } if sample_rate.is_some_and(|rate| rate != actual_rate) || channels.is_some_and(|count| count != actual_channels) { return Err(MediaError::new(MediaErrorCode::InvalidAudioParameters, "The decoded audio parameters changed during the stream.")); } *sample_rate = Some(actual_rate); *channels = Some(actual_channels); interleaved.resize(decoded.samples_interleaved(), f32::MID); decoded.copy_to_slice_interleaved(&mut *interleaved); let mono = downmix_to_mono(interleaved, actual_channels); let skip = (*delay_remaining).min(mono.len()); *delay_remaining -= skip; Ok(Some((mono[skip..].to_vec(), actual_rate))) } } } } /// Stateful, bounded mono resampler whose filter history spans decoder packets. enum StreamResampler { /// Zero-copy-rate path for already-48-kHz decoded PCM. Passthrough { /// Input rate retained for stream consistency checks. input_rate: u32, }, /// Rubato FFT resampler and its bounded pending input. Rubato { /// Input rate retained for stream consistency checks. input_rate: u32, /// Stateful resampler instance used for the entire stream. inner: Box>, /// Samples waiting to fill the next fixed input block. pending: Vec, /// Startup-delay output frames still to discard. delay_remaining: usize, /// Total real input frames accepted. total_input: u64, /// Total trimmed output frames returned to the encoder. total_output: u64, }, } impl StreamResampler { /// Creates one resampler for the decoded stream. fn new(input_rate: u32) -> Result { if input_rate == 0 { return Err(MediaError::new(MediaErrorCode::InvalidAudioParameters, "The decoded audio sample rate is missing.")); } if input_rate == OUTPUT_SAMPLE_RATE { return Ok(Self::Passthrough { input_rate }); } let inner = Fft::::new( input_rate as usize, OUTPUT_SAMPLE_RATE as usize, RESAMPLE_INPUT_BLOCK_SAMPLES, 1, FixedSync::Input, ).map_err(|error| MediaError::new(MediaErrorCode::ResampleFailed, error.to_string()))?; let delay_remaining = inner.output_delay(); Ok(Self::Rubato { input_rate, inner: Box::new(inner), pending: Vec::with_capacity(RESAMPLE_INPUT_BLOCK_SAMPLES * 2), delay_remaining, total_input: 0, total_output: 0, }) } /// Returns the configured input sample rate. fn input_rate(&self) -> u32 { match self { Self::Passthrough { input_rate } | Self::Rubato { input_rate, .. } => *input_rate, } } /// Accepts arbitrary packet-sized PCM and processes all complete bounded blocks. fn push(&mut self, samples: &[f32]) -> Result, MediaError> { match self { Self::Passthrough { .. } => Ok(samples.to_vec()), Self::Rubato { inner, pending, delay_remaining, total_input, total_output, .. } => { *total_input += samples.len() as u64; pending.extend_from_slice(samples); let mut output = Vec::new(); loop { let block_len = inner.input_frames_next(); if pending.len() < block_len { break; } let block: Vec<_> = pending.drain(..block_len).collect(); append_resampled(inner, &block, None, delay_remaining, &mut output)?; } *total_output += output.len() as u64; Ok(output) } } } /// Flushes the last partial block and filter delay, returning the exact rounded duration. fn finish(&mut self) -> Result, MediaError> { match self { Self::Passthrough { .. } => Ok(Vec::new()), Self::Rubato { input_rate, inner, pending, delay_remaining, total_input, total_output } => { let target = total_input.saturating_mul(u64::from(OUTPUT_SAMPLE_RATE)).div_ceil(u64::from(*input_rate)); let mut output = Vec::new(); if !pending.is_empty() { let valid = pending.len(); let block_len = inner.input_frames_next(); pending.resize(block_len, 0.0); append_resampled(inner, pending, Some(valid), delay_remaining, &mut output)?; pending.clear(); } while *total_output + (output.len() as u64) < target { let zeros = vec![0.0; inner.input_frames_next()]; append_resampled(inner, &zeros, Some(0), delay_remaining, &mut output)?; } output.truncate(target.saturating_sub(*total_output) as usize); *total_output += output.len() as u64; Ok(output) } } } } /// Processes one Rubato block and removes the resampler's startup delay. fn append_resampled( resampler: &mut Fft, block: &[f32], partial_len: Option, delay_remaining: &mut usize, destination: &mut Vec, ) -> Result<(), MediaError> { let input_data = vec![block.to_vec()]; let input = SequentialSliceOfVecs::new(&input_data, 1, block.len()) .map_err(|error| MediaError::new(MediaErrorCode::ResampleFailed, error.to_string()))?; let indexing = partial_len.map(|length| Indexing::new().partial_len(length)); let output = resampler.process(&input, indexing.as_ref()) .map_err(|error| MediaError::new(MediaErrorCode::ResampleFailed, error.to_string()))?; let data = output.take_data(); let skip = (*delay_remaining).min(data.len()); *delay_remaining -= skip; destination.extend_from_slice(&data[skip..]); Ok(()) } /// Encodes every complete 20 ms frame currently buffered. fn encode_complete_frames( pending: &mut Vec, encoder: &mut OpusEncoder, writer: &mut WebmOpusWriter, encoded: &mut [u8], produced_samples: &mut u64, job: &MediaJob, ) -> Result<(), MediaError> { let mut consumed = 0usize; while pending.len().saturating_sub(consumed) >= OPUS_FRAME_SAMPLES { check_cancelled(job)?; let length = encoder.encode_float(&pending[consumed..consumed + OPUS_FRAME_SAMPLES], encoded) .map_err(|error| MediaError::new(MediaErrorCode::EncodeFailed, error.to_string()))?; writer.write_packet(&encoded[..length], *produced_samples)?; *produced_samples += OPUS_FRAME_SAMPLES as u64; consumed += OPUS_FRAME_SAMPLES; } if consumed > 0 { pending.drain(..consumed); } Ok(()) } /// Copies a pass-through input in bounded blocks with cooperative cancellation checks. fn copy_with_cancellation(input_path: &FilePath, output_path: &FilePath, job: &MediaJob) -> Result<(), MediaError> { let mut input = File::open(input_path).map_err(|error| MediaError::new(MediaErrorCode::FileOpenFailed, error.to_string()))?; let mut output = File::create(output_path).map_err(|error| MediaError::new(MediaErrorCode::OutputCreateFailed, error.to_string()))?; let mut buffer = [0u8; COPY_BLOCK_BYTES]; loop { check_cancelled(job)?; let count = input.read(&mut buffer).map_err(|error| MediaError::new(MediaErrorCode::FileOpenFailed, error.to_string()))?; if count == 0 { break; } output.write_all(&buffer[..count]).map_err(|error| MediaError::new(MediaErrorCode::OutputWriteFailed, error.to_string()))?; } output.flush().map_err(|error| MediaError::new(MediaErrorCode::OutputWriteFailed, error.to_string()))?; check_cancelled(job) } /// Minimal streaming WebM writer for one mono Opus track. struct WebmOpusWriter { /// EBML writer owning the partial output file. writer: WebmWriter, /// Absolute timestamp of the current cluster in milliseconds. cluster_start_ms: Option, } impl WebmOpusWriter { /// Writes EBML, segment, info, and the single-track header. fn new(file: File) -> Result { let mut writer = WebmWriter::new(file); write_tags(&mut writer, &[ MatroskaSpec::Ebml(Master::Start), MatroskaSpec::EbmlVersion(1), MatroskaSpec::EbmlReadVersion(1), MatroskaSpec::EbmlMaxIdLength(4), MatroskaSpec::EbmlMaxSizeLength(8), MatroskaSpec::DocType("webm".to_string()), MatroskaSpec::DocTypeVersion(4), MatroskaSpec::DocTypeReadVersion(2), MatroskaSpec::Ebml(Master::End), ])?; writer.write_advanced(&MatroskaSpec::Segment(Master::Start), WriteOptions::is_unknown_sized_element()).map_err(webm_error)?; write_tags(&mut writer, &[ MatroskaSpec::Info(Master::Start), MatroskaSpec::TimestampScale(1_000_000), MatroskaSpec::MuxingApp("MindWork AI Studio".to_string()), MatroskaSpec::WritingApp("MindWork AI Studio".to_string()), MatroskaSpec::Info(Master::End), MatroskaSpec::Tracks(Master::Start), MatroskaSpec::TrackEntry(Master::Start), MatroskaSpec::TrackNumber(1), MatroskaSpec::TrackUID(1), MatroskaSpec::TrackType(2), MatroskaSpec::FlagDefault(1), MatroskaSpec::CodecID("A_OPUS".to_string()), MatroskaSpec::CodecPrivate(opus_head()), MatroskaSpec::CodecDelay(u64::from(OPUS_PRE_SKIP) * 1_000_000_000 / u64::from(OUTPUT_SAMPLE_RATE)), MatroskaSpec::SeekPreRoll(80_000_000), MatroskaSpec::Audio(Master::Start), MatroskaSpec::SamplingFrequency(f64::from(OUTPUT_SAMPLE_RATE)), MatroskaSpec::Channels(1), MatroskaSpec::Audio(Master::End), MatroskaSpec::TrackEntry(Master::End), MatroskaSpec::Tracks(Master::End), ])?; Ok(Self { writer, cluster_start_ms: None }) } /// Writes one Opus packet and rotates clusters before timestamp overflow. fn write_packet(&mut self, packet: &[u8], sample_position: u64) -> Result<(), MediaError> { let timestamp_ms = sample_position.saturating_mul(1000) / u64::from(OUTPUT_SAMPLE_RATE); let rotate = self.cluster_start_ms.map(|start| timestamp_ms.saturating_sub(start) >= CLUSTER_DURATION_MS).unwrap_or(true); if rotate { if self.cluster_start_ms.is_some() { self.writer.write(&MatroskaSpec::Cluster(Master::End)).map_err(webm_error)?; } self.writer.write(&MatroskaSpec::Cluster(Master::Start)).map_err(webm_error)?; self.writer.write(&MatroskaSpec::Timestamp(timestamp_ms)).map_err(webm_error)?; self.cluster_start_ms = Some(timestamp_ms); } let relative = timestamp_ms.saturating_sub(self.cluster_start_ms.unwrap_or(timestamp_ms)); if relative > i16::MAX as u64 { return Err(MediaError::new(MediaErrorCode::WebmTimestampOverflow, "The WebM cluster timestamp exceeded its safe range.")); } let block: MatroskaSpec = SimpleBlock::new_uncheked(packet, 1, relative as i16, false, None, false, true).into(); self.writer.write(&block).map_err(webm_error) } /// Closes the active cluster and finalizes the segment and output file. fn finish(mut self) -> Result<(), MediaError> { if self.cluster_start_ms.is_some() { self.writer.write(&MatroskaSpec::Cluster(Master::End)).map_err(webm_error)?; } self.writer.write(&MatroskaSpec::Segment(Master::End)).map_err(webm_error)?; self.writer.into_inner().map_err(webm_error)?; Ok(()) } } /// Writes a sequence of Matroska tags with a consistent error mapping. fn write_tags(writer: &mut WebmWriter, tags: &[MatroskaSpec]) -> Result<(), MediaError> { for tag in tags { writer.write(tag).map_err(webm_error)?; } Ok(()) } /// Builds the mono 48-kHz output track's Opus identification header. fn opus_head() -> Vec { let mut data = b"OpusHead".to_vec(); data.push(1); data.push(1); data.extend_from_slice(&OPUS_PRE_SKIP.to_le_bytes()); data.extend_from_slice(&OUTPUT_SAMPLE_RATE.to_le_bytes()); data.extend_from_slice(&0i16.to_le_bytes()); data.push(0); data } /// Derives the operation-owned partial path adjacent to the final output. fn partial_path(output_path: &FilePath) -> PathBuf { let mut name = output_path.file_name().unwrap_or_default().to_os_string(); name.push(".partial"); output_path.with_file_name(name) } /// Checks the EBML document type rather than trusting the input extension. fn is_webm_container(path: &FilePath) -> bool { let Ok(file) = File::open(path) else { return false; }; WebmIterator::new(file, &[]).take(16).filter_map(Result::ok).any(|tag| { matches!(tag, MatroskaSpec::DocType(doc_type) if doc_type.eq_ignore_ascii_case("webm")) }) } /// Converts the cooperative cancellation flag to a stable terminal error. fn check_cancelled(job: &MediaJob) -> Result<(), MediaError> { if job.is_cancelled() { Err(MediaError::new(MediaErrorCode::Cancelled, "The media job was cancelled.")) } else { Ok(()) } } /// Maps probe failures to stable public media error categories. fn map_probe_error(error: SymphoniaError) -> MediaError { match error { SymphoniaError::Unsupported(_) => MediaError::new(MediaErrorCode::UnsupportedContainer, "This media container or codec is not supported."), _ => MediaError::new(MediaErrorCode::DamagedContainer, format!("The media container could not be read: {error}")), } } /// Maps WebM writer failures to a stable public media error category. fn webm_error(error: impl std::fmt::Display) -> MediaError { MediaError::new(MediaErrorCode::WebmWriteFailed, format!("The WebM output could not be written: {error}")) } #[cfg(test)] mod tests { use super::*; use std::io::Cursor; use std::num::NonZeroU32; use tokio::sync::broadcast::error::TryRecvError; use symphonia::core::audio::{Channels, Position}; use symphonia::core::codecs::audio::well_known::{CODEC_ID_AC3, CODEC_ID_PCM_S16LE}; use symphonia::core::codecs::audio::AudioCodecParameters; /// Returns the checked-in, FFmpeg-free-at-test-time media fixture directory. fn fixtures() -> PathBuf { FilePath::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/media") } /// Creates a temporary output and normalizes one checked-in fixture. fn normalize_fixture(name: &str) -> Result<(MediaJobResult, PathBuf), MediaError> { let output = std::env::temp_dir().join(format!("ai-studio-fixture-{}.webm", rand::random::())); let result = normalize_media(&fixtures().join(name), &output, DEFAULT_MAX_PASS_THROUGH_BYTES, &MediaJob::new())?; Ok((result, output)) } /// Verifies the generated output identification header. #[test] fn opus_head_describes_48_khz_mono() { let head = opus_head(); assert_eq!(&head[..8], b"OpusHead"); assert_eq!(head[9], 1); assert_eq!(u32::from_le_bytes(head[12..16].try_into().unwrap()), 48_000); } /// Verifies both single-stream channel layouts accepted by the adapter. #[test] fn opus_adapter_accepts_single_stream_mono_and_stereo_headers() { for channels in [1, 2] { let mut head = opus_head(); head[9] = channels; let parsed = OpusHeader::parse(&head).unwrap(); assert_eq!(parsed.channels, usize::from(channels)); assert_eq!(parsed.pre_skip, OPUS_PRE_SKIP); } } /// Verifies unsupported multistream mappings retain their stable code. #[test] fn opus_adapter_rejects_multistream_mapping_with_stable_code() { let mut head = opus_head(); head[18] = 1; assert_eq!(OpusHeader::parse(&head).unwrap_err().code, MediaErrorCode::UnsupportedOpusMapping); } /// Verifies a decodable default track wins over an earlier fallback. #[test] fn track_selection_prefers_decodable_default_audio() { let mut first_params = AudioCodecParameters::new(); first_params.for_codec(CODEC_ID_PCM_S16LE).with_sample_rate(48_000).with_channels(Channels::Positioned(Position::FRONT_LEFT)); let mut default_params = first_params.clone(); default_params.for_codec(CODEC_ID_PCM_S16LE); let mut first = Track::new(1); first.with_codec_params(CodecParameters::Audio(first_params)); let mut preferred = Track::new(2); preferred.with_codec_params(CodecParameters::Audio(default_params)).with_flags(TrackFlags::DEFAULT); assert_eq!(select_audio_track(&[first, preferred]).unwrap().id, 2); } /// Verifies an undecodable default track does not mask a usable fallback. #[test] fn track_selection_skips_undecodable_default_audio() { let mut unsupported = AudioCodecParameters::new(); unsupported.for_codec(CODEC_ID_AC3).with_sample_rate(48_000).with_channels(Channels::Positioned(Position::FRONT_LEFT)); let mut supported = AudioCodecParameters::new(); supported.for_codec(CODEC_ID_PCM_S16LE).with_sample_rate(48_000).with_channels(Channels::Positioned(Position::FRONT_LEFT)); let mut default = Track::new(1); default.with_codec_params(CodecParameters::Audio(unsupported)).with_flags(TrackFlags::DEFAULT); let mut fallback = Track::new(2); fallback.with_codec_params(CodecParameters::Audio(supported)); assert_eq!(select_audio_track(&[default, fallback]).unwrap().id, 2); } /// Verifies long output rotates clusters before signed relative timestamps overflow. #[test] fn webm_writer_rotates_clusters_before_relative_timestamp_overflow() { let path = std::env::temp_dir().join(format!("ai-studio-media-writer-{}.webm", rand::random::())); let file = File::create(&path).unwrap(); let mut writer = WebmOpusWriter::new(file).unwrap(); writer.write_packet(&[0xf8, 0xff, 0xfe], 0).unwrap(); writer.write_packet(&[0xf8, 0xff, 0xfe], 31 * 48_000).unwrap(); writer.finish().unwrap(); let bytes = fs::read(&path).unwrap(); let clusters = WebmIterator::new(Cursor::new(bytes), &[]) .filter_map(Result::ok) .filter(|tag| matches!(tag, MatroskaSpec::Cluster(Master::Start))) .count(); let _ = fs::remove_file(path); assert_eq!(clusters, 2); } /// Verifies deterministic arithmetic-mean downmixing. #[test] fn downmix_is_bounded_and_balanced() { assert_eq!(downmix_to_mono(&[1.0, -1.0, 0.5, 0.5], 2), vec![0.0, 0.5]); } /// Verifies the configured dBFS ceiling is inclusive and a higher peak is audible. #[test] fn practical_silence_uses_the_configured_maximum_peak() { let threshold = 10.0_f32.powf(SILENCE_MAX_PEAK_DBFS / 20.0); let mut detector = AudioPeakDetector::default(); detector.observe(&[-threshold, threshold]); assert!(!detector.has_audible_signal()); detector.observe(&[threshold * 1.01]); assert!(detector.has_audible_signal()); } /// Verifies timestamp gaps become silence and overlaps do not duplicate decoded samples. #[test] fn timestamp_alignment_inserts_gaps_and_trims_overlaps() { let mut discontinuities = 0; let mut gap = vec![1.0; 960]; append_timestamp_aligned(&mut gap, &vec![2.0; 960], 960, Some(1_920), 7, 40, &mut discontinuities); assert_eq!(gap.len(), 2_880); assert!(gap[960..1_920].iter().all(|sample| *sample == 0.0)); assert!(gap[1_920..].iter().all(|sample| *sample == 2.0)); let mut overlap = vec![1.0; 960]; append_timestamp_aligned(&mut overlap, &vec![2.0; 960], 960, Some(480), 7, 10, &mut discontinuities); assert_eq!(overlap.len(), 1_440); assert!(overlap[960..].iter().all(|sample| *sample == 2.0)); } /// Verifies packet progress uses a selected-track-relative time axis. #[test] fn packet_timestamps_are_relative_to_the_first_audio_packet() { let time_base = TimeBase::new(NonZeroU32::new(1).unwrap(), NonZeroU32::new(1_000).unwrap()); assert_eq!(packet_timestamp_ms(5_250, Some(5_000), Some(time_base)), Some(250)); assert_eq!(packet_output_start(5_250, Some(5_000), Some(time_base)), Some(12_000)); } /// Verifies running updates are throttled while a phase transition remains immediate. #[test] fn running_progress_is_throttled_but_phase_changes_are_immediate() { let job = MediaJob::new(); let mut events = job.events.subscribe(); job.publish_progress(MediaJobPhase::Transcoding, Some(0.1)); job.publish_progress(MediaJobPhase::Transcoding, Some(0.2)); job.publish_progress(MediaJobPhase::Probing, Some(0.0)); assert_eq!(events.try_recv().unwrap().progress, Some(0.1)); assert_eq!(events.try_recv().unwrap().phase, MediaJobPhase::Probing); assert!(matches!(events.try_recv(), Err(TryRecvError::Empty))); } /// Verifies cancellation interrupts source reads rather than waiting for another packet. #[test] fn cancellation_aware_source_interrupts_reads() { let path = std::env::temp_dir().join(format!("ai-studio-source-cancel-{}", rand::random::())); fs::write(&path, vec![0u8; COPY_BLOCK_BYTES * 2]).unwrap(); let cancelled = Arc::new(AtomicBool::new(false)); let (mut source, _) = CancellationMediaSource::new(File::open(&path).unwrap(), Arc::clone(&cancelled)).unwrap(); cancelled.store(true, Ordering::Relaxed); let error = source.read(&mut [0u8; 16]).unwrap_err(); let _ = fs::remove_file(path); assert_eq!(error.kind(), std::io::ErrorKind::Interrupted); } /// Verifies output shape and at-most-one-frame duration rounding. #[test] fn wav_is_normalized_to_one_mono_opus_track_with_frame_bounded_duration() { let directory = std::env::temp_dir().join(format!("ai-studio-media-test-{}", rand::random::())); fs::create_dir_all(&directory).unwrap(); let input = directory.join("input.wav"); let output = directory.join("output.webm"); fs::write(&input, wav_silence(44_100, 4_410)).unwrap(); let job = MediaJob::new(); let result = normalize_media(&input, &output, DEFAULT_MAX_PASS_THROUGH_BYTES, &job).unwrap(); assert!(!result.pass_through); assert_eq!(result.output_format, OUTPUT_FORMAT); assert_eq!(result.output_codec, OUTPUT_CODEC); assert!(!result.has_audible_signal); assert!(result.duration_ms.abs_diff(100) <= 20); let file = File::open(&output).unwrap(); let tags: Vec<_> = WebmIterator::new(file, &[]) .filter_map(Result::ok) .collect(); assert_eq!(tags.iter().filter(|tag| matches!(tag, MatroskaSpec::TrackEntry(Master::Start))).count(), 1); assert!(tags.iter().any(|tag| matches!(tag, MatroskaSpec::CodecID(codec) if codec == "A_OPUS"))); assert!(tags.iter().any(|tag| matches!(tag, MatroskaSpec::Channels(1)))); assert!(tags.iter().any(|tag| matches!(tag, MatroskaSpec::SamplingFrequency(rate) if *rate == 48_000.0))); let _ = fs::remove_dir_all(directory); } /// Verifies cancellation removes both final and partial outputs. #[test] fn cancellation_does_not_leave_an_output_file() { let directory = std::env::temp_dir().join(format!("ai-studio-media-cancel-{}", rand::random::())); fs::create_dir_all(&directory).unwrap(); let input = directory.join("input.wav"); let output = directory.join("output.webm"); fs::write(&input, wav_silence(48_000, 960)).unwrap(); let job = MediaJob::new(); job.cancelled.store(true, Ordering::Relaxed); let error = normalize_media(&input, &output, DEFAULT_MAX_PASS_THROUGH_BYTES, &job).unwrap_err(); assert_eq!(error.code, MediaErrorCode::Cancelled); assert!(!output.exists()); assert!(!partial_path(&output).exists()); let _ = fs::remove_dir_all(directory); } /// Verifies an exactly compliant one-track WebM is copied unchanged. #[test] fn suitable_audio_only_webm_opus_is_passed_through() { let directory = std::env::temp_dir().join(format!("ai-studio-media-pass-through-{}", rand::random::())); fs::create_dir_all(&directory).unwrap(); let input = directory.join("input.webm"); let output = directory.join("output.webm"); let mut writer = WebmOpusWriter::new(File::create(&input).unwrap()).unwrap(); writer.write_packet(&[0xf8, 0xff, 0xfe], 0).unwrap(); writer.finish().unwrap(); let job = MediaJob::new(); let result = normalize_media(&input, &output, DEFAULT_MAX_PASS_THROUGH_BYTES, &job).unwrap(); assert!(result.pass_through); assert_eq!(result.output_format, OUTPUT_FORMAT); assert_eq!(result.output_codec, OUTPUT_CODEC); assert_eq!(fs::read(input).unwrap(), fs::read(output).unwrap()); assert!(!result.has_audible_signal); let _ = fs::remove_dir_all(directory); } /// Verifies an above-threshold PCM peak survives normalization classification. #[test] fn audible_wav_is_not_classified_as_silence() { let directory = std::env::temp_dir().join(format!("ai-studio-media-audible-{}", rand::random::())); fs::create_dir_all(&directory).unwrap(); let input = directory.join("input.wav"); let output = directory.join("output.webm"); fs::write(&input, wav_constant(48_000, 960, 1_000)).unwrap(); let result = normalize_media(&input, &output, DEFAULT_MAX_PASS_THROUGH_BYTES, &MediaJob::new()).unwrap(); assert!(result.has_audible_signal); let _ = fs::remove_dir_all(directory); } /// Exercises every checked-in supported audio container without FFmpeg at test time. #[test] fn checked_in_audio_fixtures_normalize_without_external_tools() { for name in [ "sample.m4a", "sample.mov", "sample.mp4", "sample.mkv", "sample.ogg", "sample.mp3", "sample.flac", "sample.wav", "sample.aiff", "sample.caf", ] { let (result, output) = normalize_fixture(name).unwrap_or_else(|error| panic!("{name}: {error:?}")); assert!(result.duration_ms > 0 && result.duration_ms <= 200, "{name}: {} ms", result.duration_ms); assert!(output.is_file(), "{name}"); let _ = fs::remove_file(output); } } /// Verifies video and subtitle tracks independently disable pass-through. #[test] fn pass_through_requires_exactly_one_audio_track() { let (audio_only, audio_output) = normalize_fixture("audio-only.webm").unwrap(); assert!(audio_only.pass_through); let _ = fs::remove_file(audio_output); let (video, video_output) = normalize_fixture("video.webm").unwrap(); assert!(!video.pass_through); let _ = fs::remove_file(video_output); let (subtitle, subtitle_output) = normalize_fixture("subtitle.webm").unwrap(); assert!(!subtitle.pass_through); let _ = fs::remove_file(subtitle_output); } /// Verifies malformed, audio-less, and unknown-codec fixtures return stable categories. #[test] fn fixture_errors_are_stable() { let damaged_output = std::env::temp_dir().join(format!("ai-studio-damaged-{}.webm", rand::random::())); let damaged = normalize_media(&fixtures().join("damaged.bin"), &damaged_output, DEFAULT_MAX_PASS_THROUGH_BYTES, &MediaJob::new()).unwrap_err(); assert!(matches!(damaged.code, MediaErrorCode::UnknownFormat | MediaErrorCode::NotMedia | MediaErrorCode::DamagedContainer)); let no_audio_output = std::env::temp_dir().join(format!("ai-studio-no-audio-{}.webm", rand::random::())); let no_audio = normalize_media(&fixtures().join("no-audio.webm"), &no_audio_output, DEFAULT_MAX_PASS_THROUGH_BYTES, &MediaJob::new()).unwrap_err(); assert_eq!(no_audio.code, MediaErrorCode::NoAudioTrack); let unknown_output = std::env::temp_dir().join(format!("ai-studio-unknown-{}.webm", rand::random::())); let unknown = normalize_media(&fixtures().join("unknown-codec.mkv"), &unknown_output, DEFAULT_MAX_PASS_THROUGH_BYTES, &MediaJob::new()).unwrap_err(); assert_eq!(unknown.code, MediaErrorCode::UnsupportedCodec); } /// Verifies long streaming input never grows the resampler's pending buffer unboundedly. #[test] fn long_stream_resampling_keeps_pending_input_bounded() { let mut resampler = StreamResampler::new(44_100).unwrap(); let chunk = vec![0.0; 441]; let mut produced = 0usize; for _ in 0..6_000 { produced += resampler.push(&chunk).unwrap().len(); if let StreamResampler::Rubato { inner, pending, .. } = &resampler { assert!(pending.len() < inner.input_frames_next()); } } produced += resampler.finish().unwrap().len(); assert_eq!(produced, 2_880_000); } /// Constructs a minimal mono 16-bit PCM WAV fixture in memory. fn wav_silence(sample_rate: u32, samples: u32) -> Vec { wav_constant(sample_rate, samples, 0) } /// Constructs a minimal mono 16-bit PCM WAV containing one constant sample value. fn wav_constant(sample_rate: u32, samples: u32, sample: i16) -> Vec { let data_size = samples * 2; let mut wav = Vec::with_capacity(44 + data_size as usize); wav.extend_from_slice(b"RIFF"); wav.extend_from_slice(&(36 + data_size).to_le_bytes()); wav.extend_from_slice(b"WAVEfmt "); wav.extend_from_slice(&16u32.to_le_bytes()); wav.extend_from_slice(&1u16.to_le_bytes()); wav.extend_from_slice(&1u16.to_le_bytes()); wav.extend_from_slice(&sample_rate.to_le_bytes()); wav.extend_from_slice(&(sample_rate * 2).to_le_bytes()); wav.extend_from_slice(&2u16.to_le_bytes()); wav.extend_from_slice(&16u16.to_le_bytes()); wav.extend_from_slice(b"data"); wav.extend_from_slice(&data_size.to_le_bytes()); for _ in 0..samples { wav.extend_from_slice(&sample.to_le_bytes()); } wav } }