Archives

Archives / 2020 / December
  • How to Access Webcam Properties from C#

    My Logitech C920 webcam requires some tweaking of settings like exposure, gain, focus, etc. to get good image quality. I uninstalled the “feature rich” Logitech software and now change the settings using the bare-bones Windows webcam properties dialog. This works well for me; unfortunately, the settings are not persisted reliably. After a cold-boot, or sometimes after simply starting an application that uses the webcam, I have to make the adjustments again.

    That is why I became interested in what would it take to read and write the webcam properties and to open the properties dialog from C#. The result of a web search was a bit intimidating as I came across multimedia APIs that go way beyond what I intended to do. After all, I only wanted to access the properties, not write a full-blown video capture suite.

    In the end I settled on DirectShow.Net, a C# wrapper around DirectShow under LPGL license. Even though DirectShow is an old API and the DirectShow.Net project seems to be no longer active, I found one very important reason to use it: A working sample that opened the webcam properties dialog.

    This blog post starts with a guided tour through the sample, with the intended side-effect of making the sample easier to discover on the web. Additionally, I will describe how to access the properties from your code.

    Step 1: Download DirectShow.Net

    • Visit  http://directshownet.sourceforge.net/
    • Go to “Downloads”
    • Download the latest version of the library (DirectShowLibV2-1.zip at the time of this writing)
    • Download the samples (DirectShowSamples-2010-February.zip)
    • Unpack the ZIP files so that the folder Samples is in the same directory as Docs, lib and src.
      • The lib folder contains the file DirectShowLib-2005.dll which the samples reference.

    Step 2: Run the “DxPropPages” demog

    • Open Samples\Capture\DxPropPages\DxPropPages-2008.sln in Visual Studio and let the “One-way upgrade” finish.
    • Run the project.
    • In the window that appears,
      • select your webcam and
      • press the “Show property pages” button.

       
    • On my computer, a properties dialog with two tabs appears. Depending on your drivers/webcam software, the dialog may have been replaced. But the generic, low-level dialog looks like this:



      (Remember “Video Proc Amp” and “Camera Control”, we will come across these names later)

    Step 3: Look at the code

    • Stop the program.
    • Open the code view of the file Form1.cs.
    • Set a breakpoint at the start of
      • the constructor e Form1(),
      • the comboBox1_SelectedIndexChanged() event handler, and
      • the DisplayPropertyPage() method.
    • Run the program in the debugger.

    How to get the available webcam(s)

    When the first breakpoint is hit, you will see the following lines:

    foreach (DsDevice ds in DsDevice.GetDevicesOfCat(FilterCategory.VideoInputDevice))
    {
        comboBox1.Items.Add(ds.Name);
    }

    The code gets all available “video input devices” (which include webcams) and fills the dropdown that you used in step 2 to choose your webcam.

    A DsDevice instance has two important properties for identifying a device:

    • Name returns a human-readable name (i.e., what you saw in the dropdown list)
    • DevicePath returns a unique identifier.

    At this point, the sample does not store the instances, only the names, even though we need the DsDevice instance for the selected webcam later. I am not sure whether there is a reason for this other than keeping the sample code short and to be able to re-use the CreateFilter() method (which we will look at soon).

    How to open the properties dialog

    Now continue to run the program. The comboBox1_SelectedIndexChanged event handler gets called automatically during startup. If your webcam is not the first device, let the program continue and select the webcam in the dropdown.

    After the breakpoint has been hit, look at the code.

    • The purpose of the event handler is to set the field theDevice (of type IBaseFilter) which we need later.
    • The call of Marshal.ReleaseComObject(theDevice) when switching between devices is a reminder that we are dealing with COM and its reference counting (instead of relying on garbage collection).
    • Note that the variable name devicepath is misleading; the dropdown contains the display names of the devices. This becomes clear when we look at the CreateFilter() method: The second parameter is called friendlyname which is more appropriate.

    Inside the CreateFilter() method, some “COM stuff” happens. The important bit for us is that the returned IBaseFilter is assigned to the field theDevice, which is used in the button1_Click handler when calling DisplayPropertyPage().

    The method DisplayPropertyPage() contains even more COM stuff that we can ignore for now, because the method does exactly what its name says. We will see later that we need some basic understanding what is happening inside, though.

    How to make the controls in the dialog appear “Windows 10”-like

    The steps described my blog post “Windows 10 Theme for a Properties Dialog” for a WPF application are also valid for WinForms. In the case of the sample application the change also affects the controls of the main window.

    Step 4: Start to experiment

    The code inside DisplayPropertyPage() uses the ISpecifyPropertyPages interface. Two other interesting interfaces are IAMVideoProcAmp and IAMCameraControl. The names correspond to the pages of the properties dialog. Using the two interfaces, you can access the properties you see in the dialog.

    How to read or write the webcam properties from your code

    The interfaces IAMVideoProcAmp and IAMCameraControl both offer GetRange(), Get() and Set() methods.

    For IAMCameraControl, these methods are defined like this:

    int GetRange(
    	[In] CameraControlProperty Property,
    	[Out] out int pMin,
    	[Out] out int pMax,
    	[Out] out int pSteppingDelta,
    	[Out] out int pDefault,
    	[Out] out CameraControlFlags pCapsFlags
    	);
    
    int Set(
    	[In] CameraControlProperty Property,
    	[In] int lValue,
    	[In] CameraControlFlags Flags
    	);
    
    int Get(
    	[In] CameraControlProperty Property,
    	[Out] out int lValue,
    	[Out] out CameraControlFlags Flags
    	);

    When using the methods:

    • You specify the property you want to access via an enum value of type CameraControlProperty. Your device may not support all properties, though – if you look at the screenshots above, you will notice that some sliders are disabled. Therefore it is important to check the return value to be 0 (zero) for a successful call.
    • The CameraControlFlags value contains information whether the property is (or should be) set automatically and / or manually.

    Let us say you want to access the “exposure” property of your webcam (this may or may not work on your webcam; if not, you can try another property).

    For a quick-and-dirty test, resize the “Show property pages” button so can add another button next to it, double click the new button and insert the following code into the “Click” event handler:

    var cameraControl = theDevice as IAMCameraControl;
    if (cameraControl == null) return;
    
    cameraControl.GetRange(CameraControlProperty.Exposure,
    	out int min, out int max, out int steppingDelta,
    	out int defaultValue, out var flags);
    
    Debug.WriteLine($"min: {min}, max: {max}, steppingDelta: {steppingDelta}");
    Debug.WriteLine($"defaultValue: {defaultValue}, flags: {flags}");

    When I run the program, select my Logitech C920 webcam and press the button I added above, the following appears in the debug output window in Visual Studio:

    min: -11, max: -2, steppingDelta: 1
    defaultValue: -5, flags: Auto, Manual

    This means that the exposure can be adjusted from -11 to -2, with -5 being the default. The property supports both automatic and manual mode.

    Not all properties have a stepping delta of 1. For the Logitech C920, for instance, the focus property (CameraControlProperty.Focus) has a range from 0 to 250 with a stepping delta of 5. This is why setting the property value to e.g. 47 has the same effect on the hardware as setting the value to 45.

    Calling the Get() and Set() methods is simple. For instance, setting the focus to a fixed value of 45 looks like this:

    cameraControl.Set(CameraControlProperty.Focus, 45, CameraControlFlags.Manual);

    The CameraControlFlags.Manual tells the webcam to switch off aufo-focus.

    Where to go from here

    Note the COM pitfall in the sample

    If you are as inexperienced working with COM interop as I am and look at the original sample code inside DisplayPropertyPage(), you may notice that the line

    ISpecifyPropertyPages pProp = dev as ISpecifyPropertyPages;
    

    seems to have a corresponding

    Marshal.ReleaseComObject(pProp);

    Does this mean that we need a similar call in our experimental code we added above?

    No, because if you add the (only supposedly) “missing”  Marshal.ReleaseComObject(cameraControl) to your code and click the button repeatedly, you will run into this exception:

    System.Runtime.InteropServices.InvalidComObjectException
      HResult=0x80131527
      Message=COM object that has been separated from its underlying RCW cannot be used.
      Source=DxPropPages
    …
    

    What is happening here? The answer is that simply “casting” to a COM interface in C# is not something that has to be “cleaned up”. The code may imply that, but you could change the line

    Marshal.ReleaseComObject(pProp);

    to

    Marshal.ReleaseComObject(dev); // oDevice would work, too

    and it still would run without leaking references.

    How do I know? Because Marshal.ReleaseComObject() returns the new reference count and changing the line to

    Debug.WriteLine(Marshal.ReleaseComObject(oDevice));

    will output 1 each time we opened and close the properties dialog. The value of 1 is correct, because want to continue to be able to access the device object.

    Placing a copy of that line in front of the call of the external function OleCreatePropertyFrame() obviously does not make sense and will lead to an exception. But if you do it anyway, just for testing, the debug output will show 0 instead of 1. This shows us that passing the object as a parameter in COM interop – at least in this case – caused the reference count to be increased. This is why Marshal.ReleaseComObject() is called after OleCreatePropertyFrame(), not because of the cast to ISpecifyPropertyPages.

    Practice defensive coding

    As already mentioned, not all webcams support all properties. And if a webcam supports a property, the allowed values may differ from other devices. That is why you should use GetRange() to determine

    • whether a property is supported (return value 0),
    • the range of the allowed values, and
    • whether the property can be set to “auto”.

    Last, but not least: When you access a USB webcam – like any other detachable device – be prepared for it not being available. Not only at startup, but also while your program is running, because the device could have been unplugged unexpectedly.