Introduction
This article is a sort of continuation of my previous article, which shows an implementation of a web camera control. Recently, I created another control and would like to share my experience with the community. It is a FFmpeg-based stream player control, which can do the following:
- Play a RTSP/RTMP video stream or local video file
- Retrieve the current frame being displayed by the control
The control has no additional dependencies and a minimalistic interface.
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 supports both x86 and x64 platform targets.
Background
Streaming audio, video and data over the Internet is a very usual thing these days. However, when I tried to find a .NET control to play a video stream sent over the network, I found almost nothing. This project tries to fill up that gap.
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 FFmpeg framework.
- For distribution convenience, the native DLL module is embedded into the control’s assembly as a resource. On the 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.
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 uses the facade pattern to provide a simplified interface to the FFmpeg framework. The facade consists of three classes: the StreamPlayer
class, which implements a stream playback functionality.
class StreamPlayer : private boost::noncopyable
{
public:
StreamPlayer();
void Initialize(StreamPlayerParams playerParams);
void StartPlay(std::string const& streamUrl);
void GetCurrentFrame(uint8_t **bmpPtr);
void GetFrameSize(uint32_t *widthPtr, uint32_t *heightPtr);
void Uninitialize();
};
the Stream
class, which converts a video stream into a series of frames:
class Stream : private boost::noncopyable
{
public:
Stream(std::string const& streamUrl);
std::unique_ptr<Frame> GetNextFrame();
int32_t InterframeDelayInMilliseconds() const;
~Stream();
};
and the Frame
class, which is a set of frame related utilities.
class Frame : private boost::noncopyable
{
public:
Frame(uint32_t width, uint32_t height, AVPicture &avPicture);
uint32_t Width() const { return width_; }
uint32_t Height() const { return height_; }
void Draw(HWND window);
void ToBmp(uint8_t **bmpPtr);
~Frame();
};
These three classes form the heart of the FFmpeg
Facade DLL module.
The Middle Layer
The middle layer is implemented by the StreamPlayerProxy
class, which serves as a proxy to the FFmpeg Facade DLL module.
First, what we should do is extract the FFmpeg Facade 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(Resources.StreamPlayer);
}
}
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 StopDelegate();
private StopDelegate _stop;
IntPtr procPtr = GetProcAddress(_hDll, "Stop");
_stop =
(StopDelegate)Marshal.GetDelegateForFunctionPointer(procPtr,
typeof(StopDelegate));
When the control is being disposed, we unload the DLL module and delete it.
private 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 by the StreamPlayerControl
class with the following interface:
public void StartPlay(Uri uri)
public Bitmap GetCurrentFrame();
public void Stop();
public Boolean IsPlaying { get; }
public Size VideoSize { get; }
public event EventHandler StreamStarted;
public event EventHandler StreamStopped;
public event EventHandler StreamFailed;
Threads
The control creates two threads: one for reading of the stream and another for decoding.
The stream is a series of packets stored in a queue, which is shared between the threads.
Open the Package Manager Console and add a nuget package to your project:
Install-Package WebEye.Controls.WinForms.StreamPlayerControl
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 the desired location and with the desired size. The default name of the control instance variable will be streamPlayerControl1
.
The following code asynchronously plays a stream using the supplied address.
streamPlayerControl1.StartPlay
(new Uri("rtsp://184.72.239.149/vod/mp4:BigBuckBunny_115k.mov"));
There is also an option to specify a connection and stream timeouts and underlying transport protocol.
streamPlayerControl1.StartPlay(new Uri("rtsp://184.72.239.149/vod/mp4:BigBuckBunny_115k.mov"),
TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(5),
RtspTransport.UdpMulticast, RtspFlags.None);
To get a frame being played, just call the GetCurrentFrame()
method. The resolution and quality of the frame depend on the stream quality.
using (Bitmap image = streamPlayerControl1.GetCurrentFrame())
{
}
To stop the stream, the Stop()
method is used.
streamPlayerControl1.Stop();
You can always check the playing state using the following code:
if (streamPlayerControl1.IsPlaying)
{
streamPlayerControl1.Stop();
}
Also, the StreamStarted
, StreamStopped
and StreamFailed
events can be used to monitor the playback state.
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 a complete example, please check the demo application sources.
WPF Version
The FFmpeg facade expects a WinAPI window handle (HWND
) in order to use it as a render target. The issue is that in the WPF world, windows do not have handles anymore. The VideoWindow
class workarounds this issue.
<UserControl x:Class="WebEye.StreamPlayerControl"
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:WebEye">
<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.StreamPlayerControl
GitHub
The project has a GitHub repository available on the following page:
Any questions, remarks, and comments are welcome.
Licensing
- The FFmpeg facade sources, the same as the FFmpeg framework, are licensed under The LGPL license.
- The .NET controls' sources and demos' sources are licensed under The Code Project Open License (CPOL).
You can use the control in your commercial product, the only thing is that you should mention that your product uses the FFmpeg library, here are the details.
History
- March 19th, 2015 - Initial version
- August 22nd, 2015 - Added the x64 platform support
- October 25th, 2015 - Added asyncronous stream start and stream status events
- November 8th, 2015 - Added support for local files playback
- November 30th, 2015 - Added stream connection timeout
- October 17th, 2017 - Use new FFmpeg decoding API
- August 31st, 2019 - Added stream timeout parameter