Skip to main content

libobs_simple\output/
simple.rs

1//! Simple output builder for OBS.
2//!
3//! This module provides a simplified interface for configuring OBS outputs
4//! based on the SimpleOutput implementation from OBS Studio.
5//!
6//! # Example
7//!
8//! # Example
9//!
10//! ```no_run
11//! use libobs_simple::output::simple::{SimpleOutputBuilder, X264Preset};
12//! use libobs_simple::quick_start::quick_start;
13//! use libobs_wrapper::{context::ObsContext, utils::StartupInfo, data::video::ObsVideoInfoBuilder};
14//!
15//! #[tokio::main]
16//! async fn main() {
17//! let context = StartupInfo::new()
18//!     .set_video_info(
19//!           ObsVideoInfoBuilder::new()
20//!             // Configure video info as need
21//!             .build()
22//!      ).start()
23//!       .unwrap()
24//!     
25//!     let output = SimpleOutputBuilder::new(context, "./recording.mp4")
26//!         .video_bitrate(6000)
27//!         .audio_bitrate(160)
28//!         .x264_encoder(X264Preset::VeryFast)
29//!         .build()
30//!         .unwrap();
31//!
32//!     // Add sources here (for more docs, look [this](https://github.com/libobs-rs/libobs-rs/blob/main/examples/monitor-capture/src/main.rs) example
33//!
34//!     println!("Output created!");
35//! }
36//! ```
37
38use libobs_wrapper::{
39    context::ObsContext,
40    data::{
41        output::{ObsOutputRef, ObsOutputTrait},
42        ObsData, ObsDataSetters,
43    },
44    encoders::{ObsAudioEncoderType, ObsContextEncoders, ObsVideoEncoderType},
45    utils::{AudioEncoderInfo, ObsError, ObsPath, ObsString, OutputInfo, VideoEncoderInfo},
46};
47
48/// Preset for x264 software encoder
49#[derive(Debug, Clone, Copy)]
50pub enum X264Preset {
51    /// Ultrafast preset - lowest CPU usage, largest file size
52    UltraFast,
53    /// Superfast preset
54    SuperFast,
55    /// Veryfast preset (recommended default)
56    VeryFast,
57    /// Faster preset
58    Faster,
59    /// Fast preset - higher CPU usage, better quality
60    Fast,
61    /// Medium preset
62    Medium,
63    /// Slow preset
64    Slow,
65    /// Slower preset
66    Slower,
67}
68
69impl X264Preset {
70    pub fn as_str(&self) -> &'static str {
71        match self {
72            X264Preset::UltraFast => "ultrafast",
73            X264Preset::SuperFast => "superfast",
74            X264Preset::VeryFast => "veryfast",
75            X264Preset::Faster => "faster",
76            X264Preset::Fast => "fast",
77            X264Preset::Medium => "medium",
78            X264Preset::Slow => "slow",
79            X264Preset::Slower => "slower",
80        }
81    }
82}
83
84/// Preset for hardware encoders (NVENC, AMD, QSV)
85#[derive(Debug, Clone, Copy)]
86pub enum HardwarePreset {
87    /// Prioritize encoding speed over quality
88    Speed,
89    /// Balance between speed and quality
90    Balanced,
91    /// Prioritize quality over speed
92    Quality,
93}
94
95impl HardwarePreset {
96    pub fn as_str(&self) -> &'static str {
97        match self {
98            HardwarePreset::Speed => "speed",
99            HardwarePreset::Balanced => "balanced",
100            HardwarePreset::Quality => "quality",
101        }
102    }
103}
104
105/// Video encoder configuration
106#[derive(Debug, Clone)]
107pub enum VideoEncoder {
108    /// x264 software encoder
109    X264(X264Preset),
110    /// Hardware encoder (NVENC/AMF/QSV), codec chosen generically at runtime
111    Hardware {
112        codec: HardwareCodec,
113        preset: HardwarePreset,
114    },
115    /// Custom encoder by type
116    Custom(ObsVideoEncoderType),
117}
118
119/// Target codec for generic hardware selection
120#[derive(Debug, Clone, Copy)]
121pub enum HardwareCodec {
122    H264,
123    HEVC,
124    AV1,
125}
126
127/// Audio encoder configuration
128#[derive(Debug, Clone)]
129pub enum AudioEncoder {
130    /// AAC audio encoder (ffmpeg)
131    AAC,
132    /// Opus audio encoder
133    Opus,
134    /// Custom audio encoder by type
135    Custom(ObsAudioEncoderType),
136}
137
138/// Output format for file recording
139#[derive(Debug, Clone, Copy, Default)]
140pub enum OutputFormat {
141    /// .flv
142    FlashVideo,
143    /// .mkv
144    MatroskaVideo,
145    /// .mp4
146    Mpeg4,
147    /// .mov
148    QuickTime,
149    /// .mp4 (hybrid)
150    #[default]
151    HybridMP4,
152    /// .mov (hybrid)
153    HybridMov,
154    /// .mp4 (fragmented)
155    FragmentedMP4,
156    /// .mov (fragmented)
157    FragmentedMOV,
158    /// MPEG-TS .ts
159    MpegTs,
160}
161
162/// Unified output settings
163#[derive(Debug)]
164pub struct OutputSettings {
165    name: ObsString,
166    video_bitrate: u32,
167    audio_bitrate: u32,
168    video_encoder: VideoEncoder,
169    audio_encoder: AudioEncoder,
170    custom_encoder_settings: Option<String>,
171    path: ObsPath,
172    format: OutputFormat,
173    custom_muxer_settings: Option<String>,
174}
175
176impl OutputSettings {
177    /// Sets the video bitrate in Kbps.
178    pub fn with_video_bitrate(mut self, bitrate: u32) -> Self {
179        self.video_bitrate = bitrate;
180        self
181    }
182
183    /// Sets the audio bitrate in Kbps.
184    pub fn with_audio_bitrate(mut self, bitrate: u32) -> Self {
185        self.audio_bitrate = bitrate;
186        self
187    }
188
189    /// Sets the video encoder to use x264 software encoding.
190    pub fn with_x264_encoder(mut self, preset: X264Preset) -> Self {
191        self.video_encoder = VideoEncoder::X264(preset);
192        self
193    }
194
195    /// Sets the video encoder to use a generic hardware encoder for the given codec.
196    /// The builder will choose an available backend (NVENC/AMF/QSV) at runtime.
197    pub fn with_hardware_encoder(mut self, codec: HardwareCodec, preset: HardwarePreset) -> Self {
198        self.video_encoder = VideoEncoder::Hardware { codec, preset };
199        self
200    }
201
202    /// Sets a custom video encoder.
203    pub fn with_custom_video_encoder(mut self, encoder: ObsVideoEncoderType) -> Self {
204        self.video_encoder = VideoEncoder::Custom(encoder);
205        self
206    }
207
208    /// Sets custom x264 encoder settings.
209    pub fn with_custom_settings<S: Into<String>>(mut self, settings: S) -> Self {
210        self.custom_encoder_settings = Some(settings.into());
211        self
212    }
213
214    /// Sets the output path.
215    pub fn with_path<P: Into<ObsPath>>(mut self, path: P) -> Self {
216        self.path = path.into();
217        self
218    }
219
220    /// Sets the output format.
221    pub fn with_format(mut self, format: OutputFormat) -> Self {
222        self.format = format;
223        self
224    }
225
226    /// Sets custom muxer settings.
227    pub fn with_custom_muxer_settings<S: Into<String>>(mut self, settings: S) -> Self {
228        self.custom_muxer_settings = Some(settings.into());
229        self
230    }
231
232    /// Sets the audio encoder.
233    pub fn with_audio_encoder(mut self, encoder: AudioEncoder) -> Self {
234        self.audio_encoder = encoder;
235        self
236    }
237}
238
239#[derive(Debug)]
240pub struct SimpleOutputBuilder {
241    settings: OutputSettings,
242    context: ObsContext,
243}
244
245pub trait ObsContextSimpleExt {
246    fn simple_output_builder<K: Into<ObsPath>, T: Into<ObsString>>(
247        &self,
248        name: T,
249        path: K,
250    ) -> SimpleOutputBuilder;
251}
252
253impl ObsContextSimpleExt for ObsContext {
254    fn simple_output_builder<K: Into<ObsPath>, T: Into<ObsString>>(
255        &self,
256        name: T,
257        path: K,
258    ) -> SimpleOutputBuilder {
259        SimpleOutputBuilder::new(self.clone(), name, path)
260    }
261}
262
263impl SimpleOutputBuilder {
264    /// Creates a new SimpleOutputBuilder with default settings.
265    pub fn new<K: Into<ObsPath>, T: Into<ObsString>>(
266        context: ObsContext,
267        name: T,
268        path: K,
269    ) -> Self {
270        SimpleOutputBuilder {
271            settings: OutputSettings {
272                video_bitrate: 6000,
273                audio_bitrate: 160,
274                video_encoder: VideoEncoder::X264(X264Preset::VeryFast),
275                audio_encoder: AudioEncoder::AAC,
276                custom_encoder_settings: None,
277                path: path.into(),
278                format: OutputFormat::default(),
279                custom_muxer_settings: None,
280                name: name.into(),
281            },
282            context,
283        }
284    }
285
286    /// Sets the output settings.
287    pub fn settings(mut self, settings: OutputSettings) -> Self {
288        self.settings = settings;
289        self
290    }
291
292    /// Sets the video bitrate in Kbps.
293    pub fn video_bitrate(mut self, bitrate: u32) -> Self {
294        self.settings.video_bitrate = bitrate;
295        self
296    }
297
298    /// Sets the audio bitrate in Kbps.
299    pub fn audio_bitrate(mut self, bitrate: u32) -> Self {
300        self.settings.audio_bitrate = bitrate;
301        self
302    }
303
304    /// Sets the output path.
305    pub fn path<P: Into<ObsPath>>(mut self, path: P) -> Self {
306        self.settings.path = path.into();
307        self
308    }
309
310    /// Sets the output format.
311    pub fn format(mut self, format: OutputFormat) -> Self {
312        self.settings.format = format;
313        self
314    }
315
316    /// Sets the video encoder to x264.
317    pub fn x264_encoder(mut self, preset: X264Preset) -> Self {
318        self.settings.video_encoder = VideoEncoder::X264(preset);
319        self
320    }
321
322    /// Sets the video encoder to a generic hardware encoder.
323    pub fn hardware_encoder(mut self, codec: HardwareCodec, preset: HardwarePreset) -> Self {
324        self.settings.video_encoder = VideoEncoder::Hardware { codec, preset };
325        self
326    }
327
328    /// Builds and returns the configured output.
329    pub fn build(mut self) -> Result<ObsOutputRef, ObsError> {
330        // Determine the output type based on format
331        let output_id = match self.settings.format {
332            OutputFormat::HybridMP4 => "mp4_output",
333            OutputFormat::HybridMov => "mov_output",
334            _ => "ffmpeg_muxer",
335        };
336
337        // Create output settings
338        let mut output_settings = self.context.data()?;
339        output_settings.set_string("path", self.settings.path.clone().build())?;
340
341        if let Some(ref muxer_settings) = self.settings.custom_muxer_settings {
342            output_settings.set_string("muxer_settings", muxer_settings.as_str())?;
343        }
344
345        // Create the output
346        let output_info = OutputInfo::new(
347            output_id,
348            self.settings.name.clone(),
349            Some(output_settings),
350            None,
351        );
352
353        let mut output = self.context.output(output_info)?;
354
355        // Create and configure video encoder (with hardware fallback)
356        let video_encoder_type = self.select_video_encoder_type(&self.settings.video_encoder)?;
357        let mut video_settings = self.context.data()?;
358
359        self.configure_video_encoder(&mut video_settings)?;
360
361        let video_encoder_info = VideoEncoderInfo::new(
362            video_encoder_type,
363            format!("{}_video_encoder", self.settings.name),
364            Some(video_settings),
365            None,
366        );
367
368        output.create_and_set_video_encoder(video_encoder_info)?;
369
370        // Create and configure audio encoder
371        let audio_encoder_type = match &self.settings.audio_encoder {
372            AudioEncoder::AAC => ObsAudioEncoderType::FFMPEG_AAC,
373            AudioEncoder::Opus => ObsAudioEncoderType::FFMPEG_OPUS,
374            AudioEncoder::Custom(encoder_type) => encoder_type.clone(),
375        };
376
377        log::trace!("Selected audio encoder: {:?}", audio_encoder_type);
378        let mut audio_settings = self.context.data()?;
379        audio_settings.set_string("rate_control", "CBR")?;
380        audio_settings.set_int("bitrate", self.settings.audio_bitrate as i64)?;
381
382        let audio_encoder_info = AudioEncoderInfo::new(
383            audio_encoder_type,
384            format!("{}_audio_encoder", self.settings.name),
385            Some(audio_settings),
386            None,
387        );
388
389        log::trace!("Creating audio encoder with info: {:?}", audio_encoder_info);
390        output.create_and_set_audio_encoder(audio_encoder_info, 0)?;
391
392        Ok(output)
393    }
394
395    fn select_video_encoder_type(
396        &self,
397        encoder: &VideoEncoder,
398    ) -> Result<ObsVideoEncoderType, ObsError> {
399        match encoder {
400            VideoEncoder::X264(_) => Ok(ObsVideoEncoderType::OBS_X264),
401            VideoEncoder::Custom(t) => Ok(t.clone()),
402            VideoEncoder::Hardware { codec, .. } => {
403                // Build preferred candidates for the requested codec
404                let candidates = self.hardware_candidates(*codec);
405                // Query available encoders
406                let available = self
407                    .context
408                    .available_video_encoders()?
409                    .into_iter()
410                    .map(|b| b.get_encoder_id().clone())
411                    .collect::<Vec<_>>();
412                // Pick first preferred candidate that is available
413                for cand in candidates {
414                    if available.iter().any(|a| a == &cand) {
415                        return Ok(cand);
416                    }
417                }
418                // Fallback to x264 if no hardware encoder is available
419                Ok(ObsVideoEncoderType::OBS_X264)
420            }
421        }
422    }
423
424    fn hardware_candidates(&self, codec: HardwareCodec) -> Vec<ObsVideoEncoderType> {
425        match codec {
426            HardwareCodec::H264 => vec![
427                ObsVideoEncoderType::OBS_NVENC_H264_TEX,
428                ObsVideoEncoderType::H264_TEXTURE_AMF,
429                ObsVideoEncoderType::OBS_QSV11_V2,
430                // software fallbacks for vendor SDKs
431                ObsVideoEncoderType::OBS_NVENC_H264_SOFT,
432                ObsVideoEncoderType::OBS_QSV11_SOFT_V2,
433            ],
434            HardwareCodec::HEVC => vec![
435                ObsVideoEncoderType::OBS_NVENC_HEVC_TEX,
436                ObsVideoEncoderType::H265_TEXTURE_AMF,
437                ObsVideoEncoderType::OBS_QSV11_HEVC,
438                ObsVideoEncoderType::OBS_NVENC_HEVC_SOFT,
439                ObsVideoEncoderType::OBS_QSV11_HEVC_SOFT,
440            ],
441            HardwareCodec::AV1 => vec![
442                ObsVideoEncoderType::OBS_NVENC_AV1_TEX,
443                ObsVideoEncoderType::AV1_TEXTURE_AMF,
444                ObsVideoEncoderType::OBS_QSV11_AV1,
445                ObsVideoEncoderType::OBS_NVENC_AV1_SOFT,
446                ObsVideoEncoderType::OBS_QSV11_AV1_SOFT,
447            ],
448        }
449    }
450
451    fn get_encoder_preset(&self, encoder: &VideoEncoder) -> Option<&str> {
452        match encoder {
453            VideoEncoder::X264(preset) => Some(preset.as_str()),
454            VideoEncoder::Hardware { preset, .. } => Some(preset.as_str()),
455            VideoEncoder::Custom(_) => None,
456        }
457    }
458
459    fn configure_video_encoder(&self, settings: &mut ObsData) -> Result<(), ObsError> {
460        // Set rate control to CBR
461        settings.set_string("rate_control", "CBR")?;
462        settings.set_int("bitrate", self.settings.video_bitrate as i64)?;
463
464        // Set preset if available
465        if let Some(preset) = self.get_encoder_preset(&self.settings.video_encoder) {
466            settings.set_string("preset", preset)?;
467        }
468
469        // Apply custom encoder settings if provided (mainly for x264)
470        if let Some(ref custom) = self.settings.custom_encoder_settings {
471            settings.set_string("x264opts", custom.as_str())?;
472        }
473
474        Ok(())
475    }
476}