Introduction
In this article you will find yet another implementation of a web camera control. The control is a simple and easy to use one: no additional dependencies and a minimalistic interface.
The control provides the following functionalities:
- Gets a list of available web camera devices on a system.
- Displays a video stream from a web camera device.
- Gets the current image being captured.
Requirements
- The WinForms version of the control is implemented using .NET Framework 2.0.
- The WPF version of the control is implemented using .NET Framework 4 Client Profile.
- The control uses the VMR-7 renderer filter available since Windows XP SP1.
The control supports both x86 and x64 platform targets.
Background
There are a number of ways to capture a video stream in Windows. Not mentioning all of them, the basic are DirectShow framework and AVICap library. We will use the DirectShow based approach, as it is more powerful and flexible.
The DirectShow framework operates using such concepts as a graph, filters and pins. The filters form a capture graph, through which a media stream flows. The filters in the graph are connected to each other using pins. A web camera is the capture filter a video stream starts from. The control’s window is passed to a renderer filter, which receives and shows the video stream. There are other in-the-middle filters possible, for example, color space conversion filters. That is all about the capture graph. See MSDN DirectShow documentation for more information.
Implementation Details
If you are not interested in implementation details, then you can skip this section.
The implementation is divided into three layers.
- The bottom layer is implemented as a native DLL module, which forwards our calls to the DirectShow framework.
- For distribution convenience, the native DLL module is embedded into the control’s assembly as a resource. On runtime stage, the DLL module will be extracted to a temporary file on disk and used via late binding technique. Once the control is disposed, the temporary file will be deleted. In other words, the control is distributed as a single file. All those operations are implemented by the middle layer.
- The top layer implements the control class itself and the
WebCameraId
class used to identify a web camera device.
The following diagram shows a logical structure of the implementation.
Only the top layer is supposed to be used by clients.
The Bottom Layer
The bottom layer implements the following utilities to work with the capture graph.
DSUTILS_API void __stdcall EnumVideoInputDevices(EnumVideoInputDevicesCallback callback);
DSUTILS_API int __stdcall BuildCaptureGraph();
DSUTILS_API int __stdcall AddRenderFilter(HWND hWnd);
DSUTILS_API int __stdcall AddCaptureFilter(BSTR devicePath);
DSUTILS_API int __stdcall ResetCaptureGraph();
DSUTILS_API int __stdcall Start();
DSUTILS_API int __stdcall Stop();
DSUTILS_API int __stdcall GetCurrentImage(BYTE **ppDib);
DSUTILS_API int __stdcall GetVideoSize(LONG *lpWidth, LONG *lpHeight);
DSUTILS_API void __stdcall DestroyCaptureGraph();
The Middle Layer
The middle layer is implemented in the DirectShowProxy
class.
First, what we should do is extract the capture graph utilities DLL module from the resources and save it to a temporary file.
_dllFile = Path.GetTempFileName();
using (FileStream stream = new FileStream(_dllFile, FileMode.Create, FileAccess.Write))
{
using (BinaryWriter writer = new BinaryWriter(stream))
{
writer.Write(IsX86Platform ?
Resources.DirectShowFacade : Resources.DirectShowFacade64);
}
}
Then we load our DLL module into the address space of the calling process.
_hDll = LoadLibrary(_dllFile);
if (_hDll == IntPtr.Zero)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
And bind the DLL module functions to the class instance methods.
private delegate Int32 BuildCaptureGraphDelegate();
private BuildCaptureGraphDelegate _buildCaptureGraph;
IntPtr pProcPtr = GetProcAddress(_hDll, "BuildCaptureGraph");
_buildCaptureGraph =
(BuildCaptureGraphDelegate)Marshal.GetDelegateForFunctionPointer(pProcPtr,
typeof(BuildCaptureGraphDelegate));
When the control is being disposed, we unload the DLL module and delete it.
public void Dispose()
{
if (_hDll != IntPtr.Zero)
{
FreeLibrary(_hDll);
_hDll = IntPtr.Zero;
}
if (File.Exists(_dllFile))
{
File.Delete(_dllFile);
}
}
The Top Layer
The top layer is implemented in the WebCameraControl
class with the following interface.
public IEnumerable<WebCameraId> GetVideoCaptureDevices();
public Boolean IsCapturing { get; }
public void StartCapture(WebCameraId camera);
public Bitmap GetCurrentImage();
public Size VideoSize { get; }
public void StopCapture();
Open the Package Manager Console and add a nuget package to your project:
Install-Package WebEye.Controls.WinForms.WebCameraControl
First, we need to add the control to the Visual Studio Designer Toolbox, using a right-click and then the "Choose Items..." menu item. Then we place the control on a form at desired location and with desired size. The default name of the control instance variable will be webCameraControl1
.
Then, on run-time stage, we need to get a list of web cameras available on the system.
List<WebCameraId> cameras = new List<WebCameraId>(webCameraControl1.GetVideoCaptureDevices());
The following code starts a capture from the first camera in the list.
webCameraControl1.StartCapture(cameras[0]);
Please note that the control's window has to be created in order to start a capture, otherwise you will get an exception due to the fact that there is no output pin for the video stream. The common mistake is to start a capture in the Form.Load
event handler, when the control's window have not yet been created.
To get an image being captured just call the GetCurrentImage()
method. The resolution and quality of the image depend on your camera device characteristics.
Bitmap image = webCameraControl1.GetCurrentImage();
To stop the capture the StopCapture()
method is used.
webCameraControl1.StopCapture();
You can always ask the capture state using the following code.
if (webCameraControl1.IsCapturing)
{
webCameraControl1.StopCapture();
}
To start the capture from another web camera just call the StartCapture
method again.
webCameraControl1.StartCapture(cameras[1]);
To report errors, exceptions are used, so do not forget to wrap your code in a try
/catch
block. That is all about using it. To see the complete example please check the demo application sources.
WPF Version
In WPF user controls do not have a WinAPI window handle (HWND
) associated with them and this is a problem, because the DirectShow framework requires a window handle in order to output the video stream. The VideoWindow class has been introduced to workaround this problem.
<UserControl x:Class="WebCamera.WebCameraControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
xmlns:local="clr-namespace:WebCamera">
<local:VideoWindow x:Name="_videoWindow" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</UserControl>
To add a WPF version of the control to your project use the following nuget command:
Install-Package WebEye.Controls.Wpf.WebCameraControl
GitHub
The project has a GitHub repository available on the following page.
https://github.com/jacobbo/WebEye
Any questions, remarks, and comments are welcome.
History
- November 5, 2017 - Replaced VMR9 with VMR7 to extend a range of supported configurations.
- February 29, 2016 - Added a nuget package.
- April 10, 2015 - Added the x64 platform support.
- October 24, 2012 - Added a GitHub repository.
- September 12, 2012 - The demo apps are updated to report exceptions.
- September 9, 2012 - The WPF version of the control plus demo application.
- February 19, 2012 - Updated demo code and documentation.
- February 15, 2012 - The initial version.