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`] turns encoder effort off (compression level 0) so a
67/// full-resolution clip encodes well under the ffmpeg timeout, since full
68/// effort can take minutes.
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 /// Optional cap on the output width in pixels: `Some(w)` scales a wider
77 /// source down keeping its aspect ratio (never upscaling), while `None`
78 /// keeps the source resolution.
79 pub max_width: Option<u32>,
80 /// Encode losslessly (much larger); off by default.
81 pub lossless: bool,
82 /// Encoder effort, 0-6 (higher is smaller and slower). `0` by default,
83 /// because full effort can take minutes on a full-resolution clip and
84 /// exceed the transcode timeout.
85 pub compression_level: u8,
86}
87
88impl Default for WebpEncodeSettings {
89 fn default() -> Self {
90 Self {
91 quality: 70,
92 max_fps: 24,
93 max_width: None,
94 lossless: false,
95 compression_level: 0,
96 }
97 }
98}
99
100/// The ffmpeg port the executor transcodes through.
101///
102/// Async so the adapter can offload the blocking child process without stalling
103/// the runtime; tests resolve immediately.
104pub trait Ffmpeg {
105 /// Transcode `wav` to the given lossless `format`'s bytes.
106 fn wav_to_lossless(
107 &self,
108 wav: &[u8],
109 format: AudioFormat,
110 ) -> impl Future<Output = Result<Vec<u8>, FfmpegError>> + Send;
111
112 /// Transcode an MP4 video preview to animated WebP bytes under `settings`.
113 ///
114 /// Used to derive a clip's `cover.webp` sidecar from its `video_cover_url`
115 /// MP4. Like [`wav_to_lossless`](Ffmpeg::wav_to_lossless) the adapter offloads
116 /// the blocking child process; tests resolve immediately.
117 fn mp4_to_webp(
118 &self,
119 mp4: &[u8],
120 settings: WebpEncodeSettings,
121 ) -> impl Future<Output = Result<Vec<u8>, FfmpegError>> + Send;
122}