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
13/// Why an ffmpeg transcode failed, so the executor can treat a full scratch
14/// disk as a systemic abort rather than a skippable per-clip fault.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum FfmpegErrorKind {
17    /// The scratch device or quota ran out of space.
18    OutOfSpace,
19    /// Any other failure (bad input, missing binary, encode error).
20    Other,
21}
22
23/// An ffmpeg transcode failure, carrying a kind and a human-readable,
24/// secret-free reason.
25#[derive(Debug, thiserror::Error)]
26#[error("{reason}")]
27pub struct FfmpegError {
28    kind: FfmpegErrorKind,
29    reason: String,
30}
31
32impl FfmpegError {
33    /// Build an [`FfmpegError`] of kind [`FfmpegErrorKind::Other`] from any
34    /// displayable cause.
35    pub fn new(reason: impl Into<String>) -> Self {
36        Self {
37            kind: FfmpegErrorKind::Other,
38            reason: reason.into(),
39        }
40    }
41
42    /// Build an out-of-space [`FfmpegError`] (kind [`FfmpegErrorKind::OutOfSpace`]).
43    pub fn out_of_space(reason: impl Into<String>) -> Self {
44        Self {
45            kind: FfmpegErrorKind::OutOfSpace,
46            reason: reason.into(),
47        }
48    }
49
50    /// The failure kind.
51    pub fn kind(&self) -> FfmpegErrorKind {
52        self.kind
53    }
54
55    /// Whether this failure was a full scratch disk or exhausted quota.
56    pub fn is_out_of_space(&self) -> bool {
57        self.kind == FfmpegErrorKind::OutOfSpace
58    }
59}
60
61/// Encoder settings for the animated WebP cover derived from a clip's MP4
62/// preview.
63///
64/// The [`Default`] targets a small, broadly compatible file: a couple of
65/// megabytes, well under the 25 MB ceiling some players (e.g. Symfonium) place
66/// on embedded/sidecar art. A single hardcoded default is used this phase behind
67/// one `--animated-covers` toggle; per-knob tuning is deliberately not surfaced
68/// on the CLI.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub struct WebpEncodeSettings {
71    /// Lossy encoder quality, 0-100 (higher is better and larger). Ignored when
72    /// `lossless` is set.
73    pub quality: u8,
74    /// Cap on the output frame rate; a faster source is downsampled to this.
75    pub max_fps: u32,
76    /// Cap on the output width in pixels; a wider source scales down keeping its
77    /// aspect ratio, and a narrower one is never upscaled.
78    pub max_width: u32,
79    /// Encode losslessly (much larger); off by default.
80    pub lossless: bool,
81    /// Spend extra effort compressing (smaller file, slower encode); on by
82    /// default.
83    pub compression: bool,
84}
85
86impl Default for WebpEncodeSettings {
87    fn default() -> Self {
88        Self {
89            quality: 70,
90            max_fps: 24,
91            max_width: 720,
92            lossless: false,
93            compression: true,
94        }
95    }
96}
97
98/// The ffmpeg port the executor transcodes through.
99///
100/// Async so the adapter can offload the blocking child process without stalling
101/// the runtime; tests resolve immediately.
102pub trait Ffmpeg {
103    /// Transcode `wav` to FLAC bytes.
104    fn wav_to_flac(&self, wav: &[u8]) -> impl Future<Output = Result<Vec<u8>, FfmpegError>> + Send;
105
106    /// Transcode an MP4 video preview to animated WebP bytes under `settings`.
107    ///
108    /// Used to derive a clip's `cover.webp` sidecar from its `video_cover_url`
109    /// MP4. Like [`wav_to_flac`](Ffmpeg::wav_to_flac) the adapter offloads the
110    /// blocking child process; tests resolve immediately.
111    fn mp4_to_webp(
112        &self,
113        mp4: &[u8],
114        settings: WebpEncodeSettings,
115    ) -> impl Future<Output = Result<Vec<u8>, FfmpegError>> + Send;
116}