@@ -73,9 +73,13 @@ public WebcamCapture(string name, string? deviceId = null)
7373 /// Returns null if no capture devices are present on the system.
7474 /// </summary>
7575 /// <param name="name">Optional source name.</param>
76- public static WebcamCapture ? FromDefault ( string ? name = null )
76+ /// <param name="includeVirtualDevices">
77+ /// If true, also consider virtual / proxy entries that have no DirectShow device path
78+ /// (Meta Quest companion app, NVIDIA Broadcast, OBS Virtual Camera, etc.). Default false.
79+ /// </param>
80+ public static WebcamCapture ? FromDefault ( string ? name = null , bool includeVirtualDevices = false )
7781 {
78- var device = ListDevices ( ) . FirstOrDefault ( ) ;
82+ var device = ListDevices ( includeVirtualDevices ) . FirstOrDefault ( ) ;
7983 if ( device == null )
8084 return null ;
8185 return new WebcamCapture ( name ?? device . Name , device . DeviceId ) ;
@@ -87,10 +91,14 @@ public WebcamCapture(string name, string? deviceId = null)
8791 /// </summary>
8892 /// <param name="nameSubstring">Substring to match against device names.</param>
8993 /// <param name="name">Optional source name; defaults to the matched device's name.</param>
94+ /// <param name="includeVirtualDevices">
95+ /// If true, virtual / proxy entries (Meta Quest, NVIDIA Broadcast, OBS Virtual Camera, ...)
96+ /// are also eligible for matching. Default false.
97+ /// </param>
9098 /// <returns>A configured WebcamCapture, or null if no device matches.</returns>
91- public static WebcamCapture ? FromDeviceName ( string nameSubstring , string ? name = null )
99+ public static WebcamCapture ? FromDeviceName ( string nameSubstring , string ? name = null , bool includeVirtualDevices = false )
92100 {
93- var device = ListDevices ( ) . FirstOrDefault ( d =>
101+ var device = ListDevices ( includeVirtualDevices ) . FirstOrDefault ( d =>
94102 d . Name . Contains ( nameSubstring , StringComparison . OrdinalIgnoreCase ) ) ;
95103 if ( device == null )
96104 return null ;
@@ -102,7 +110,12 @@ public WebcamCapture(string name, string? deviceId = null)
102110 /// a temporary dshow_input source and asking OBS to populate its property list, which is
103111 /// the same code path the OBS UI uses for the device dropdown.
104112 /// </summary>
105- public static IReadOnlyList < WebcamDeviceInfo > ListDevices ( )
113+ /// <param name="includeVirtualDevices">
114+ /// If true, also return virtual / proxy entries that have no DirectShow device path —
115+ /// e.g. Meta Quest companion app, NVIDIA Broadcast, OBS Virtual Camera. These are usually
116+ /// not openable in a headless app, so they are excluded by default.
117+ /// </param>
118+ public static IReadOnlyList < WebcamDeviceInfo > ListDevices ( bool includeVirtualDevices = false )
106119 {
107120 // The dshow plugin only populates the device list when an instance exists, so we
108121 // create a private (un-saved) source just for the property query and dispose it.
@@ -114,6 +127,19 @@ public static IReadOnlyList<WebcamDeviceInfo> ListDevices()
114127 {
115128 if ( string . IsNullOrEmpty ( itemValue ) )
116129 continue ;
130+
131+ // OBS encodes device ids as "Name:Path" (the dshow plugin escapes any literal
132+ // ':' and '#' inside the components, so the first ':' is always the separator).
133+ // Entries with an empty path component are virtual / proxy devices (Meta Quest
134+ // companion app, NVIDIA Broadcast, OBS Virtual Camera, ...). Skip them unless
135+ // the caller opts in.
136+ if ( ! includeVirtualDevices && OperatingSystem . IsWindows ( ) )
137+ {
138+ var sep = itemValue . IndexOf ( ':' ) ;
139+ if ( sep < 0 || sep == itemValue . Length - 1 )
140+ continue ;
141+ }
142+
117143 result . Add ( new WebcamDeviceInfo ( itemName , itemValue ) ) ;
118144 }
119145 return result ;
0 commit comments