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`] turns encoder effort off so a full-resolution clip encodes
65/// well under the ffmpeg timeout, since full effort can take minutes. The file
66/// is larger as a result, but stays under the 25 MB ceiling some players such
67/// as Symfonium place on embedded/sidecar art. A single hardcoded default is
68/// used this phase behind one `--animated-covers` toggle; per-knob tuning is
69/// deliberately not surfaced on the CLI.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct WebpEncodeSettings {
72 /// Lossy encoder quality, 0-100 (higher is better and larger). Ignored when
73 /// `lossless` is set.
74 pub quality: u8,
75 /// Cap on the output frame rate; a faster source is downsampled to this.
76 pub max_fps: u32,
77 /// Optional cap on the output width in pixels: `Some(w)` scales a wider
78 /// source down keeping its aspect ratio (never upscaling), while `None`
79 /// keeps the source resolution.
80 pub max_width: Option<u32>,
81 /// Encode losslessly (much larger); off by default.
82 pub lossless: bool,
83 /// Spend extra encoder effort: a smaller file, but far slower. Off by
84 /// default, because `libwebp_anim` at full effort can take minutes on a
85 /// full-resolution clip and exceed the transcode timeout.
86 pub compression: bool,
87}
88
89impl Default for WebpEncodeSettings {
90 fn default() -> Self {
91 Self {
92 quality: 70,
93 max_fps: 24,
94 max_width: None,
95 lossless: false,
96 compression: false,
97 }
98 }
99}
100
101/// The ffmpeg port the executor transcodes through.
102///
103/// Async so the adapter can offload the blocking child process without stalling
104/// the runtime; tests resolve immediately.
105pub trait Ffmpeg {
106 /// Transcode `wav` to FLAC bytes.
107 fn wav_to_flac(&self, wav: &[u8]) -> impl Future<Output = Result<Vec<u8>, FfmpegError>> + Send;
108
109 /// Transcode an MP4 video preview to animated WebP bytes under `settings`.
110 ///
111 /// Used to derive a clip's `cover.webp` sidecar from its `video_cover_url`
112 /// MP4. Like [`wav_to_flac`](Ffmpeg::wav_to_flac) the adapter offloads the
113 /// blocking child process; tests resolve immediately.
114 fn mp4_to_webp(
115 &self,
116 mp4: &[u8],
117 settings: WebpEncodeSettings,
118 ) -> impl Future<Output = Result<Vec<u8>, FfmpegError>> + Send;
119}