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/// An ffmpeg transcode failure, carrying a human-readable, secret-free reason.
14#[derive(Debug, thiserror::Error)]
15#[error("{0}")]
16pub struct FfmpegError(pub String);
17
18impl FfmpegError {
19    /// Build an [`FfmpegError`] from any displayable cause.
20    pub fn new(reason: impl Into<String>) -> Self {
21        Self(reason.into())
22    }
23}
24
25/// Encoder settings for the animated WebP cover derived from a clip's MP4
26/// preview.
27///
28/// The [`Default`] targets a small, broadly compatible file: a couple of
29/// megabytes, well under the 25 MB ceiling some players (e.g. Symfonium) place
30/// on embedded/sidecar art. A single hardcoded default is used this phase behind
31/// one `--animated-covers` toggle; per-knob tuning is deliberately not surfaced
32/// on the CLI.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct WebpEncodeSettings {
35    /// Lossy encoder quality, 0-100 (higher is better and larger). Ignored when
36    /// `lossless` is set.
37    pub quality: u8,
38    /// Cap on the output frame rate; a faster source is downsampled to this.
39    pub max_fps: u32,
40    /// Cap on the output width in pixels; a wider source scales down keeping its
41    /// aspect ratio, and a narrower one is never upscaled.
42    pub max_width: u32,
43    /// Encode losslessly (much larger); off by default.
44    pub lossless: bool,
45    /// Spend extra effort compressing (smaller file, slower encode); on by
46    /// default.
47    pub compression: bool,
48}
49
50impl Default for WebpEncodeSettings {
51    fn default() -> Self {
52        Self {
53            quality: 70,
54            max_fps: 24,
55            max_width: 720,
56            lossless: false,
57            compression: true,
58        }
59    }
60}
61
62/// The ffmpeg port the executor transcodes through.
63///
64/// Async so the adapter can offload the blocking child process without stalling
65/// the runtime; tests resolve immediately.
66pub trait Ffmpeg {
67    /// Transcode `wav` to FLAC bytes.
68    fn wav_to_flac(&self, wav: &[u8]) -> impl Future<Output = Result<Vec<u8>, FfmpegError>> + Send;
69
70    /// Transcode an MP4 video preview to animated WebP bytes under `settings`.
71    ///
72    /// Used to derive a clip's `cover.webp` sidecar from its `video_cover_url`
73    /// MP4. Like [`wav_to_flac`](Ffmpeg::wav_to_flac) the adapter offloads the
74    /// blocking child process; tests resolve immediately.
75    fn mp4_to_webp(
76        &self,
77        mp4: &[u8],
78        settings: WebpEncodeSettings,
79    ) -> impl Future<Output = Result<Vec<u8>, FfmpegError>> + Send;
80}