vtx_sdk/modules/media/
ffmpeg.rs

1//! Host-side FFmpeg helpers.
2
3use crate::bindings::vtx::api::vtx_ffmpeg::{self, FfmpegOption, TranscodeProfile};
4use crate::bindings::vtx::api::vtx_types::HttpResponse;
5use crate::bindings::vtx::api::vtx_vfs::Buffer;
6use crate::error::{VtxError, VtxResult};
7
8/// Pure builder for `TranscodeProfile`.
9///
10/// This builder does not call any host APIs and can be tested locally.
11pub struct TranscodeProfileBuilder {
12    profile: String,
13    input_id: String,
14    options: Vec<FfmpegOption>,
15}
16
17impl TranscodeProfileBuilder {
18    /// Create a new profile builder.
19    ///
20    /// - `profile`: Target FFmpeg profile (e.g., "mini", "remux", "thumbnail").
21    /// - `input_id`: Input resource ID (UUID) or "pipe:0".
22    pub fn new(profile: impl Into<String>, input_id: impl Into<String>) -> Self {
23        Self {
24            profile: profile.into(),
25            input_id: input_id.into(),
26            options: Vec::new(),
27        }
28    }
29
30    /// Create a builder that uses stdin as input (`input_id = "pipe:0"`).
31    pub fn new_pipe(profile: impl Into<String>) -> Self {
32        Self::new(profile, "pipe:0")
33    }
34
35    /// Add a key/value FFmpeg option (encoded as `-key=value`).
36    pub fn option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
37        self.options.push(FfmpegOption {
38            key: key.into(),
39            value: Some(value.into()),
40        });
41        self
42    }
43
44    /// Add a flag-style FFmpeg option (encoded as `-key`).
45    pub fn flag(mut self, key: impl Into<String>) -> Self {
46        self.options.push(FfmpegOption {
47            key: key.into(),
48            value: None,
49        });
50        self
51    }
52
53    /// Add a batch of key/value options.
54    pub fn options<I, K, V>(mut self, options: I) -> Self
55    where
56        I: IntoIterator<Item = (K, V)>,
57        K: Into<String>,
58        V: Into<String>,
59    {
60        for (key, value) in options {
61            self.options.push(FfmpegOption {
62                key: key.into(),
63                value: Some(value.into()),
64            });
65        }
66        self
67    }
68
69    /// Helper: Set output format (equivalent to `-f=format`).
70    pub fn format(self, format: &str) -> Self {
71        self.option("f", format)
72    }
73
74    /// Helper: Set seek window (equivalent to `-ss` + optional `-t`).
75    pub fn seek(self, start: &str, duration: Option<&str>) -> Self {
76        let mut s = self.option("ss", start);
77        if let Some(d) = duration {
78            s = s.option("t", d);
79        }
80        s
81    }
82
83    /// Builds the `TranscodeProfile` payload.
84    pub fn build(self) -> TranscodeProfile {
85        TranscodeProfile {
86            profile: self.profile,
87            input_id: self.input_id,
88            options: self.options,
89        }
90    }
91}
92
93/// IO boundary for executing a `TranscodeProfile`.
94pub struct FfmpegExecutor;
95
96impl FfmpegExecutor {
97    /// Execute and return the stdout pipe buffer.
98    pub fn execute_buffer(profile: &TranscodeProfile) -> VtxResult<Buffer> {
99        vtx_ffmpeg::execute(profile).map_err(VtxError::from_host_message)
100    }
101
102    /// Execute and return an HTTP response (`200` with stdout pipe body).
103    pub fn execute(profile: &TranscodeProfile) -> VtxResult<HttpResponse> {
104        let buffer = Self::execute_buffer(profile)?;
105        Ok(HttpResponse {
106            status: 200,
107            body: Some(buffer),
108        })
109    }
110}
111
112/// FFmpeg task builder for running host-side transcoding.
113///
114/// # Example
115///
116/// ```rust
117/// use vtx_sdk::prelude::*;
118///
119/// fn handle_video(vid: String) -> VtxResult<Response> {
120///     FfmpegTask::new("mini", vid)
121///         .option("ss", "10")
122///         .option("t", "30")
123///         .execute()
124/// }
125/// ```
126pub struct FfmpegTask {
127    builder: TranscodeProfileBuilder,
128}
129
130impl FfmpegTask {
131    /// Create a new FFmpeg task.
132    ///
133    /// - `profile`: Target FFmpeg profile (e.g., "mini", "remux", "thumbnail").
134    /// - `input_id`: Input resource ID (UUID) or "pipe:0".
135    pub fn new(profile: impl Into<String>, input_id: impl Into<String>) -> Self {
136        Self {
137            builder: TranscodeProfileBuilder::new(profile, input_id),
138        }
139    }
140
141    /// Create a task that uses stdin as input (`input_id = "pipe:0"`).
142    pub fn new_pipe(profile: impl Into<String>) -> Self {
143        Self {
144            builder: TranscodeProfileBuilder::new_pipe(profile),
145        }
146    }
147
148    /// Add a key/value FFmpeg option (encoded as `-key=value`).
149    pub fn option(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
150        self.builder = self.builder.option(key, value);
151        self
152    }
153
154    /// Add a flag-style FFmpeg option (encoded as `-key`).
155    pub fn flag(mut self, key: impl Into<String>) -> Self {
156        self.builder = self.builder.flag(key);
157        self
158    }
159
160    /// Add a batch of key/value options.
161    pub fn options<I, K, V>(mut self, options: I) -> Self
162    where
163        I: IntoIterator<Item = (K, V)>,
164        K: Into<String>,
165        V: Into<String>,
166    {
167        self.builder = self.builder.options(options);
168        self
169    }
170
171    /// Helper: Set output format (equivalent to `-f=format`).
172    pub fn format(self, format: &str) -> Self {
173        self.option("f", format)
174    }
175
176    /// Helper: Set seek window (equivalent to `-ss` + optional `-t`).
177    pub fn seek(self, start: &str, duration: Option<&str>) -> Self {
178        let mut s = self.option("ss", start);
179        if let Some(d) = duration {
180            s = s.option("t", d);
181        }
182        s
183    }
184
185    /// Builds the `TranscodeProfile` payload.
186    pub fn build(self) -> TranscodeProfile {
187        self.builder.build()
188    }
189
190    /// Execute and return the stdout pipe buffer.
191    ///
192    /// This method performs an IO operation.
193    pub fn execute_buffer(self) -> VtxResult<Buffer> {
194        let profile = self.build();
195        FfmpegExecutor::execute_buffer(&profile)
196    }
197
198    /// Execute and return an HTTP response (`200` with stdout pipe body).
199    ///
200    /// This method performs an IO operation.
201    pub fn execute(self) -> VtxResult<HttpResponse> {
202        let profile = self.build();
203        FfmpegExecutor::execute(&profile)
204    }
205}