Skip to main content

suno_core/
ffmpeg.rs

1//! The ffmpeg port: transcode WAV bytes to FLAC bytes, and MP4 video previews
2//! to animated WebP cover bytes.
3//!
4//! The lossless download path renders a clip to WAV, then re-encodes it to
5//! FLAC. The animated-cover path fetches a clip's MP4 preview and re-encodes it
6//! to a small looping WebP. Both are the engine's only calls into ffmpeg, so
7//! they sit behind this trait: the CLI adapter wraps a child process (with a
8//! hard timeout), while tests use a stub that returns canned bytes. The steps
9//! only re-encode media; tagging is the pure tagger's job.
10
11use std::future::Future;
12
13use crate::config::AudioFormat;
14
15/// Why an ffmpeg transcode failed, so the executor can treat a full scratch
16/// disk as a systemic abort rather than a skippable per-clip fault.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum FfmpegErrorKind {
19    /// The scratch device or quota ran out of space.
20    OutOfSpace,
21    /// Any other failure (bad input, missing binary, encode error).
22    Other,
23}
24
25/// An ffmpeg transcode failure, carrying a kind and a human-readable,
26/// secret-free reason.
27#[derive(Debug, thiserror::Error)]
28#[error("{reason}")]
29pub struct FfmpegError {
30    kind: FfmpegErrorKind,
31    reason: String,
32}
33
34impl FfmpegError {
35    /// Build an [`FfmpegError`] of kind [`FfmpegErrorKind::Other`] from any
36    /// displayable cause.
37    pub fn new(reason: impl Into<String>) -> Self {
38        Self {
39            kind: FfmpegErrorKind::Other,
40            reason: reason.into(),
41        }
42    }
43
44    /// Build an out-of-space [`FfmpegError`] (kind [`FfmpegErrorKind::OutOfSpace`]).
45    pub fn out_of_space(reason: impl Into<String>) -> Self {
46        Self {
47            kind: FfmpegErrorKind::OutOfSpace,
48            reason: reason.into(),
49        }
50    }
51
52    /// The failure kind.
53    pub fn kind(&self) -> FfmpegErrorKind {
54        self.kind
55    }
56
57    /// Whether this failure was a full scratch disk or exhausted quota.
58    pub fn is_out_of_space(&self) -> bool {
59        self.kind == FfmpegErrorKind::OutOfSpace
60    }
61}
62
63/// Encoder settings for the animated WebP cover derived from a clip's MP4
64/// preview.
65///
66/// The [`Default`] is visually transparent lossy: quality 95 at effort
67/// (`compression_level`) 4. Measurement on real Suno covers showed 95 reaches
68/// ~46 dB (indistinguishable from lossless for a cover) at roughly a fifth of
69/// the lossless size, and that effort 6 only matches effort 4's size for 7-13x
70/// the encode time, so effort is capped at 4. Lossless is opt-in and much
71/// larger (a 5 s cover is ~145 MB versus ~31 MB at quality 95).
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct WebpEncodeSettings {
74    /// Lossy encoder quality, 0-100 (higher is better and larger). Ignored when
75    /// `lossless` is set.
76    pub quality: u8,
77    /// Cap on the output frame rate; a faster source is downsampled to this.
78    pub max_fps: u32,
79    /// Optional cap on the output width in pixels: `Some(w)` scales a wider
80    /// source down keeping its aspect ratio (never upscaling), while `None`
81    /// keeps the source resolution.
82    pub max_width: Option<u32>,
83    /// Encode losslessly. Off by default: lossless animated WebP of real video
84    /// is intrinsically huge (roughly 30x the lossy source) with no visible
85    /// gain over quality 95 for a cover.
86    pub lossless: bool,
87    /// Encoder effort, 0-4 (higher is smaller and slower). Capped at 4 because
88    /// effort 6 yields the same size for many times the encode time.
89    pub compression_level: u8,
90}
91
92impl Default for WebpEncodeSettings {
93    fn default() -> Self {
94        Self {
95            quality: 95,
96            max_fps: 24,
97            max_width: None,
98            lossless: false,
99            compression_level: 4,
100        }
101    }
102}
103
104/// The ffmpeg port the executor transcodes through.
105///
106/// Async so the adapter can offload the blocking child process without stalling
107/// the runtime; tests resolve immediately.
108pub trait Ffmpeg {
109    /// Transcode `wav` to the given lossless `format`'s bytes.
110    fn wav_to_lossless(
111        &self,
112        wav: &[u8],
113        format: AudioFormat,
114    ) -> impl Future<Output = Result<Vec<u8>, FfmpegError>> + Send;
115
116    /// Transcode an MP4 video preview to animated WebP bytes under `settings`.
117    ///
118    /// Used to derive a clip's `cover.webp` sidecar from its `video_cover_url`
119    /// MP4. Like [`wav_to_lossless`](Ffmpeg::wav_to_lossless) the adapter offloads
120    /// the blocking child process; tests resolve immediately.
121    fn mp4_to_webp(
122        &self,
123        mp4: &[u8],
124        settings: WebpEncodeSettings,
125    ) -> impl Future<Output = Result<Vec<u8>, FfmpegError>> + Send;
126}