1use 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#[derive(Debug)]
53pub struct ReplayBufferSettings {
54 name: ObsString,
55 max_time_sec: i64,
57 max_size_mb: i64,
59 format: ObsString,
61 extension: ObsString,
63 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 pub fn with_max_time_sec(mut self, seconds: i64) -> Self {
76 self.max_time_sec = seconds;
77 self
78 }
79
80 pub fn with_max_size_mb(mut self, megabytes: i64) -> Self {
82 self.max_size_mb = megabytes;
83 self
84 }
85
86 pub fn with_format<S: Into<ObsString>>(mut self, format: S) -> Self {
88 self.format = format.into();
89 self
90 }
91
92 pub fn with_extension<S: Into<ObsString>>(mut self, extension: S) -> Self {
94 self.extension = extension.into();
95 self
96 }
97
98 pub fn with_allow_spaces(mut self, allow: bool) -> Self {
100 self.allow_spaces = allow;
101 self
102 }
103
104 pub fn with_video_bitrate(mut self, bitrate: u32) -> Self {
106 self.video_bitrate = bitrate;
107 self
108 }
109
110 pub fn with_audio_bitrate(mut self, bitrate: u32) -> Self {
112 self.audio_bitrate = bitrate;
113 self
114 }
115
116 pub fn with_x264_encoder(mut self, preset: X264Preset) -> Self {
118 self.video_encoder = VideoEncoder::X264(preset);
119 self
120 }
121
122 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 pub fn with_custom_video_encoder(mut self, encoder: ObsVideoEncoderType) -> Self {
130 self.video_encoder = VideoEncoder::Custom(encoder);
131 self
132 }
133
134 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 pub fn with_audio_encoder(mut self, encoder: AudioEncoder) -> Self {
142 self.audio_encoder = encoder;
143 self
144 }
145}
146
147#[derive(Debug)]
149pub struct ReplayBufferBuilder {
150 settings: ReplayBufferSettings,
151 context: ObsContext,
152}
153
154pub 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 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 pub fn settings(mut self, settings: ReplayBufferSettings) -> Self {
201 self.settings = settings;
202 self
203 }
204
205 pub fn max_time_sec(mut self, seconds: i64) -> Self {
207 self.settings.max_time_sec = seconds;
208 self
209 }
210
211 pub fn max_size_mb(mut self, megabytes: i64) -> Self {
213 self.settings.max_size_mb = megabytes;
214 self
215 }
216
217 pub fn format<S: Into<ObsString>>(mut self, format: S) -> Self {
219 self.settings.format = format.into();
220 self
221 }
222
223 pub fn extension<S: Into<ObsString>>(mut self, extension: S) -> Self {
225 self.settings.extension = extension.into();
226 self
227 }
228
229 pub fn allow_spaces(mut self, allow: bool) -> Self {
231 self.settings.allow_spaces = allow;
232 self
233 }
234
235 pub fn video_bitrate(mut self, bitrate: u32) -> Self {
237 self.settings.video_bitrate = bitrate;
238 self
239 }
240
241 pub fn audio_bitrate(mut self, bitrate: u32) -> Self {
243 self.settings.audio_bitrate = bitrate;
244 self
245 }
246
247 pub fn x264_encoder(mut self, preset: X264Preset) -> Self {
249 self.settings.video_encoder = VideoEncoder::X264(preset);
250 self
251 }
252
253 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 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 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 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 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 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 let candidates = self.hardware_candidates(*codec);
347 let available = self
349 .context
350 .available_video_encoders()?
351 .into_iter()
352 .map(|b| b.get_encoder_id().clone())
353 .collect::<Vec<_>>();
354 for cand in candidates {
356 if available.iter().any(|a| a == &cand) {
357 return Ok(cand);
358 }
359 }
360 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 settings.set_string("rate_control", "CBR")?;
403 settings.set_int("bitrate", self.settings.video_bitrate as i64)?;
404
405 if let Some(preset) = self.get_encoder_preset(&self.settings.video_encoder) {
407 settings.set_string("preset", preset)?;
408 }
409
410 if let Some(ref custom) = self.settings.custom_encoder_settings {
412 settings.set_string("x264opts", custom.as_str())?;
413 }
414
415 Ok(())
416 }
417}