Skip to main content

libobs_simple\output/
replay.rs

1//! Replay buffer builder for OBS.
2//!
3//! This module provides a simplified interface for configuring OBS replay buffers.
4//! A replay buffer continuously records the last N seconds of content, allowing
5//! on-demand saving of recent footage.
6//!
7//! # Example
8//!
9//! ```no_run
10//! use libobs_simple::output::replay::ReplayBufferBuilder;
11//! use libobs_wrapper::{context::ObsContext, utils::StartupInfo, data::video::ObsVideoInfoBuilder};
12//!
13//! #[tokio::main]
14//! async fn main() {
15//!     let context = StartupInfo::new()
16//!         .set_video_info(
17//!             ObsVideoInfoBuilder::new()
18//!                 // Configure video info as needed
19//!                 .build()
20//!         ).start()
21//!         .unwrap();
22//!     
23//!     let replay = ReplayBufferBuilder::new(context, "my_replay")
24//!         .max_time_sec(30)
25//!         .max_size_mb(1000)
26//!         .format("%CCYY-%MM-%DD %hh-%mm-%ss")
27//!         .extension("mp4")
28//!         .build()
29//!         .unwrap();
30//!
31//!     // Configure video and audio encoders on the replay buffer
32//!     // Start the replay buffer
33//!     // Call replay.save_buffer() when you want to save the buffer
34//!
35//!     println!("Replay buffer created!");
36//! }
37//! ```
38
39use libobs_wrapper::{
40    context::ObsContext,
41    data::{
42        output::{ObsOutputTrait, ObsReplayBufferOutputRef},
43        ObsData, ObsDataGetters, ObsDataSetters,
44    },
45    encoders::{ObsAudioEncoderType, ObsContextEncoders, ObsVideoEncoderType},
46    utils::{AudioEncoderInfo, ObsError, ObsPath, ObsString, OutputInfo, VideoEncoderInfo},
47};
48
49use super::simple::{AudioEncoder, HardwareCodec, HardwarePreset, VideoEncoder, X264Preset};
50
51/// Settings for replay buffer output
52#[derive(Debug)]
53pub struct ReplayBufferSettings {
54    name: ObsString,
55    /// Maximum duration to keep in buffer (seconds)
56    max_time_sec: i64,
57    /// Maximum buffer size (megabytes)
58    max_size_mb: i64,
59    /// Filename format string (e.g., "%CCYY-%MM-%DD %hh-%mm-%ss")
60    format: ObsString,
61    /// File extension (e.g., "mp4", "mkv")
62    extension: ObsString,
63    /// Allow spaces in filenames
64    allow_spaces: bool,
65    video_encoder: VideoEncoder,
66    audio_encoder: AudioEncoder,
67    video_bitrate: u32,
68    audio_bitrate: u32,
69    directory: ObsPath,
70    custom_encoder_settings: Option<String>,
71}
72
73impl ReplayBufferSettings {
74    /// Sets the maximum time to keep in buffer (seconds).
75    pub fn with_max_time_sec(mut self, seconds: i64) -> Self {
76        self.max_time_sec = seconds;
77        self
78    }
79
80    /// Sets the maximum buffer size (megabytes).
81    pub fn with_max_size_mb(mut self, megabytes: i64) -> Self {
82        self.max_size_mb = megabytes;
83        self
84    }
85
86    /// Sets the filename format string.
87    pub fn with_format<S: Into<ObsString>>(mut self, format: S) -> Self {
88        self.format = format.into();
89        self
90    }
91
92    /// Sets the file extension.
93    pub fn with_extension<S: Into<ObsString>>(mut self, extension: S) -> Self {
94        self.extension = extension.into();
95        self
96    }
97
98    /// Sets whether to allow spaces in filenames.
99    pub fn with_allow_spaces(mut self, allow: bool) -> Self {
100        self.allow_spaces = allow;
101        self
102    }
103
104    /// Sets the video bitrate in Kbps.
105    pub fn with_video_bitrate(mut self, bitrate: u32) -> Self {
106        self.video_bitrate = bitrate;
107        self
108    }
109
110    /// Sets the audio bitrate in Kbps.
111    pub fn with_audio_bitrate(mut self, bitrate: u32) -> Self {
112        self.audio_bitrate = bitrate;
113        self
114    }
115
116    /// Sets the video encoder to use x264 software encoding.
117    pub fn with_x264_encoder(mut self, preset: X264Preset) -> Self {
118        self.video_encoder = VideoEncoder::X264(preset);
119        self
120    }
121
122    /// Sets the video encoder to use a generic hardware encoder for the given codec.
123    pub fn with_hardware_encoder(mut self, codec: HardwareCodec, preset: HardwarePreset) -> Self {
124        self.video_encoder = VideoEncoder::Hardware { codec, preset };
125        self
126    }
127
128    /// Sets a custom video encoder.
129    pub fn with_custom_video_encoder(mut self, encoder: ObsVideoEncoderType) -> Self {
130        self.video_encoder = VideoEncoder::Custom(encoder);
131        self
132    }
133
134    /// Sets custom encoder settings.
135    pub fn with_custom_encoder_settings<S: Into<String>>(mut self, settings: S) -> Self {
136        self.custom_encoder_settings = Some(settings.into());
137        self
138    }
139
140    /// Sets the audio encoder.
141    pub fn with_audio_encoder(mut self, encoder: AudioEncoder) -> Self {
142        self.audio_encoder = encoder;
143        self
144    }
145}
146
147/// Builder for replay buffer outputs
148#[derive(Debug)]
149pub struct ReplayBufferBuilder {
150    settings: ReplayBufferSettings,
151    context: ObsContext,
152}
153
154/// Extension trait for ObsContext to create replay buffer builders
155pub trait ObsContextReplayExt {
156    fn replay_buffer_builder<T: Into<ObsString>, K: Into<ObsPath>>(
157        &self,
158        name: T,
159        directory_path: K,
160    ) -> ReplayBufferBuilder;
161}
162
163impl ObsContextReplayExt for ObsContext {
164    fn replay_buffer_builder<T: Into<ObsString>, K: Into<ObsPath>>(
165        &self,
166        name: T,
167        directory_path: K,
168    ) -> ReplayBufferBuilder {
169        ReplayBufferBuilder::new(self.clone(), name, directory_path)
170    }
171}
172
173impl ReplayBufferBuilder {
174    /// Creates a new ReplayBufferBuilder with default settings.
175    pub fn new<T: Into<ObsString>, K: Into<ObsPath>>(
176        context: ObsContext,
177        name: T,
178        directory_path: K,
179    ) -> Self {
180        ReplayBufferBuilder {
181            settings: ReplayBufferSettings {
182                name: name.into(),
183                max_time_sec: 15,
184                max_size_mb: 500,
185                format: "%CCYY-%MM-%DD %hh-%mm-%ss".into(),
186                extension: "mp4".into(),
187                directory: directory_path.into(),
188                allow_spaces: true,
189                video_bitrate: 6000,
190                audio_bitrate: 160,
191                video_encoder: VideoEncoder::X264(X264Preset::VeryFast),
192                audio_encoder: AudioEncoder::AAC,
193                custom_encoder_settings: None,
194            },
195            context,
196        }
197    }
198
199    /// Sets the replay buffer settings.
200    pub fn settings(mut self, settings: ReplayBufferSettings) -> Self {
201        self.settings = settings;
202        self
203    }
204
205    /// Sets the maximum time to keep in buffer (seconds).
206    pub fn max_time_sec(mut self, seconds: i64) -> Self {
207        self.settings.max_time_sec = seconds;
208        self
209    }
210
211    /// Sets the maximum buffer size (megabytes).
212    pub fn max_size_mb(mut self, megabytes: i64) -> Self {
213        self.settings.max_size_mb = megabytes;
214        self
215    }
216
217    /// Sets the filename format string.
218    pub fn format<S: Into<ObsString>>(mut self, format: S) -> Self {
219        self.settings.format = format.into();
220        self
221    }
222
223    /// Sets the file extension.
224    pub fn extension<S: Into<ObsString>>(mut self, extension: S) -> Self {
225        self.settings.extension = extension.into();
226        self
227    }
228
229    /// Sets whether to allow spaces in filenames.
230    pub fn allow_spaces(mut self, allow: bool) -> Self {
231        self.settings.allow_spaces = allow;
232        self
233    }
234
235    /// Sets the video bitrate in Kbps.
236    pub fn video_bitrate(mut self, bitrate: u32) -> Self {
237        self.settings.video_bitrate = bitrate;
238        self
239    }
240
241    /// Sets the audio bitrate in Kbps.
242    pub fn audio_bitrate(mut self, bitrate: u32) -> Self {
243        self.settings.audio_bitrate = bitrate;
244        self
245    }
246
247    /// Sets the video encoder to x264.
248    pub fn x264_encoder(mut self, preset: X264Preset) -> Self {
249        self.settings.video_encoder = VideoEncoder::X264(preset);
250        self
251    }
252
253    /// Sets the video encoder to a generic hardware encoder.
254    pub fn hardware_encoder(mut self, codec: HardwareCodec, preset: HardwarePreset) -> Self {
255        self.settings.video_encoder = VideoEncoder::Hardware { codec, preset };
256        self
257    }
258
259    /// Builds and returns the configured replay buffer output.
260    pub fn build(mut self) -> Result<ObsReplayBufferOutputRef, ObsError> {
261        if self.settings.max_size_mb <= 0 {
262            return Err(ObsError::InvalidOperation(
263                "max_size_mb must be greater than 0".into(),
264            ));
265        }
266
267        if self.settings.max_time_sec <= 0 {
268            return Err(ObsError::InvalidOperation(
269                "max_time_sec must be greater than 0".into(),
270            ));
271        }
272
273        // Create replay buffer settings
274        let mut output_settings = self.context.data()?;
275        output_settings.set_int("max_time_sec", self.settings.max_time_sec)?;
276        output_settings.set_int("max_size_mb", self.settings.max_size_mb)?;
277        output_settings.set_string("format", self.settings.format.clone())?;
278        output_settings.set_string("extension", self.settings.extension.clone())?;
279        output_settings.set_string("directory", self.settings.directory.clone().build())?;
280        output_settings.set_bool("allow_spaces", self.settings.allow_spaces)?;
281
282        log::trace!(
283            "Replay buffer output settings: {:?}",
284            output_settings.get_json()
285        );
286
287        // Create the replay buffer output
288        let output_info = OutputInfo::new(
289            "replay_buffer",
290            self.settings.name.clone(),
291            Some(output_settings),
292            None,
293        );
294
295        let mut output = self.context.replay_buffer(output_info)?;
296
297        // Create and configure video encoder (with hardware fallback)
298        let video_encoder_type = self.select_video_encoder_type(&self.settings.video_encoder)?;
299        let mut video_settings = self.context.data()?;
300
301        self.configure_video_encoder(&mut video_settings)?;
302
303        let video_encoder_info = VideoEncoderInfo::new(
304            video_encoder_type,
305            format!("{}_video_encoder", self.settings.name),
306            Some(video_settings),
307            None,
308        );
309
310        output.create_and_set_video_encoder(video_encoder_info)?;
311
312        // Create and configure audio encoder
313        let audio_encoder_type = match &self.settings.audio_encoder {
314            AudioEncoder::AAC => ObsAudioEncoderType::FFMPEG_AAC,
315            AudioEncoder::Opus => ObsAudioEncoderType::FFMPEG_OPUS,
316            AudioEncoder::Custom(encoder_type) => encoder_type.clone(),
317        };
318
319        log::trace!("Selected audio encoder: {:?}", audio_encoder_type);
320        let mut audio_settings = self.context.data()?;
321        audio_settings.set_string("rate_control", "CBR")?;
322        audio_settings.set_int("bitrate", self.settings.audio_bitrate as i64)?;
323
324        let audio_encoder_info = AudioEncoderInfo::new(
325            audio_encoder_type,
326            format!("{}_audio_encoder", self.settings.name),
327            Some(audio_settings),
328            None,
329        );
330
331        log::trace!("Creating audio encoder with info: {:?}", audio_encoder_info);
332        output.create_and_set_audio_encoder(audio_encoder_info, 0)?;
333
334        Ok(output)
335    }
336
337    fn select_video_encoder_type(
338        &self,
339        encoder: &VideoEncoder,
340    ) -> Result<ObsVideoEncoderType, ObsError> {
341        match encoder {
342            VideoEncoder::X264(_) => Ok(ObsVideoEncoderType::OBS_X264),
343            VideoEncoder::Custom(t) => Ok(t.clone()),
344            VideoEncoder::Hardware { codec, .. } => {
345                // Build preferred candidates for the requested codec
346                let candidates = self.hardware_candidates(*codec);
347                // Query available encoders
348                let available = self
349                    .context
350                    .available_video_encoders()?
351                    .into_iter()
352                    .map(|b| b.get_encoder_id().clone())
353                    .collect::<Vec<_>>();
354                // Pick first preferred candidate that is available
355                for cand in candidates {
356                    if available.iter().any(|a| a == &cand) {
357                        return Ok(cand);
358                    }
359                }
360                // Fallback to x264 if no hardware encoder is available
361                Ok(ObsVideoEncoderType::OBS_X264)
362            }
363        }
364    }
365
366    fn hardware_candidates(&self, codec: HardwareCodec) -> Vec<ObsVideoEncoderType> {
367        match codec {
368            HardwareCodec::H264 => vec![
369                ObsVideoEncoderType::OBS_NVENC_H264_TEX,
370                ObsVideoEncoderType::H264_TEXTURE_AMF,
371                ObsVideoEncoderType::OBS_QSV11_V2,
372                ObsVideoEncoderType::OBS_NVENC_H264_SOFT,
373                ObsVideoEncoderType::OBS_QSV11_SOFT_V2,
374            ],
375            HardwareCodec::HEVC => vec![
376                ObsVideoEncoderType::OBS_NVENC_HEVC_TEX,
377                ObsVideoEncoderType::H265_TEXTURE_AMF,
378                ObsVideoEncoderType::OBS_QSV11_HEVC,
379                ObsVideoEncoderType::OBS_NVENC_HEVC_SOFT,
380                ObsVideoEncoderType::OBS_QSV11_HEVC_SOFT,
381            ],
382            HardwareCodec::AV1 => vec![
383                ObsVideoEncoderType::OBS_NVENC_AV1_TEX,
384                ObsVideoEncoderType::AV1_TEXTURE_AMF,
385                ObsVideoEncoderType::OBS_QSV11_AV1,
386                ObsVideoEncoderType::OBS_NVENC_AV1_SOFT,
387                ObsVideoEncoderType::OBS_QSV11_AV1_SOFT,
388            ],
389        }
390    }
391
392    fn get_encoder_preset(&self, encoder: &VideoEncoder) -> Option<&str> {
393        match encoder {
394            VideoEncoder::X264(preset) => Some(preset.as_str()),
395            VideoEncoder::Hardware { preset, .. } => Some(preset.as_str()),
396            VideoEncoder::Custom(_) => None,
397        }
398    }
399
400    fn configure_video_encoder(&self, settings: &mut ObsData) -> Result<(), ObsError> {
401        // Set rate control to CBR
402        settings.set_string("rate_control", "CBR")?;
403        settings.set_int("bitrate", self.settings.video_bitrate as i64)?;
404
405        // Set preset if available
406        if let Some(preset) = self.get_encoder_preset(&self.settings.video_encoder) {
407            settings.set_string("preset", preset)?;
408        }
409
410        // Apply custom encoder settings if provided
411        if let Some(ref custom) = self.settings.custom_encoder_settings {
412            settings.set_string("x264opts", custom.as_str())?;
413        }
414
415        Ok(())
416    }
417}