1use std::{
17 collections::{HashMap, HashSet},
18 ffi::CStr,
19 os::raw::{c_char, c_void},
20 ptr,
21};
22
23use crate::{
24 context::ObsContext,
25 data::{output::ObsOutputRef, ImmutableObsData, ObsData, ObsDataPointers},
26 encoders::{audio::ObsAudioEncoder, video::ObsVideoEncoder},
27 run_with_obs,
28 runtime::ObsRuntime,
29 scenes::{ObsSceneItemRef, ObsSceneRef},
30 services::ObsServiceRef,
31 settings::{SettingsSchema, SettingsSnapshot},
32 sources::{ObsFilterRef, ObsSourceRef},
33 unsafe_send::Sendable,
34 utils::{ObjectInfo, ObsError, ObsString},
35};
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
38pub enum SourceKind {
39 Input,
40 Filter,
41 Transition,
42 Other,
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46pub enum EncoderKind {
47 Audio,
48 Video,
49 Unknown(i64),
50}
51
52bitflags::bitflags! {
53 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
55 pub struct EncoderCapabilities: u32 {
56 const DEPRECATED = libobs::OBS_ENCODER_CAP_DEPRECATED;
57 const PASS_TEXTURE = libobs::OBS_ENCODER_CAP_PASS_TEXTURE;
58 const DYNAMIC_BITRATE = libobs::OBS_ENCODER_CAP_DYN_BITRATE;
59 const INTERNAL = libobs::OBS_ENCODER_CAP_INTERNAL;
60 const ROI = libobs::OBS_ENCODER_CAP_ROI;
61 const SCALING = libobs::OBS_ENCODER_CAP_SCALING;
62 }
63}
64
65bitflags::bitflags! {
66 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
68 pub struct OutputCapabilities: u32 {
69 const VIDEO = libobs::OBS_OUTPUT_VIDEO;
70 const AUDIO = libobs::OBS_OUTPUT_AUDIO;
71 const ENCODED = libobs::OBS_OUTPUT_ENCODED;
72 const SERVICE = libobs::OBS_OUTPUT_SERVICE;
73 const MULTI_TRACK_AUDIO = libobs::OBS_OUTPUT_MULTI_TRACK_AUDIO;
74 const CAN_PAUSE = libobs::OBS_OUTPUT_CAN_PAUSE;
75 const MULTI_TRACK_VIDEO = libobs::OBS_OUTPUT_MULTI_TRACK_VIDEO;
76 }
77}
78
79#[derive(Clone, Debug)]
80pub struct SourceTypeInfo {
81 id: String,
82 display_name: Option<String>,
83 kind: SourceKind,
84 output_flags: u32,
85 runtime: ObsRuntime,
86}
87
88impl SourceTypeInfo {
89 pub fn id(&self) -> &str {
90 &self.id
91 }
92
93 pub fn display_name(&self) -> Option<&str> {
94 self.display_name.as_deref()
95 }
96
97 pub fn kind(&self) -> SourceKind {
98 self.kind
99 }
100
101 pub fn output_flags(&self) -> u32 {
102 self.output_flags
103 }
104
105 pub fn properties(&self) -> Result<Vec<PropertyMetadata>, ObsError> {
106 source_properties(&self.runtime, &self.id)
107 }
108
109 pub fn settings_schema(&self) -> Result<SettingsSchema, ObsError> {
110 Ok(SettingsSchema::new(self.properties()?))
111 }
112
113 pub fn settings_schema_for(&self, settings: &ObsData) -> Result<SettingsSchema, ObsError> {
116 self.runtime.ensure_same_runtime(settings.runtime())?;
117 Ok(SettingsSchema::new(source_properties_for_settings(
118 &self.runtime,
119 &self.id,
120 settings,
121 )?))
122 }
123
124 pub fn settings_snapshot_for(&self, settings: &ObsData) -> Result<SettingsSnapshot, ObsError> {
125 let schema = self.settings_schema_for(settings)?;
126 let defaults = self.default_settings()?;
127 schema.snapshot(settings, defaults.as_ref())
128 }
129
130 pub fn default_settings(&self) -> Result<Option<ImmutableObsData>, ObsError> {
131 default_settings(&self.runtime, &self.id, libobs::obs_get_source_defaults)
132 }
133
134 pub fn default_settings_mut(&self) -> Result<ObsData, ObsError> {
137 mutable_default_settings(&self.runtime, &self.id, libobs::obs_get_source_defaults)
138 }
139}
140
141#[derive(Clone, Debug)]
142pub struct OutputTypeInfo {
143 id: String,
144 display_name: Option<String>,
145 video_codecs: Vec<String>,
146 audio_codecs: Vec<String>,
147 protocols: Vec<String>,
148 flags: u32,
149 runtime: ObsRuntime,
150}
151
152impl OutputTypeInfo {
153 pub fn id(&self) -> &str {
154 &self.id
155 }
156
157 pub fn display_name(&self) -> Option<&str> {
158 self.display_name.as_deref()
159 }
160
161 pub fn video_codecs(&self) -> &[String] {
162 &self.video_codecs
163 }
164
165 pub fn audio_codecs(&self) -> &[String] {
166 &self.audio_codecs
167 }
168
169 pub fn protocols(&self) -> &[String] {
170 &self.protocols
171 }
172
173 pub fn flags(&self) -> u32 {
174 self.flags
175 }
176
177 pub(crate) fn runtime(&self) -> &ObsRuntime {
178 &self.runtime
179 }
180
181 pub fn capability_flags(&self) -> OutputCapabilities {
182 OutputCapabilities::from_bits_retain(self.flags)
183 }
184
185 pub fn supports_video_codec(&self, codec: &str) -> bool {
186 capability_list_supports(&self.video_codecs, codec)
187 }
188
189 pub fn supports_audio_codec(&self, codec: &str) -> bool {
190 capability_list_supports(&self.audio_codecs, codec)
191 }
192
193 pub fn supports_protocol(&self, protocol: &str) -> bool {
194 capability_list_supports(&self.protocols, protocol)
195 }
196
197 pub fn properties(&self) -> Result<Vec<PropertyMetadata>, ObsError> {
198 type_properties(&self.runtime, &self.id, libobs::obs_get_output_properties)
199 }
200
201 pub fn settings_schema(&self) -> Result<SettingsSchema, ObsError> {
202 Ok(SettingsSchema::new(self.properties()?))
203 }
204
205 pub fn settings_schema_for(&self, settings: &ObsData) -> Result<SettingsSchema, ObsError> {
206 self.runtime.ensure_same_runtime(settings.runtime())?;
207 Ok(SettingsSchema::new(type_properties_for_settings(
208 &self.runtime,
209 &self.id,
210 libobs::obs_get_output_properties,
211 settings,
212 )?))
213 }
214
215 pub fn settings_snapshot_for(&self, settings: &ObsData) -> Result<SettingsSnapshot, ObsError> {
216 let schema = self.settings_schema_for(settings)?;
217 let defaults = self.default_settings()?;
218 schema.snapshot(settings, defaults.as_ref())
219 }
220
221 pub fn default_settings(&self) -> Result<Option<ImmutableObsData>, ObsError> {
222 default_settings(&self.runtime, &self.id, libobs::obs_output_defaults)
223 }
224
225 pub fn default_settings_mut(&self) -> Result<ObsData, ObsError> {
227 mutable_default_settings(&self.runtime, &self.id, libobs::obs_output_defaults)
228 }
229}
230
231#[derive(Clone, Debug)]
232pub struct EncoderTypeInfo {
233 id: String,
234 display_name: Option<String>,
235 kind: EncoderKind,
236 codec: Option<String>,
237 capabilities: u32,
238 runtime: ObsRuntime,
239}
240
241impl EncoderTypeInfo {
242 pub fn id(&self) -> &str {
243 &self.id
244 }
245
246 pub fn display_name(&self) -> Option<&str> {
247 self.display_name.as_deref()
248 }
249
250 pub fn kind(&self) -> EncoderKind {
251 self.kind
252 }
253
254 pub fn codec(&self) -> Option<&str> {
255 self.codec.as_deref()
256 }
257
258 pub fn capabilities(&self) -> u32 {
259 self.capabilities
260 }
261
262 pub fn capability_flags(&self) -> EncoderCapabilities {
263 EncoderCapabilities::from_bits_retain(self.capabilities)
264 }
265
266 pub fn is_deprecated(&self) -> bool {
267 self.capability_flags()
268 .contains(EncoderCapabilities::DEPRECATED)
269 }
270
271 pub fn is_internal(&self) -> bool {
272 self.capability_flags()
273 .contains(EncoderCapabilities::INTERNAL)
274 }
275
276 pub fn is_likely_hardware_accelerated(&self) -> bool {
283 if self
284 .capability_flags()
285 .contains(EncoderCapabilities::PASS_TEXTURE)
286 {
287 return true;
288 }
289 let id = self.id.to_ascii_lowercase();
290 ["nvenc", "qsv", "amf", "vaapi", "videotoolbox"]
291 .iter()
292 .any(|marker| id.contains(marker))
293 }
294
295 pub fn properties(&self) -> Result<Vec<PropertyMetadata>, ObsError> {
296 type_properties(&self.runtime, &self.id, libobs::obs_get_encoder_properties)
297 }
298
299 pub fn settings_schema(&self) -> Result<SettingsSchema, ObsError> {
300 Ok(SettingsSchema::new(self.properties()?))
301 }
302
303 pub fn settings_schema_for(&self, settings: &ObsData) -> Result<SettingsSchema, ObsError> {
304 self.runtime.ensure_same_runtime(settings.runtime())?;
305 Ok(SettingsSchema::new(type_properties_for_settings(
306 &self.runtime,
307 &self.id,
308 libobs::obs_get_encoder_properties,
309 settings,
310 )?))
311 }
312
313 pub fn settings_snapshot_for(&self, settings: &ObsData) -> Result<SettingsSnapshot, ObsError> {
314 let schema = self.settings_schema_for(settings)?;
315 let defaults = self.default_settings()?;
316 schema.snapshot(settings, defaults.as_ref())
317 }
318
319 pub fn default_settings(&self) -> Result<Option<ImmutableObsData>, ObsError> {
320 default_settings(&self.runtime, &self.id, libobs::obs_encoder_defaults)
321 }
322
323 pub fn default_settings_mut(&self) -> Result<ObsData, ObsError> {
325 mutable_default_settings(&self.runtime, &self.id, libobs::obs_encoder_defaults)
326 }
327}
328
329#[derive(Clone, Debug)]
330pub struct ServiceTypeInfo {
331 id: String,
332 display_name: Option<String>,
333 runtime: ObsRuntime,
334}
335
336impl ServiceTypeInfo {
337 pub fn id(&self) -> &str {
338 &self.id
339 }
340
341 pub fn display_name(&self) -> Option<&str> {
342 self.display_name.as_deref()
343 }
344
345 pub fn properties(&self) -> Result<Vec<PropertyMetadata>, ObsError> {
346 type_properties(&self.runtime, &self.id, libobs::obs_get_service_properties)
347 }
348
349 pub fn settings_schema(&self) -> Result<SettingsSchema, ObsError> {
350 Ok(SettingsSchema::new(self.properties()?))
351 }
352
353 pub fn settings_schema_for(&self, settings: &ObsData) -> Result<SettingsSchema, ObsError> {
354 self.runtime.ensure_same_runtime(settings.runtime())?;
355 Ok(SettingsSchema::new(type_properties_for_settings(
356 &self.runtime,
357 &self.id,
358 libobs::obs_get_service_properties,
359 settings,
360 )?))
361 }
362
363 pub fn settings_snapshot_for(&self, settings: &ObsData) -> Result<SettingsSnapshot, ObsError> {
364 let schema = self.settings_schema_for(settings)?;
365 let defaults = self.default_settings()?;
366 schema.snapshot(settings, defaults.as_ref())
367 }
368
369 pub fn default_settings(&self) -> Result<Option<ImmutableObsData>, ObsError> {
370 default_settings(&self.runtime, &self.id, libobs::obs_service_defaults)
371 }
372
373 pub fn default_settings_mut(&self) -> Result<ObsData, ObsError> {
375 mutable_default_settings(&self.runtime, &self.id, libobs::obs_service_defaults)
376 }
377}
378
379#[derive(Clone, Debug, PartialEq, Eq)]
380pub struct ModuleInfo {
381 pub file_name: Option<String>,
382 pub name: Option<String>,
383 pub author: Option<String>,
384 pub description: Option<String>,
385 pub binary_path: Option<String>,
386 pub data_path: Option<String>,
387}
388
389#[derive(Clone, Debug)]
390pub struct ObsCapabilities {
391 source_types: Vec<SourceTypeInfo>,
392 input_types: Vec<SourceTypeInfo>,
393 filter_types: Vec<SourceTypeInfo>,
394 transition_types: Vec<SourceTypeInfo>,
395 outputs: Vec<OutputTypeInfo>,
396 encoders: Vec<EncoderTypeInfo>,
397 services: Vec<ServiceTypeInfo>,
398 protocols: Vec<String>,
399 modules: Vec<ModuleInfo>,
400}
401
402impl ObsCapabilities {
403 pub fn source_types(&self) -> &[SourceTypeInfo] {
404 &self.source_types
405 }
406 pub fn input_types(&self) -> &[SourceTypeInfo] {
407 &self.input_types
408 }
409 pub fn filter_types(&self) -> &[SourceTypeInfo] {
410 &self.filter_types
411 }
412 pub fn transition_types(&self) -> &[SourceTypeInfo] {
413 &self.transition_types
414 }
415 pub fn outputs(&self) -> &[OutputTypeInfo] {
416 &self.outputs
417 }
418 pub fn encoders(&self) -> &[EncoderTypeInfo] {
419 &self.encoders
420 }
421 pub fn services(&self) -> &[ServiceTypeInfo] {
422 &self.services
423 }
424 pub fn protocols(&self) -> &[String] {
425 &self.protocols
426 }
427 pub fn modules(&self) -> &[ModuleInfo] {
428 &self.modules
429 }
430
431 pub fn select_video_encoder(&self) -> EncoderSelector<'_> {
433 EncoderSelector::new(&self.encoders, EncoderKind::Video)
434 }
435
436 pub fn select_audio_encoder(&self) -> EncoderSelector<'_> {
438 EncoderSelector::new(&self.encoders, EncoderKind::Audio)
439 }
440
441 pub fn select_output(&self) -> OutputSelector<'_> {
443 OutputSelector::new(&self.outputs)
444 }
445
446 pub fn best_output_plan(
449 &self,
450 request: &OutputCompatibilityRequest,
451 ) -> Result<OutputCompatibilityPlan, OutputCompatibilityReport> {
452 let video_encoder = request.video_codec.as_deref().and_then(|codec| {
453 let selector = self.select_video_encoder().codec(codec);
454 if request.prefer_hardware_video {
455 selector.prefer_hardware().best_available().cloned()
456 } else {
457 selector.best_available().cloned()
458 }
459 });
460 let audio_encoder = request.audio_codec.as_deref().and_then(|codec| {
461 self.select_audio_encoder()
462 .codec(codec)
463 .best_available()
464 .cloned()
465 });
466
467 let mut issues = Vec::new();
468 if let Some(codec) = request.video_codec.as_deref() {
469 if video_encoder.is_none() {
470 issues.push(CompatibilityIssue::NoVideoEncoder {
471 codec: codec.into(),
472 });
473 }
474 }
475 if let Some(codec) = request.audio_codec.as_deref() {
476 if audio_encoder.is_none() {
477 issues.push(CompatibilityIssue::NoAudioEncoder {
478 codec: codec.into(),
479 });
480 }
481 }
482
483 let mut matching_outputs = Vec::new();
484 for output in &self.outputs {
485 let mut reasons = Vec::new();
486 if let Some(required_id) = request.output_id.as_deref() {
487 if output.id() != required_id {
488 reasons.push(OutputRejectionReason::OutputId {
489 required: required_id.into(),
490 });
491 }
492 }
493 let flags = output.capability_flags();
494 if !flags.contains(request.required_output_capabilities) {
495 reasons.push(OutputRejectionReason::MissingCapabilities {
496 required: request.required_output_capabilities,
497 actual: flags,
498 });
499 }
500 if let Some(protocol) = request.protocol.as_deref() {
501 if !output.supports_protocol(protocol) {
502 reasons.push(OutputRejectionReason::Protocol {
503 required: protocol.into(),
504 });
505 }
506 }
507 if let Some(codec) = request.video_codec.as_deref() {
508 if !output.supports_video_codec(codec) {
509 reasons.push(OutputRejectionReason::VideoCodec {
510 required: codec.into(),
511 });
512 }
513 }
514 if let Some(codec) = request.audio_codec.as_deref() {
515 if !output.supports_audio_codec(codec) {
516 reasons.push(OutputRejectionReason::AudioCodec {
517 required: codec.into(),
518 });
519 }
520 }
521 if reasons.is_empty() {
522 matching_outputs.push(output.clone());
523 } else {
524 issues.push(CompatibilityIssue::OutputRejected {
525 output_id: output.id.clone(),
526 reasons,
527 });
528 }
529 }
530
531 matching_outputs.sort_by(|a, b| a.id.cmp(&b.id));
532 let output = matching_outputs.into_iter().next();
533 if output.is_none() {
534 issues.push(CompatibilityIssue::NoCompatibleOutput);
535 }
536
537 let video_required = request.video_codec.is_some();
538 let audio_required = request.audio_codec.is_some();
539 match (
540 output,
541 video_required && video_encoder.is_none(),
542 audio_required && audio_encoder.is_none(),
543 ) {
544 (Some(output), false, false) => Ok(OutputCompatibilityPlan {
545 output,
546 video_encoder,
547 audio_encoder,
548 protocol: request.protocol.clone(),
549 }),
550 _ => Err(OutputCompatibilityReport { issues }),
551 }
552 }
553}
554
555#[derive(Clone, Debug, Default)]
557pub struct OutputCompatibilityRequest {
558 output_id: Option<String>,
559 protocol: Option<String>,
560 video_codec: Option<String>,
561 audio_codec: Option<String>,
562 required_output_capabilities: OutputCapabilities,
563 prefer_hardware_video: bool,
564}
565
566impl OutputCompatibilityRequest {
567 pub fn new() -> Self {
568 Self::default()
569 }
570
571 pub fn output_id(mut self, output_id: impl Into<String>) -> Self {
573 self.output_id = Some(output_id.into());
574 self
575 }
576
577 pub fn protocol(mut self, protocol: impl Into<String>) -> Self {
578 self.protocol = Some(protocol.into());
579 self
580 }
581
582 pub fn video_codec(mut self, codec: impl Into<String>) -> Self {
583 self.video_codec = Some(codec.into());
584 self
585 }
586
587 pub fn audio_codec(mut self, codec: impl Into<String>) -> Self {
588 self.audio_codec = Some(codec.into());
589 self
590 }
591
592 pub fn require_output_capabilities(mut self, capabilities: OutputCapabilities) -> Self {
593 self.required_output_capabilities |= capabilities;
594 self
595 }
596
597 pub fn prefer_hardware_video(mut self, prefer: bool) -> Self {
598 self.prefer_hardware_video = prefer;
599 self
600 }
601}
602
603#[derive(Clone, Debug)]
605pub struct OutputCompatibilityPlan {
606 output: OutputTypeInfo,
607 video_encoder: Option<EncoderTypeInfo>,
608 audio_encoder: Option<EncoderTypeInfo>,
609 protocol: Option<String>,
610}
611
612impl OutputCompatibilityPlan {
613 pub fn output(&self) -> &OutputTypeInfo {
614 &self.output
615 }
616 pub fn video_encoder(&self) -> Option<&EncoderTypeInfo> {
617 self.video_encoder.as_ref()
618 }
619 pub fn audio_encoder(&self) -> Option<&EncoderTypeInfo> {
620 self.audio_encoder.as_ref()
621 }
622 pub fn protocol(&self) -> Option<&str> {
623 self.protocol.as_deref()
624 }
625}
626
627#[derive(Clone, Debug, PartialEq, Eq)]
628pub enum OutputRejectionReason {
629 OutputId {
630 required: String,
631 },
632 MissingCapabilities {
633 required: OutputCapabilities,
634 actual: OutputCapabilities,
635 },
636 Protocol {
637 required: String,
638 },
639 VideoCodec {
640 required: String,
641 },
642 AudioCodec {
643 required: String,
644 },
645}
646
647#[derive(Clone, Debug, PartialEq, Eq)]
648pub enum CompatibilityIssue {
649 NoVideoEncoder {
650 codec: String,
651 },
652 NoAudioEncoder {
653 codec: String,
654 },
655 OutputRejected {
656 output_id: String,
657 reasons: Vec<OutputRejectionReason>,
658 },
659 NoCompatibleOutput,
660}
661
662#[derive(Clone, Debug, PartialEq, Eq)]
663pub struct OutputCompatibilityReport {
664 issues: Vec<CompatibilityIssue>,
665}
666
667impl OutputCompatibilityReport {
668 pub fn issues(&self) -> &[CompatibilityIssue] {
669 &self.issues
670 }
671
672 pub fn summary(&self) -> String {
673 let encoder_issues = self
674 .issues
675 .iter()
676 .filter(|issue| {
677 matches!(
678 issue,
679 CompatibilityIssue::NoVideoEncoder { .. }
680 | CompatibilityIssue::NoAudioEncoder { .. }
681 )
682 })
683 .count();
684 let rejected_outputs = self
685 .issues
686 .iter()
687 .filter(|issue| matches!(issue, CompatibilityIssue::OutputRejected { .. }))
688 .count();
689 format!(
690 "no compatible OBS output graph: {encoder_issues} encoder requirement(s) unavailable, {rejected_outputs} output type(s) rejected"
691 )
692 }
693}
694
695#[derive(Clone, Debug)]
697pub struct EncoderSelector<'a> {
698 encoders: &'a [EncoderTypeInfo],
699 kind: EncoderKind,
700 codec: Option<String>,
701 required_capabilities: EncoderCapabilities,
702 include_deprecated: bool,
703 include_internal: bool,
704 prefer_hardware: bool,
705}
706
707impl<'a> EncoderSelector<'a> {
708 fn new(encoders: &'a [EncoderTypeInfo], kind: EncoderKind) -> Self {
709 Self {
710 encoders,
711 kind,
712 codec: None,
713 required_capabilities: EncoderCapabilities::empty(),
714 include_deprecated: false,
715 include_internal: false,
716 prefer_hardware: false,
717 }
718 }
719
720 pub fn codec(mut self, codec: impl Into<String>) -> Self {
721 self.codec = Some(codec.into());
722 self
723 }
724
725 pub fn require_capabilities(mut self, capabilities: EncoderCapabilities) -> Self {
726 self.required_capabilities |= capabilities;
727 self
728 }
729
730 pub fn include_deprecated(mut self, include: bool) -> Self {
731 self.include_deprecated = include;
732 self
733 }
734
735 pub fn include_internal(mut self, include: bool) -> Self {
736 self.include_internal = include;
737 self
738 }
739
740 pub fn prefer_hardware(mut self) -> Self {
742 self.prefer_hardware = true;
743 self
744 }
745
746 pub fn matches(&self) -> Vec<&'a EncoderTypeInfo> {
747 let mut candidates = self
748 .encoders
749 .iter()
750 .filter(|encoder| encoder.kind == self.kind)
751 .filter(|encoder| self.include_deprecated || !encoder.is_deprecated())
752 .filter(|encoder| self.include_internal || !encoder.is_internal())
753 .filter(|encoder| {
754 self.codec.as_ref().is_none_or(|codec| {
755 encoder
756 .codec
757 .as_deref()
758 .is_some_and(|actual| actual.eq_ignore_ascii_case(codec))
759 })
760 })
761 .filter(|encoder| {
762 encoder
763 .capability_flags()
764 .contains(self.required_capabilities)
765 })
766 .collect::<Vec<_>>();
767
768 candidates.sort_by(|a, b| {
769 let a_hardware = self.prefer_hardware && a.is_likely_hardware_accelerated();
770 let b_hardware = self.prefer_hardware && b.is_likely_hardware_accelerated();
771 b_hardware.cmp(&a_hardware).then_with(|| a.id.cmp(&b.id))
772 });
773 candidates
774 }
775
776 pub fn best_available(&self) -> Option<&'a EncoderTypeInfo> {
777 self.matches().into_iter().next()
778 }
779}
780
781#[derive(Clone, Debug)]
783pub struct OutputSelector<'a> {
784 outputs: &'a [OutputTypeInfo],
785 protocol: Option<String>,
786 video_codec: Option<String>,
787 audio_codec: Option<String>,
788 required_capabilities: OutputCapabilities,
789}
790
791impl<'a> OutputSelector<'a> {
792 fn new(outputs: &'a [OutputTypeInfo]) -> Self {
793 Self {
794 outputs,
795 protocol: None,
796 video_codec: None,
797 audio_codec: None,
798 required_capabilities: OutputCapabilities::empty(),
799 }
800 }
801
802 pub fn protocol(mut self, protocol: impl Into<String>) -> Self {
803 self.protocol = Some(protocol.into());
804 self
805 }
806
807 pub fn video_codec(mut self, codec: impl Into<String>) -> Self {
808 self.video_codec = Some(codec.into());
809 self
810 }
811
812 pub fn audio_codec(mut self, codec: impl Into<String>) -> Self {
813 self.audio_codec = Some(codec.into());
814 self
815 }
816
817 pub fn require_capabilities(mut self, capabilities: OutputCapabilities) -> Self {
818 self.required_capabilities |= capabilities;
819 self
820 }
821
822 pub fn matches(&self) -> Vec<&'a OutputTypeInfo> {
823 let mut candidates = self
824 .outputs
825 .iter()
826 .filter(|output| {
827 self.protocol
828 .as_deref()
829 .is_none_or(|protocol| output.supports_protocol(protocol))
830 })
831 .filter(|output| {
832 self.video_codec
833 .as_deref()
834 .is_none_or(|codec| output.supports_video_codec(codec))
835 })
836 .filter(|output| {
837 self.audio_codec
838 .as_deref()
839 .is_none_or(|codec| output.supports_audio_codec(codec))
840 })
841 .filter(|output| {
842 output
843 .capability_flags()
844 .contains(self.required_capabilities)
845 })
846 .collect::<Vec<_>>();
847 candidates.sort_by(|a, b| a.id.cmp(&b.id));
848 candidates
849 }
850
851 pub fn best_available(&self) -> Option<&'a OutputTypeInfo> {
852 self.matches().into_iter().next()
853 }
854}
855
856#[derive(Clone, Debug, PartialEq)]
857pub struct PropertyMetadata {
858 pub name: String,
859 pub description: Option<String>,
860 pub long_description: Option<String>,
861 pub enabled: bool,
862 pub visible: bool,
863 pub kind: PropertyKind,
864}
865
866#[derive(Clone, Debug, PartialEq)]
867pub enum PropertyKind {
868 Invalid,
869 Bool,
870 Integer {
871 min: i32,
872 max: i32,
873 step: i32,
874 suffix: Option<String>,
875 control: NumberControl,
876 },
877 Float {
878 min: f64,
879 max: f64,
880 step: f64,
881 suffix: Option<String>,
882 control: NumberControl,
883 },
884 Text {
885 text_type: TextType,
886 monospace: bool,
887 info_type: TextInfoType,
888 word_wrap: bool,
889 },
890 Path {
891 path_type: PathType,
892 filter: Option<String>,
893 default_path: Option<String>,
894 },
895 List {
896 list_type: ListType,
897 format: ListFormat,
898 items: Vec<ListItem>,
899 },
900 Color,
901 Button {
902 button_type: ButtonType,
903 url: Option<String>,
904 },
905 Font,
906 EditableList {
907 list_type: EditableListType,
908 filter: Option<String>,
909 default_path: Option<String>,
910 },
911 FrameRate {
912 options: Vec<FrameRateOption>,
913 ranges: Vec<FrameRateRange>,
914 },
915 Group {
916 group_type: GroupType,
917 properties: Vec<PropertyMetadata>,
918 },
919 ColorAlpha,
920 Unknown(i64),
922}
923
924macro_rules! raw_enum {
925 ($name:ident { $($variant:ident = $constant:path),+ $(,)? }) => {
926 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
927 pub enum $name {
928 $($variant,)+
929 Unknown(i64),
930 }
931 impl $name {
932 fn from_raw(raw: i64) -> Self {
933 $(if raw == $constant as i64 { return Self::$variant; })+
934 Self::Unknown(raw)
935 }
936 }
937 };
938}
939
940raw_enum!(NumberControl {
941 Scroller = libobs::obs_number_type_OBS_NUMBER_SCROLLER,
942 Slider = libobs::obs_number_type_OBS_NUMBER_SLIDER,
943});
944raw_enum!(TextType {
945 Default = libobs::obs_text_type_OBS_TEXT_DEFAULT,
946 Password = libobs::obs_text_type_OBS_TEXT_PASSWORD,
947 Multiline = libobs::obs_text_type_OBS_TEXT_MULTILINE,
948 Info = libobs::obs_text_type_OBS_TEXT_INFO,
949});
950raw_enum!(TextInfoType {
951 Normal = libobs::obs_text_info_type_OBS_TEXT_INFO_NORMAL,
952 Warning = libobs::obs_text_info_type_OBS_TEXT_INFO_WARNING,
953 Error = libobs::obs_text_info_type_OBS_TEXT_INFO_ERROR,
954});
955raw_enum!(PathType {
956 File = libobs::obs_path_type_OBS_PATH_FILE,
957 FileSave = libobs::obs_path_type_OBS_PATH_FILE_SAVE,
958 Directory = libobs::obs_path_type_OBS_PATH_DIRECTORY,
959});
960raw_enum!(ListType {
961 Invalid = libobs::obs_combo_type_OBS_COMBO_TYPE_INVALID,
962 Editable = libobs::obs_combo_type_OBS_COMBO_TYPE_EDITABLE,
963 List = libobs::obs_combo_type_OBS_COMBO_TYPE_LIST,
964 Radio = libobs::obs_combo_type_OBS_COMBO_TYPE_RADIO,
965});
966raw_enum!(ListFormat {
967 Invalid = libobs::obs_combo_format_OBS_COMBO_FORMAT_INVALID,
968 Int = libobs::obs_combo_format_OBS_COMBO_FORMAT_INT,
969 Float = libobs::obs_combo_format_OBS_COMBO_FORMAT_FLOAT,
970 String = libobs::obs_combo_format_OBS_COMBO_FORMAT_STRING,
971 Bool = libobs::obs_combo_format_OBS_COMBO_FORMAT_BOOL,
972});
973raw_enum!(ButtonType {
974 Default = libobs::obs_button_type_OBS_BUTTON_DEFAULT,
975 Url = libobs::obs_button_type_OBS_BUTTON_URL,
976});
977raw_enum!(EditableListType {
978 Strings = libobs::obs_editable_list_type_OBS_EDITABLE_LIST_TYPE_STRINGS,
979 Files = libobs::obs_editable_list_type_OBS_EDITABLE_LIST_TYPE_FILES,
980 FilesAndUrls = libobs::obs_editable_list_type_OBS_EDITABLE_LIST_TYPE_FILES_AND_URLS,
981});
982raw_enum!(GroupType {
983 Invalid = libobs::obs_group_type_OBS_COMBO_INVALID,
984 Normal = libobs::obs_group_type_OBS_GROUP_NORMAL,
985 Checkable = libobs::obs_group_type_OBS_GROUP_CHECKABLE,
986});
987
988#[derive(Clone, Debug, PartialEq)]
989pub struct ListItem {
990 pub name: String,
991 pub value: ListValue,
992 pub disabled: bool,
993}
994
995#[derive(Clone, Debug, PartialEq)]
996pub enum ListValue {
997 String(String),
998 Int(i64),
999 Float(f64),
1000 Bool(bool),
1001 UnknownFormat(i64),
1002}
1003
1004#[derive(Clone, Debug, PartialEq, Eq)]
1005pub struct FrameRateOption {
1006 pub name: Option<String>,
1007 pub description: Option<String>,
1008}
1009
1010#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1011pub struct FrameRate {
1012 pub numerator: u32,
1013 pub denominator: u32,
1014}
1015
1016#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1017pub struct FrameRateRange {
1018 pub min: FrameRate,
1019 pub max: FrameRate,
1020}
1021
1022impl ObsSceneRef {
1023 pub fn add_discovered_source(
1025 &mut self,
1026 source_type: &SourceTypeInfo,
1027 name: impl Into<ObsString>,
1028 settings: Option<ObsData>,
1029 ) -> Result<ObsSceneItemRef<ObsSourceRef>, ObsError> {
1030 self.runtime().ensure_same_runtime(&source_type.runtime)?;
1031 if source_type.kind == SourceKind::Filter {
1032 return Err(capability_kind_mismatch(
1033 &source_type.id,
1034 "source/input/transition",
1035 "filter",
1036 ));
1037 }
1038 ensure_settings_runtime(self.runtime(), settings.as_ref())?;
1039 let source = ObsSourceRef::new_from_info(
1040 ObjectInfo::new(source_type.id.as_str(), name, settings, None),
1041 self.runtime().clone(),
1042 )?;
1043 self.add(source)
1044 }
1045}
1046
1047impl ObsContext {
1048 pub fn source_types(&self) -> Result<Vec<SourceTypeInfo>, ObsError> {
1049 discover_sources(self.runtime())
1050 }
1051
1052 pub fn input_types(&self) -> Result<Vec<SourceTypeInfo>, ObsError> {
1053 discover_source_category(
1054 self.runtime(),
1055 SourceKind::Input,
1056 libobs::obs_enum_input_types,
1057 )
1058 }
1059
1060 pub fn filter_types(&self) -> Result<Vec<SourceTypeInfo>, ObsError> {
1061 discover_source_category(
1062 self.runtime(),
1063 SourceKind::Filter,
1064 libobs::obs_enum_filter_types,
1065 )
1066 }
1067
1068 pub fn transition_types(&self) -> Result<Vec<SourceTypeInfo>, ObsError> {
1069 discover_source_category(
1070 self.runtime(),
1071 SourceKind::Transition,
1072 libobs::obs_enum_transition_types,
1073 )
1074 }
1075
1076 pub fn output_types(&self) -> Result<Vec<OutputTypeInfo>, ObsError> {
1077 discover_outputs(self.runtime())
1078 }
1079
1080 pub fn encoder_types(&self) -> Result<Vec<EncoderTypeInfo>, ObsError> {
1081 discover_encoders(self.runtime())
1082 }
1083
1084 pub fn service_types(&self) -> Result<Vec<ServiceTypeInfo>, ObsError> {
1085 discover_services(self.runtime())
1086 }
1087
1088 pub fn protocols(&self) -> Result<Vec<String>, ObsError> {
1089 discover_protocols(self.runtime())
1090 }
1091
1092 pub fn loaded_modules(&self) -> Result<Vec<ModuleInfo>, ObsError> {
1093 discover_modules(self.runtime())
1094 }
1095
1096 pub fn source_type(&self, id: &str) -> Result<Option<SourceTypeInfo>, ObsError> {
1097 Ok(self.source_types()?.into_iter().find(|info| info.id == id))
1098 }
1099
1100 pub fn output_type(&self, id: &str) -> Result<Option<OutputTypeInfo>, ObsError> {
1101 Ok(self.output_types()?.into_iter().find(|info| info.id == id))
1102 }
1103
1104 pub fn encoder_type(&self, id: &str) -> Result<Option<EncoderTypeInfo>, ObsError> {
1105 Ok(self.encoder_types()?.into_iter().find(|info| info.id == id))
1106 }
1107
1108 pub fn service_type(&self, id: &str) -> Result<Option<ServiceTypeInfo>, ObsError> {
1109 Ok(self.service_types()?.into_iter().find(|info| info.id == id))
1110 }
1111
1112 pub fn create_source(
1115 &self,
1116 source_type: &SourceTypeInfo,
1117 name: impl Into<ObsString>,
1118 settings: Option<ObsData>,
1119 ) -> Result<ObsSourceRef, ObsError> {
1120 self.runtime().ensure_same_runtime(&source_type.runtime)?;
1121 if source_type.kind == SourceKind::Filter {
1122 return Err(capability_kind_mismatch(
1123 &source_type.id,
1124 "source/input/transition",
1125 "filter",
1126 ));
1127 }
1128 ensure_settings_runtime(self.runtime(), settings.as_ref())?;
1129 ObsSourceRef::new_from_info(
1130 ObjectInfo::new(source_type.id.as_str(), name, settings, None),
1131 self.runtime().clone(),
1132 )
1133 }
1134
1135 pub fn create_filter(
1137 &mut self,
1138 filter_type: &SourceTypeInfo,
1139 name: impl Into<ObsString>,
1140 settings: Option<ObsData>,
1141 ) -> Result<ObsFilterRef, ObsError> {
1142 self.runtime().ensure_same_runtime(&filter_type.runtime)?;
1143 if filter_type.kind != SourceKind::Filter {
1144 return Err(capability_kind_mismatch(
1145 &filter_type.id,
1146 "filter",
1147 &format!("{:?}", filter_type.kind),
1148 ));
1149 }
1150 ensure_settings_runtime(self.runtime(), settings.as_ref())?;
1151 self.obs_filter(ObjectInfo::new(
1152 filter_type.id.as_str(),
1153 name,
1154 settings,
1155 None,
1156 ))
1157 }
1158
1159 pub fn create_output(
1161 &mut self,
1162 output_type: &OutputTypeInfo,
1163 name: impl Into<ObsString>,
1164 settings: Option<ObsData>,
1165 ) -> Result<ObsOutputRef, ObsError> {
1166 self.runtime().ensure_same_runtime(&output_type.runtime)?;
1167 ensure_settings_runtime(self.runtime(), settings.as_ref())?;
1168 self.output(ObjectInfo::new(
1169 output_type.id.as_str(),
1170 name,
1171 settings,
1172 None,
1173 ))
1174 }
1175
1176 pub fn create_video_encoder(
1178 &self,
1179 encoder_type: &EncoderTypeInfo,
1180 name: impl Into<ObsString>,
1181 settings: Option<ObsData>,
1182 ) -> Result<std::sync::Arc<ObsVideoEncoder>, ObsError> {
1183 self.runtime().ensure_same_runtime(&encoder_type.runtime)?;
1184 if encoder_type.kind != EncoderKind::Video {
1185 return Err(capability_kind_mismatch(
1186 &encoder_type.id,
1187 "video encoder",
1188 &format!("{:?}", encoder_type.kind),
1189 ));
1190 }
1191 ensure_settings_runtime(self.runtime(), settings.as_ref())?;
1192 ObsVideoEncoder::new_from_info(
1193 ObjectInfo::new(encoder_type.id.as_str(), name, settings, None),
1194 self.runtime().clone(),
1195 )
1196 }
1197
1198 pub fn create_audio_encoder(
1200 &self,
1201 encoder_type: &EncoderTypeInfo,
1202 name: impl Into<ObsString>,
1203 settings: Option<ObsData>,
1204 mixer_index: usize,
1205 ) -> Result<std::sync::Arc<ObsAudioEncoder>, ObsError> {
1206 self.runtime().ensure_same_runtime(&encoder_type.runtime)?;
1207 if encoder_type.kind != EncoderKind::Audio {
1208 return Err(capability_kind_mismatch(
1209 &encoder_type.id,
1210 "audio encoder",
1211 &format!("{:?}", encoder_type.kind),
1212 ));
1213 }
1214 ensure_settings_runtime(self.runtime(), settings.as_ref())?;
1215 ObsAudioEncoder::new_from_info(
1216 ObjectInfo::new(encoder_type.id.as_str(), name, settings, None),
1217 mixer_index,
1218 self.runtime().clone(),
1219 )
1220 }
1221
1222 pub fn create_service(
1224 &self,
1225 service_type: &ServiceTypeInfo,
1226 name: impl Into<ObsString>,
1227 settings: Option<ObsData>,
1228 ) -> Result<std::sync::Arc<ObsServiceRef>, ObsError> {
1229 self.runtime().ensure_same_runtime(&service_type.runtime)?;
1230 ensure_settings_runtime(self.runtime(), settings.as_ref())?;
1231 ObsServiceRef::new_from_info(
1232 ObjectInfo::new(service_type.id.as_str(), name, settings, None),
1233 self.runtime().clone(),
1234 )
1235 }
1236
1237 pub fn capabilities(&self) -> Result<ObsCapabilities, ObsError> {
1238 Ok(ObsCapabilities {
1239 source_types: self.source_types()?,
1240 input_types: self.input_types()?,
1241 filter_types: self.filter_types()?,
1242 transition_types: self.transition_types()?,
1243 outputs: self.output_types()?,
1244 encoders: self.encoder_types()?,
1245 services: self.service_types()?,
1246 protocols: self.protocols()?,
1247 modules: self.loaded_modules()?,
1248 })
1249 }
1250}
1251
1252type EnumTypeFn = unsafe extern "C" fn(usize, *mut *const c_char) -> bool;
1253type PropertiesFn = unsafe extern "C" fn(*const c_char) -> *mut libobs::obs_properties_t;
1254type DefaultsFn = unsafe extern "C" fn(*const c_char) -> *mut libobs::obs_data_t;
1255
1256#[allow(unknown_lints)]
1257#[allow(ensure_obs_call_in_runtime)]
1258unsafe fn enum_ids_on_actor(enum_fn: EnumTypeFn) -> Vec<String> {
1262 let mut result = Vec::new();
1263 let mut index = 0;
1264 loop {
1265 let mut id = ptr::null();
1266 if !unsafe { enum_fn(index, &mut id) } {
1268 break;
1269 }
1270 index += 1;
1271 if let Some(id) = unsafe { cstr_owned(id) } {
1273 result.push(id);
1274 }
1275 }
1276 result.sort();
1277 result.dedup();
1278 result
1279}
1280
1281fn discover_sources(runtime: &ObsRuntime) -> Result<Vec<SourceTypeInfo>, ObsError> {
1282 let runtime_for_result = runtime.clone();
1283 let rows = run_with_obs!(runtime, move || unsafe {
1285 let inputs: HashSet<_> = enum_ids_on_actor(libobs::obs_enum_input_types)
1286 .into_iter()
1287 .collect();
1288 let filters: HashSet<_> = enum_ids_on_actor(libobs::obs_enum_filter_types)
1289 .into_iter()
1290 .collect();
1291 let transitions: HashSet<_> = enum_ids_on_actor(libobs::obs_enum_transition_types)
1292 .into_iter()
1293 .collect();
1294 enum_ids_on_actor(libobs::obs_enum_source_types)
1295 .into_iter()
1296 .map(|id| {
1297 let c_id = ObsString::new(&id);
1298 let display_name = cstr_owned(libobs::obs_source_get_display_name(c_id.as_ptr().0));
1299 let output_flags = libobs::obs_get_source_output_flags(c_id.as_ptr().0);
1300 let kind = if inputs.contains(&id) {
1301 SourceKind::Input
1302 } else if filters.contains(&id) {
1303 SourceKind::Filter
1304 } else if transitions.contains(&id) {
1305 SourceKind::Transition
1306 } else {
1307 SourceKind::Other
1308 };
1309 (id, display_name, kind, output_flags)
1310 })
1311 .collect::<Vec<_>>()
1312 })?;
1313 Ok(rows
1314 .into_iter()
1315 .map(|(id, display_name, kind, output_flags)| SourceTypeInfo {
1316 id,
1317 display_name,
1318 kind,
1319 output_flags,
1320 runtime: runtime_for_result.clone(),
1321 })
1322 .collect())
1323}
1324
1325fn discover_source_category(
1326 runtime: &ObsRuntime,
1327 kind: SourceKind,
1328 enum_fn: EnumTypeFn,
1329) -> Result<Vec<SourceTypeInfo>, ObsError> {
1330 let runtime_for_result = runtime.clone();
1331 let rows = run_with_obs!(runtime, move || unsafe {
1333 enum_ids_on_actor(enum_fn)
1334 .into_iter()
1335 .map(|id| {
1336 let c_id = ObsString::new(&id);
1337 (
1338 id,
1339 cstr_owned(libobs::obs_source_get_display_name(c_id.as_ptr().0)),
1340 libobs::obs_get_source_output_flags(c_id.as_ptr().0),
1341 )
1342 })
1343 .collect::<Vec<_>>()
1344 })?;
1345 Ok(rows
1346 .into_iter()
1347 .map(|(id, display_name, output_flags)| SourceTypeInfo {
1348 id,
1349 display_name,
1350 kind,
1351 output_flags,
1352 runtime: runtime_for_result.clone(),
1353 })
1354 .collect())
1355}
1356
1357fn discover_outputs(runtime: &ObsRuntime) -> Result<Vec<OutputTypeInfo>, ObsError> {
1358 let runtime_for_result = runtime.clone();
1359 let rows = run_with_obs!(runtime, move || unsafe {
1361 let ids = enum_ids_on_actor(libobs::obs_enum_output_types);
1362 let mut protocols_by_output = HashMap::<String, Vec<String>>::new();
1363 let mut protocol_index = 0;
1364 loop {
1365 let mut protocol = ptr::null_mut();
1366 if !libobs::obs_enum_output_protocols(protocol_index, &mut protocol) {
1367 break;
1368 }
1369 protocol_index += 1;
1370 let Some(protocol_name) = cstr_owned(protocol.cast_const()) else {
1371 continue;
1372 };
1373 let mut matching_ids = HashSet::<String>::new();
1374 libobs::obs_enum_output_types_with_protocol(
1375 protocol.cast_const(),
1376 (&mut matching_ids as *mut HashSet<String>).cast::<c_void>(),
1377 Some(collect_output_type_id),
1378 );
1379 for id in matching_ids {
1380 protocols_by_output
1381 .entry(id)
1382 .or_default()
1383 .push(protocol_name.clone());
1384 }
1385 }
1386
1387 ids.into_iter()
1388 .map(|id| {
1389 let c_id = ObsString::new(&id);
1390 let mut protocols = protocols_by_output.remove(&id).unwrap_or_default();
1391 protocols.sort();
1392 protocols.dedup();
1393 (
1394 id,
1395 cstr_owned(libobs::obs_output_get_display_name(c_id.as_ptr().0)),
1396 split_capability_string(libobs::obs_get_output_supported_video_codecs(
1397 c_id.as_ptr().0,
1398 )),
1399 split_capability_string(libobs::obs_get_output_supported_audio_codecs(
1400 c_id.as_ptr().0,
1401 )),
1402 protocols,
1403 libobs::obs_get_output_flags(c_id.as_ptr().0),
1404 )
1405 })
1406 .collect::<Vec<_>>()
1407 })?;
1408 Ok(rows
1409 .into_iter()
1410 .map(
1411 |(id, display_name, video_codecs, audio_codecs, protocols, flags)| OutputTypeInfo {
1412 id,
1413 display_name,
1414 video_codecs,
1415 audio_codecs,
1416 protocols,
1417 flags,
1418 runtime: runtime_for_result.clone(),
1419 },
1420 )
1421 .collect())
1422}
1423
1424unsafe extern "C" fn collect_output_type_id(data: *mut c_void, id: *const c_char) -> bool {
1428 let Some(ids) = data.cast::<HashSet<String>>().as_mut() else {
1429 return false;
1430 };
1431 if let Some(id) = cstr_owned(id) {
1432 ids.insert(id);
1433 }
1434 true
1435}
1436
1437fn discover_encoders(runtime: &ObsRuntime) -> Result<Vec<EncoderTypeInfo>, ObsError> {
1438 let runtime_for_result = runtime.clone();
1439 let rows = run_with_obs!(runtime, move || unsafe {
1441 enum_ids_on_actor(libobs::obs_enum_encoder_types)
1442 .into_iter()
1443 .map(|id| {
1444 let c_id = ObsString::new(&id);
1445 let raw_kind = libobs::obs_get_encoder_type(c_id.as_ptr().0) as i64;
1446 let kind = if raw_kind == libobs::obs_encoder_type_OBS_ENCODER_AUDIO as i64 {
1447 EncoderKind::Audio
1448 } else if raw_kind == libobs::obs_encoder_type_OBS_ENCODER_VIDEO as i64 {
1449 EncoderKind::Video
1450 } else {
1451 EncoderKind::Unknown(raw_kind)
1452 };
1453 (
1454 id,
1455 cstr_owned(libobs::obs_encoder_get_display_name(c_id.as_ptr().0)),
1456 kind,
1457 cstr_owned(libobs::obs_get_encoder_codec(c_id.as_ptr().0)),
1458 libobs::obs_get_encoder_caps(c_id.as_ptr().0),
1459 )
1460 })
1461 .collect::<Vec<_>>()
1462 })?;
1463 Ok(rows
1464 .into_iter()
1465 .map(
1466 |(id, display_name, kind, codec, capabilities)| EncoderTypeInfo {
1467 id,
1468 display_name,
1469 kind,
1470 codec,
1471 capabilities,
1472 runtime: runtime_for_result.clone(),
1473 },
1474 )
1475 .collect())
1476}
1477
1478fn discover_services(runtime: &ObsRuntime) -> Result<Vec<ServiceTypeInfo>, ObsError> {
1479 let runtime_for_result = runtime.clone();
1480 let rows = run_with_obs!(runtime, move || unsafe {
1482 enum_ids_on_actor(libobs::obs_enum_service_types)
1483 .into_iter()
1484 .map(|id| {
1485 let c_id = ObsString::new(&id);
1486 (
1487 id,
1488 cstr_owned(libobs::obs_service_get_display_name(c_id.as_ptr().0)),
1489 )
1490 })
1491 .collect::<Vec<_>>()
1492 })?;
1493 Ok(rows
1494 .into_iter()
1495 .map(|(id, display_name)| ServiceTypeInfo {
1496 id,
1497 display_name,
1498 runtime: runtime_for_result.clone(),
1499 })
1500 .collect())
1501}
1502
1503fn discover_protocols(runtime: &ObsRuntime) -> Result<Vec<String>, ObsError> {
1504 run_with_obs!(runtime, move || unsafe {
1506 let mut result = Vec::new();
1507 let mut index = 0;
1508 loop {
1509 let mut protocol: *mut c_char = ptr::null_mut();
1510 if !libobs::obs_enum_output_protocols(index, &mut protocol) {
1511 break;
1512 }
1513 index += 1;
1514 if let Some(protocol) = cstr_owned(protocol.cast_const()) {
1515 result.push(protocol);
1516 }
1517 }
1518 result.sort();
1519 result.dedup();
1520 result
1521 })
1522}
1523
1524fn discover_modules(runtime: &ObsRuntime) -> Result<Vec<ModuleInfo>, ObsError> {
1525 #[allow(unknown_lints)]
1526 #[allow(ensure_obs_call_in_runtime)]
1527 unsafe extern "C" fn collect(param: *mut std::ffi::c_void, module: *mut libobs::obs_module_t) {
1531 let rows = unsafe { &mut *(param as *mut Vec<ModuleInfo>) };
1533 rows.push(ModuleInfo {
1534 file_name: unsafe { cstr_owned(libobs::obs_get_module_file_name(module)) },
1536 name: unsafe { cstr_owned(libobs::obs_get_module_name(module)) },
1538 author: unsafe { cstr_owned(libobs::obs_get_module_author(module)) },
1540 description: unsafe { cstr_owned(libobs::obs_get_module_description(module)) },
1542 binary_path: unsafe { cstr_owned(libobs::obs_get_module_binary_path(module)) },
1544 data_path: unsafe { cstr_owned(libobs::obs_get_module_data_path(module)) },
1546 });
1547 }
1548
1549 run_with_obs!(runtime, move || unsafe {
1551 let mut rows: Vec<ModuleInfo> = Vec::new();
1552 libobs::obs_enum_modules(Some(collect), (&mut rows as *mut Vec<ModuleInfo>).cast());
1555 rows.sort_by(|a, b| a.file_name.cmp(&b.file_name));
1556 rows
1557 })
1558}
1559
1560fn type_properties(
1561 runtime: &ObsRuntime,
1562 id: &str,
1563 getter: PropertiesFn,
1564) -> Result<Vec<PropertyMetadata>, ObsError> {
1565 let id = ObsString::new(id);
1566 run_with_obs!(runtime, (id), move || unsafe {
1568 let properties = getter(id.as_ptr().0);
1570 snapshot_owned_properties(properties)
1571 })
1572}
1573
1574fn type_properties_for_settings(
1575 runtime: &ObsRuntime,
1576 id: &str,
1577 getter: PropertiesFn,
1578 settings: &ObsData,
1579) -> Result<Vec<PropertyMetadata>, ObsError> {
1580 runtime.ensure_same_runtime(settings.runtime())?;
1581 let id = ObsString::new(id);
1582 let settings_ptr = settings.as_ptr();
1583 run_with_obs!(runtime, (id, settings_ptr), move || unsafe {
1584 let properties = getter(id.as_ptr().0);
1587 if properties.is_null() {
1588 return Vec::new();
1589 }
1590 libobs::obs_properties_apply_settings(properties, settings_ptr.get_ptr());
1591 snapshot_owned_properties(properties)
1592 })
1593}
1594
1595fn source_properties(runtime: &ObsRuntime, id: &str) -> Result<Vec<PropertyMetadata>, ObsError> {
1596 type_properties(runtime, id, libobs::obs_get_source_properties)
1597}
1598
1599fn source_properties_for_settings(
1600 runtime: &ObsRuntime,
1601 id: &str,
1602 settings: &ObsData,
1603) -> Result<Vec<PropertyMetadata>, ObsError> {
1604 type_properties_for_settings(runtime, id, libobs::obs_get_source_properties, settings)
1605}
1606
1607fn default_settings(
1608 runtime: &ObsRuntime,
1609 id: &str,
1610 getter: DefaultsFn,
1611) -> Result<Option<ImmutableObsData>, ObsError> {
1612 let id = ObsString::new(id);
1613 let ptr = run_with_obs!(runtime, (id), move || unsafe {
1615 Sendable(getter(id.as_ptr().0))
1617 })?;
1618 if ptr.0.is_null() {
1619 Ok(None)
1620 } else {
1621 Ok(Some(ImmutableObsData::from_raw_pointer(
1622 ptr,
1623 runtime.clone(),
1624 )))
1625 }
1626}
1627
1628fn mutable_default_settings(
1629 runtime: &ObsRuntime,
1630 id: &str,
1631 getter: DefaultsFn,
1632) -> Result<ObsData, ObsError> {
1633 let id = ObsString::new(id);
1634 let ptr = run_with_obs!(runtime, (id), move || unsafe {
1637 Sendable(getter(id.as_ptr().0))
1638 })?;
1639 if ptr.0.is_null() {
1640 ObsData::new(runtime.clone())
1641 } else {
1642 Ok(ObsData::from_raw_pointer(ptr, runtime.clone()))
1643 }
1644}
1645
1646fn ensure_settings_runtime(
1647 runtime: &ObsRuntime,
1648 settings: Option<&ObsData>,
1649) -> Result<(), ObsError> {
1650 if let Some(settings) = settings {
1651 runtime.ensure_same_runtime(settings.runtime())?;
1652 }
1653 Ok(())
1654}
1655
1656fn capability_kind_mismatch(id: &str, expected: &str, actual: &str) -> ObsError {
1657 ObsError::CapabilityKindMismatch {
1658 id: id.to_owned(),
1659 expected: expected.to_owned(),
1660 actual: actual.to_owned(),
1661 }
1662}
1663
1664#[allow(unknown_lints)]
1665#[allow(ensure_obs_call_in_runtime)]
1666unsafe fn snapshot_owned_properties(
1670 properties: *mut libobs::obs_properties_t,
1671) -> Vec<PropertyMetadata> {
1672 if properties.is_null() {
1673 return Vec::new();
1674 }
1675 let result = unsafe { snapshot_properties(properties) };
1677 unsafe { libobs::obs_properties_destroy(properties) };
1679 result
1680}
1681
1682#[allow(unknown_lints)]
1683#[allow(ensure_obs_call_in_runtime)]
1684unsafe fn snapshot_properties(properties: *mut libobs::obs_properties_t) -> Vec<PropertyMetadata> {
1688 let mut result = Vec::new();
1689 let mut property = unsafe { libobs::obs_properties_first(properties) };
1691 while !property.is_null() {
1692 let name = unsafe { cstr_owned(libobs::obs_property_name(property)) }.unwrap_or_default();
1694 let description = unsafe { cstr_owned(libobs::obs_property_description(property)) };
1696 let long_description =
1697 unsafe { cstr_owned(libobs::obs_property_long_description(property)) };
1699 let enabled = unsafe { libobs::obs_property_enabled(property) };
1701 let visible = unsafe { libobs::obs_property_visible(property) };
1703 let raw_type = unsafe { libobs::obs_property_get_type(property) } as i64;
1705 let kind = unsafe { snapshot_property_kind(property, raw_type) };
1707 result.push(PropertyMetadata {
1708 name,
1709 description,
1710 long_description,
1711 enabled,
1712 visible,
1713 kind,
1714 });
1715 if !unsafe { libobs::obs_property_next(&mut property) } {
1717 break;
1718 }
1719 }
1720 result
1721}
1722
1723#[allow(unknown_lints)]
1724#[allow(ensure_obs_call_in_runtime)]
1725unsafe fn snapshot_property_kind(
1729 property: *mut libobs::obs_property_t,
1730 raw_type: i64,
1731) -> PropertyKind {
1732 if raw_type == libobs::obs_property_type_OBS_PROPERTY_INVALID as i64 {
1733 PropertyKind::Invalid
1734 } else if raw_type == libobs::obs_property_type_OBS_PROPERTY_BOOL as i64 {
1735 PropertyKind::Bool
1736 } else if raw_type == libobs::obs_property_type_OBS_PROPERTY_INT as i64 {
1737 PropertyKind::Integer {
1738 min: unsafe { libobs::obs_property_int_min(property) },
1740 max: unsafe { libobs::obs_property_int_max(property) },
1742 step: unsafe { libobs::obs_property_int_step(property) },
1744 suffix: unsafe { cstr_owned(libobs::obs_property_int_suffix(property)) },
1746 control: NumberControl::from_raw(
1747 unsafe { libobs::obs_property_int_type(property) } as i64,
1749 ),
1750 }
1751 } else if raw_type == libobs::obs_property_type_OBS_PROPERTY_FLOAT as i64 {
1752 PropertyKind::Float {
1753 min: unsafe { libobs::obs_property_float_min(property) },
1755 max: unsafe { libobs::obs_property_float_max(property) },
1757 step: unsafe { libobs::obs_property_float_step(property) },
1759 suffix: unsafe { cstr_owned(libobs::obs_property_float_suffix(property)) },
1761 control: NumberControl::from_raw(
1762 unsafe { libobs::obs_property_float_type(property) } as i64,
1764 ),
1765 }
1766 } else if raw_type == libobs::obs_property_type_OBS_PROPERTY_TEXT as i64 {
1767 PropertyKind::Text {
1768 text_type: TextType::from_raw(
1769 unsafe { libobs::obs_property_text_type(property) } as i64,
1771 ),
1772 monospace: unsafe { libobs::obs_property_text_monospace(property) },
1774 info_type: TextInfoType::from_raw(unsafe {
1776 libobs::obs_property_text_info_type(property)
1777 } as i64),
1778 word_wrap: unsafe { libobs::obs_property_text_info_word_wrap(property) },
1780 }
1781 } else if raw_type == libobs::obs_property_type_OBS_PROPERTY_PATH as i64 {
1782 PropertyKind::Path {
1783 path_type: PathType::from_raw(
1784 unsafe { libobs::obs_property_path_type(property) } as i64,
1786 ),
1787 filter: unsafe { cstr_owned(libobs::obs_property_path_filter(property)) },
1789 default_path: unsafe { cstr_owned(libobs::obs_property_path_default_path(property)) },
1791 }
1792 } else if raw_type == libobs::obs_property_type_OBS_PROPERTY_LIST as i64 {
1793 let format_raw = unsafe { libobs::obs_property_list_format(property) } as i64;
1795 let count = unsafe { libobs::obs_property_list_item_count(property) };
1797 let mut items = Vec::with_capacity(count);
1798 for index in 0..count {
1799 let name = unsafe { cstr_owned(libobs::obs_property_list_item_name(property, index)) }
1801 .unwrap_or_default();
1802 let value = if format_raw == libobs::obs_combo_format_OBS_COMBO_FORMAT_STRING as i64 {
1803 ListValue::String(
1804 unsafe { cstr_owned(libobs::obs_property_list_item_string(property, index)) }
1806 .unwrap_or_default(),
1807 )
1808 } else if format_raw == libobs::obs_combo_format_OBS_COMBO_FORMAT_INT as i64 {
1809 ListValue::Int(unsafe { libobs::obs_property_list_item_int(property, index) })
1811 } else if format_raw == libobs::obs_combo_format_OBS_COMBO_FORMAT_FLOAT as i64 {
1812 ListValue::Float(unsafe { libobs::obs_property_list_item_float(property, index) })
1814 } else if format_raw == libobs::obs_combo_format_OBS_COMBO_FORMAT_BOOL as i64 {
1815 ListValue::Bool(unsafe { libobs::obs_property_list_item_bool(property, index) })
1817 } else {
1818 ListValue::UnknownFormat(format_raw)
1819 };
1820 items.push(ListItem {
1821 name,
1822 value,
1823 disabled: unsafe { libobs::obs_property_list_item_disabled(property, index) },
1825 });
1826 }
1827 PropertyKind::List {
1828 list_type: ListType::from_raw(
1829 unsafe { libobs::obs_property_list_type(property) } as i64,
1831 ),
1832 format: ListFormat::from_raw(format_raw),
1833 items,
1834 }
1835 } else if raw_type == libobs::obs_property_type_OBS_PROPERTY_COLOR as i64 {
1836 PropertyKind::Color
1837 } else if raw_type == libobs::obs_property_type_OBS_PROPERTY_BUTTON as i64 {
1838 PropertyKind::Button {
1839 button_type: ButtonType::from_raw(
1840 unsafe { libobs::obs_property_button_type(property) } as i64,
1842 ),
1843 url: unsafe { cstr_owned(libobs::obs_property_button_url(property)) },
1845 }
1846 } else if raw_type == libobs::obs_property_type_OBS_PROPERTY_FONT as i64 {
1847 PropertyKind::Font
1848 } else if raw_type == libobs::obs_property_type_OBS_PROPERTY_EDITABLE_LIST as i64 {
1849 PropertyKind::EditableList {
1850 list_type: EditableListType::from_raw(unsafe {
1852 libobs::obs_property_editable_list_type(property)
1853 } as i64),
1854 filter: unsafe { cstr_owned(libobs::obs_property_editable_list_filter(property)) },
1856 default_path: unsafe {
1858 cstr_owned(libobs::obs_property_editable_list_default_path(property))
1859 },
1860 }
1861 } else if raw_type == libobs::obs_property_type_OBS_PROPERTY_FRAME_RATE as i64 {
1862 let option_count = unsafe { libobs::obs_property_frame_rate_options_count(property) };
1864 let mut options = Vec::with_capacity(option_count);
1865 for index in 0..option_count {
1866 options.push(FrameRateOption {
1867 name: unsafe {
1869 cstr_owned(libobs::obs_property_frame_rate_option_name(property, index))
1870 },
1871 description: unsafe {
1873 cstr_owned(libobs::obs_property_frame_rate_option_description(
1874 property, index,
1875 ))
1876 },
1877 });
1878 }
1879 let range_count = unsafe { libobs::obs_property_frame_rate_fps_ranges_count(property) };
1881 let mut ranges = Vec::with_capacity(range_count);
1882 for index in 0..range_count {
1883 let min = unsafe { libobs::obs_property_frame_rate_fps_range_min(property, index) };
1885 let max = unsafe { libobs::obs_property_frame_rate_fps_range_max(property, index) };
1887 ranges.push(FrameRateRange {
1888 min: FrameRate {
1889 numerator: min.numerator,
1890 denominator: min.denominator,
1891 },
1892 max: FrameRate {
1893 numerator: max.numerator,
1894 denominator: max.denominator,
1895 },
1896 });
1897 }
1898 PropertyKind::FrameRate { options, ranges }
1899 } else if raw_type == libobs::obs_property_type_OBS_PROPERTY_GROUP as i64 {
1900 let content = unsafe { libobs::obs_property_group_content(property) };
1902 PropertyKind::Group {
1903 group_type: GroupType::from_raw(
1904 unsafe { libobs::obs_property_group_type(property) } as i64,
1906 ),
1907 properties: if content.is_null() {
1908 Vec::new()
1909 } else {
1910 unsafe { snapshot_properties(content) }
1912 },
1913 }
1914 } else if raw_type == libobs::obs_property_type_OBS_PROPERTY_COLOR_ALPHA as i64 {
1915 PropertyKind::ColorAlpha
1916 } else {
1917 PropertyKind::Unknown(raw_type)
1918 }
1919}
1920
1921unsafe fn cstr_owned(ptr: *const c_char) -> Option<String> {
1924 if ptr.is_null() {
1925 None
1926 } else {
1927 Some(
1928 unsafe { CStr::from_ptr(ptr) }
1930 .to_string_lossy()
1931 .into_owned(),
1932 )
1933 }
1934}
1935
1936fn capability_list_supports(values: &[String], requested: &str) -> bool {
1939 values.is_empty()
1940 || values
1941 .iter()
1942 .any(|value| value.eq_ignore_ascii_case(requested))
1943}
1944
1945fn split_capability_string(ptr: *const c_char) -> Vec<String> {
1946 let Some(value) = (unsafe { cstr_owned(ptr) }) else {
1948 return Vec::new();
1949 };
1950 value
1951 .split(|c: char| c == ',' || c == ';' || c.is_ascii_whitespace())
1952 .filter(|part| !part.is_empty())
1953 .map(str::to_owned)
1954 .collect()
1955}
1956
1957#[cfg(test)]
1958mod tests {
1959 use super::*;
1960
1961 #[test]
1962 fn future_enum_values_are_preserved() {
1963 assert_eq!(
1964 NumberControl::from_raw(99_999),
1965 NumberControl::Unknown(99_999)
1966 );
1967 assert_eq!(ListFormat::from_raw(-7), ListFormat::Unknown(-7));
1968 }
1969
1970 #[test]
1971 fn capability_string_parser_is_owned_and_tolerant() {
1972 let text = std::ffi::CString::new("h264, hevc;av1").unwrap();
1973 let parsed = split_capability_string(text.as_ptr());
1975 assert_eq!(parsed, ["h264", "hevc", "av1"]);
1976 }
1977}