@@ -12,13 +12,287 @@ public readonly record struct CameraResolution(int Width, int Height)
1212 public override string ToString ( ) => $ "{ Width } x{ Height } ";
1313 }
1414
15+ public static IReadOnlyList < CameraResolution > GetSuggestedResolutionsByIndex ( int deviceIndex )
16+ {
17+ var max = GetMaximumResolutionByIndex ( deviceIndex ) ;
18+ if ( max . Width <= 0 || max . Height <= 0 )
19+ {
20+ max = new CameraResolution ( 1920 , 1080 ) ;
21+ }
22+
23+ var result = new HashSet < CameraResolution > ( ) ;
24+ result . Add ( NormalizeEven ( max ) ) ;
25+
26+ var scalePercents = new [ ] { 100 , 75 , 67 , 50 , 40 , 33 , 25 } ;
27+ foreach ( var percent in scalePercents )
28+ {
29+ if ( percent == 100 )
30+ {
31+ continue ;
32+ }
33+
34+ var w = ( int ) Math . Round ( max . Width * ( percent / 100.0 ) ) ;
35+ var h = ( int ) Math . Round ( max . Height * ( percent / 100.0 ) ) ;
36+ var scaled = NormalizeEven ( new CameraResolution ( w , h ) ) ;
37+ if ( scaled . Width >= 320 && scaled . Height >= 240 )
38+ {
39+ result . Add ( scaled ) ;
40+ }
41+ }
42+
43+ foreach ( var targetHeight in new [ ] { 2160 , 1440 , 1080 , 720 , 540 , 480 , 360 , 240 } )
44+ {
45+ if ( targetHeight >= max . Height )
46+ {
47+ continue ;
48+ }
49+
50+ var w = ( int ) Math . Round ( max . Width * ( targetHeight / ( double ) max . Height ) ) ;
51+ var h = targetHeight ;
52+ var scaled = NormalizeEven ( new CameraResolution ( w , h ) ) ;
53+ if ( scaled . Width >= 320 && scaled . Height >= 240 )
54+ {
55+ result . Add ( scaled ) ;
56+ }
57+ }
58+
59+ return result
60+ . OrderByDescending ( x => ( long ) x . Width * x . Height )
61+ . ThenByDescending ( x => x . Width )
62+ . ThenByDescending ( x => x . Height )
63+ . ToList ( ) ;
64+ }
65+
66+ public static CameraResolution GetMaximumResolutionByIndex ( int deviceIndex )
67+ {
68+ using var _ = new ComInitScope ( ) ;
69+
70+ var list = GetResolutionsByIndex ( deviceIndex ) ;
71+ if ( list . Count == 0 )
72+ {
73+ var current = GetCurrentResolutionByIndex ( deviceIndex ) ;
74+ if ( current . Width > 0 && current . Height > 0 )
75+ {
76+ return current ;
77+ }
78+
79+ return default ;
80+ }
81+
82+ CameraResolution best = default ;
83+ long bestArea = - 1 ;
84+ foreach ( var r in list )
85+ {
86+ if ( r . Width <= 0 || r . Height <= 0 )
87+ {
88+ continue ;
89+ }
90+
91+ var area = ( long ) r . Width * r . Height ;
92+ if ( area > bestArea )
93+ {
94+ best = r ;
95+ bestArea = area ;
96+ }
97+ }
98+
99+ return best ;
100+ }
101+
102+ private static CameraResolution NormalizeEven ( CameraResolution resolution )
103+ {
104+ var w = resolution . Width ;
105+ var h = resolution . Height ;
106+ if ( w <= 0 || h <= 0 )
107+ {
108+ return default ;
109+ }
110+
111+ if ( ( w & 1 ) == 1 )
112+ {
113+ w -- ;
114+ }
115+
116+ if ( ( h & 1 ) == 1 )
117+ {
118+ h -- ;
119+ }
120+
121+ if ( w <= 0 || h <= 0 )
122+ {
123+ return default ;
124+ }
125+
126+ return new CameraResolution ( w , h ) ;
127+ }
128+
129+ private static CameraResolution GetCurrentResolutionByIndex ( int deviceIndex )
130+ {
131+ if ( ! OperatingSystem . IsWindows ( ) )
132+ {
133+ return default ;
134+ }
135+
136+ if ( deviceIndex < 0 )
137+ {
138+ return default ;
139+ }
140+
141+ var devEnumType = Type . GetTypeFromCLSID ( Clsid . SystemDeviceEnum ) ;
142+ if ( devEnumType is null )
143+ {
144+ return default ;
145+ }
146+
147+ var createDevEnum = ( ICreateDevEnum ? ) Activator . CreateInstance ( devEnumType ) ;
148+ if ( createDevEnum is null )
149+ {
150+ return default ;
151+ }
152+
153+ IEnumMoniker ? enumMoniker = null ;
154+ IBaseFilter ? filter = null ;
155+ IEnumPins ? enumPins = null ;
156+ IPin ? pin = null ;
157+
158+ try
159+ {
160+ var category = FilterCategory . VideoInputDevice ;
161+ var hr = createDevEnum . CreateClassEnumerator ( in category , out enumMoniker , 0 ) ;
162+ if ( hr != 0 || enumMoniker is null )
163+ {
164+ return default ;
165+ }
166+
167+ var targetMoniker = GetMonikerByIndex ( enumMoniker , deviceIndex ) ;
168+ if ( targetMoniker is null )
169+ {
170+ return default ;
171+ }
172+
173+ try
174+ {
175+ var filterGuid = typeof ( IBaseFilter ) . GUID ;
176+ targetMoniker . BindToObject ( null , null , in filterGuid , out var filterObj ) ;
177+ filter = filterObj as IBaseFilter ;
178+ if ( filter is null )
179+ {
180+ if ( filterObj is not null )
181+ {
182+ Marshal . ReleaseComObject ( filterObj ) ;
183+ }
184+ return default ;
185+ }
186+
187+ filter . EnumPins ( out enumPins ) ;
188+ if ( enumPins is null )
189+ {
190+ return default ;
191+ }
192+
193+ var pins = new IPin [ 1 ] ;
194+ while ( enumPins . Next ( 1 , pins , IntPtr . Zero ) == 0 )
195+ {
196+ pin = pins [ 0 ] ;
197+ try
198+ {
199+ pin . QueryDirection ( out var direction ) ;
200+ if ( direction != PinDirection . Output )
201+ {
202+ continue ;
203+ }
204+
205+ if ( ! TryGetStreamConfig ( pin , out var streamConfig ) || streamConfig is null )
206+ {
207+ continue ;
208+ }
209+
210+ try
211+ {
212+ var fmtHr = streamConfig . GetFormat ( out var pmtPtr ) ;
213+ if ( fmtHr != 0 || pmtPtr == IntPtr . Zero )
214+ {
215+ continue ;
216+ }
217+
218+ try
219+ {
220+ var mt = Marshal . PtrToStructure < AMMediaType > ( pmtPtr ) ;
221+ try
222+ {
223+ if ( TryParseResolution ( mt , out var resolution ) &&
224+ resolution . Width > 0 &&
225+ resolution . Height > 0 )
226+ {
227+ return resolution ;
228+ }
229+ }
230+ finally
231+ {
232+ FreeMediaType ( ref mt ) ;
233+ }
234+ }
235+ finally
236+ {
237+ Marshal . FreeCoTaskMem ( pmtPtr ) ;
238+ }
239+ }
240+ finally
241+ {
242+ Marshal . ReleaseComObject ( streamConfig ) ;
243+ }
244+ }
245+ finally
246+ {
247+ Marshal . ReleaseComObject ( pin ) ;
248+ pin = null ;
249+ }
250+
251+ pins = new IPin [ 1 ] ;
252+ }
253+
254+ return default ;
255+ }
256+ finally
257+ {
258+ Marshal . ReleaseComObject ( targetMoniker ) ;
259+ }
260+ }
261+ catch
262+ {
263+ return default ;
264+ }
265+ finally
266+ {
267+ if ( pin is not null )
268+ {
269+ Marshal . ReleaseComObject ( pin ) ;
270+ }
271+ if ( enumPins is not null )
272+ {
273+ Marshal . ReleaseComObject ( enumPins ) ;
274+ }
275+ if ( filter is not null )
276+ {
277+ Marshal . ReleaseComObject ( filter ) ;
278+ }
279+ if ( enumMoniker is not null )
280+ {
281+ Marshal . ReleaseComObject ( enumMoniker ) ;
282+ }
283+ Marshal . ReleaseComObject ( createDevEnum ) ;
284+ }
285+ }
286+
15287 public static IReadOnlyList < string > GetCameraNames ( )
16288 {
17289 if ( ! OperatingSystem . IsWindows ( ) )
18290 {
19291 return [ ] ;
20292 }
21293
294+ using var _ = new ComInitScope ( ) ;
295+
22296 var devEnumType = Type . GetTypeFromCLSID ( Clsid . SystemDeviceEnum ) ;
23297 if ( devEnumType is null )
24298 {
@@ -96,6 +370,8 @@ public static IReadOnlyList<CameraResolution> GetResolutionsByIndex(int deviceIn
96370 return [ ] ;
97371 }
98372
373+ using var _ = new ComInitScope ( ) ;
374+
99375 try
100376 {
101377 return EnumerateDirectShowResolutions ( deviceIndex ) ;
@@ -281,6 +557,12 @@ private static IReadOnlyList<CameraResolution> EnumerateDirectShowResolutions(in
281557
282558 private static bool TryGetStreamConfig ( IPin pin , out IAMStreamConfig ? streamConfig )
283559 {
560+ if ( pin is IAMStreamConfig directCast )
561+ {
562+ streamConfig = directCast ;
563+ return true ;
564+ }
565+
284566 streamConfig = null ;
285567 var iid = typeof ( IAMStreamConfig ) . GUID ;
286568 var unk = Marshal . GetIUnknownForObject ( pin ) ;
@@ -310,6 +592,43 @@ private static bool TryGetStreamConfig(IPin pin, out IAMStreamConfig? streamConf
310592 }
311593 }
312594
595+ private sealed class ComInitScope : IDisposable
596+ {
597+ private readonly bool _initialized ;
598+
599+ public ComInitScope ( )
600+ {
601+ if ( ! OperatingSystem . IsWindows ( ) )
602+ {
603+ _initialized = false ;
604+ return ;
605+ }
606+
607+ var hr = CoInitializeEx ( IntPtr . Zero , CoInit . MultiThreaded ) ;
608+ _initialized = hr == 0 || hr == 1 ;
609+ }
610+
611+ public void Dispose ( )
612+ {
613+ if ( _initialized )
614+ {
615+ CoUninitialize ( ) ;
616+ }
617+ }
618+ }
619+
620+ [ Flags ]
621+ private enum CoInit : uint
622+ {
623+ MultiThreaded = 0x0
624+ }
625+
626+ [ DllImport ( "ole32.dll" ) ]
627+ private static extern int CoInitializeEx ( IntPtr pvReserved , CoInit dwCoInit ) ;
628+
629+ [ DllImport ( "ole32.dll" ) ]
630+ private static extern void CoUninitialize ( ) ;
631+
313632 private static string ? TryGetMonikerFriendlyName ( IMoniker moniker )
314633 {
315634 IPropertyBag ? bag = null ;
0 commit comments