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