65.9K
CodeProject 正在变化。 阅读更多。
Home

流播放器控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (80投票s)

2015年3月19日

CPOL

5分钟阅读

viewsIcon

733652

downloadIcon

47387

在本文中,您将找到一个流媒体播放器控件的实现。

引言

本文是我上一篇文章的延续,该文章展示了一个网络摄像头控件的实现。最近,我创建了另一个控件,并希望与社区分享我的经验。它是一个基于FFmpeg的流媒体播放器控件,它可以做到以下几点:

  1. 播放RTSP/RTMP视频流或本地视频文件
  2. 检索控件当前显示的帧

该控件没有额外的依赖项,并且具有极简的界面。

要求

  1. WinForms 版本控件使用 .NET Framework 2.0 实现。
  2. WPF 版本控件使用 .NET Framework 4 Client Profile 实现。

该控件同时支持 x86 和 x64 平台目标。

背景

如今,通过互联网流式传输音频、视频和数据是一件非常普遍的事情。然而,当我试图寻找一个 .NET 控件来播放通过网络发送的视频流时,我几乎找不到任何东西。本项目试图填补这一空白。

实现细节

如果您对实现细节不感兴趣,可以跳过本节。

实现分为三个层次。

  1. 底层实现为一个本地 DLL 模块,它将我们的调用转发给 FFmpeg 框架。
  2. 为了方便分发,本地 DLL 模块被嵌入到控件的程序集中作为资源。在运行时,DLL 模块将被提取到磁盘上的临时文件中,并通过后期绑定技术使用。一旦控件被释放,临时文件将被删除。换句话说,该控件是以单个文件形式分发的。所有这些操作都由中间层实现。
  3. 顶层实现控件类本身。

下图展示了实现的逻辑结构。

只有顶层供客户端使用。

底层

底层使用外观模式为 FFmpeg 框架提供简化的接口。外观由三个类组成:StreamPlayer 类,实现了流播放功能。

/// <summary>
/// The StreamPlayer class implements a stream playback functionality.
/// </summary>
class StreamPlayer : private boost::noncopyable
{
public:

    /// <summary>
    /// Initializes a new instance of the StreamPlayer class.
    /// </summary>
    StreamPlayer();

    /// <summary>
    /// Initializes the player.
    /// </summary>
    /// <param name="playerParams">The StreamPlayerParams object 
    /// that contains the information that is used to initialize the player.</param>
    void Initialize(StreamPlayerParams playerParams);

    /// <summary>
    /// Asynchronously plays a stream.
    /// </summary>
    /// <param name="streamUrl">The url of a stream to play.</param>
    void StartPlay(std::string const& streamUrl);
    
    /// <summary>
    /// Retrieves the current frame being displayed by the player.
    /// </summary>
    /// <param name="bmpPtr">Address of a pointer to a byte that will receive the DIB.</param>
    void GetCurrentFrame(uint8_t **bmpPtr);

    /// <summary>
    /// Retrieves the unstretched frame size, in pixels.
    /// </summary>
    /// <param name="widthPtr">A pointer to an int that will receive the width.</param>
    /// <param name="heightPtr">A pointer to an int that will receive the height.</param>
    void GetFrameSize(uint32_t *widthPtr, uint32_t *heightPtr);

    /// <summary>
    /// Uninitializes the player.
    /// </summary>
    void Uninitialize();
};

Stream 类,将视频流转换为一系列帧

/// <summary>
/// A Stream class converts a stream into series of frames. 
/// </summary>
class Stream : private boost::noncopyable
{
public:
    /// <summary>
    /// Initializes a new instance of the Stream class.
    /// </summary>
    /// <param name="streamUrl">The url of a stream to decode.</param>
    Stream(std::string const& streamUrl);

    /// <summary>
    /// Gets the next frame in the stream.
    /// </summary>
    /// <returns>The next frame in the stream.</returns>
    std::unique_ptr<Frame> GetNextFrame();

    /// <summary>
    /// Gets an interframe delay, in milliseconds.
    /// </summary>
    int32_t InterframeDelayInMilliseconds() const;

    /// <summary>
    /// Releases all resources used by the stream.
    /// </summary>
    ~Stream();
};

以及 Frame 类,这是一组与帧相关的实用程序。

/// <summary>
/// The Frame class implements a set of frame-related utilities. 
/// </summary>
class Frame : private boost::noncopyable
{
public:
    /// <summary>
    /// Initializes a new instance of the Frame class.
    /// </summary>
    Frame(uint32_t width, uint32_t height, AVPicture &avPicture);

    /// <summary>
    /// Gets the width, in pixels, of the frame.
    /// </summary>
    uint32_t Width() const { return width_; }

    /// <summary>
    /// Gets the height, in pixels, of the frame.
    /// </summary>
    uint32_t Height() const { return height_; }

    /// <summary>
    /// Draws the frame.
    /// </summary>
    /// <param name="window">A container window that frame should be drawn on.</param>
    void Draw(HWND window);

    /// <summary>
    /// Converts the frame to a bitmap.
    /// </summary>
    /// <param name="bmpPtr">Address of a pointer to a byte that will receive the DIB.</param>
    void ToBmp(uint8_t **bmpPtr);

    /// <summary>
    /// Releases all resources used by the frame.
    /// </summary>
    ~Frame();
};

这三个类构成了 FFmpeg Facade DLL 模块的核心。

中间层

中间层由 StreamPlayerProxy 类实现,它作为 FFmpeg Facade DLL 模块的代理。

首先,我们应该做的是从资源中提取 FFmpeg Facade DLL 模块并将其保存到临时文件中。

_dllFile = Path.GetTempFileName();
using (FileStream stream = new FileStream(_dllFile, FileMode.Create, FileAccess.Write))
{
    using (BinaryWriter writer = new BinaryWriter(stream))
    {
        writer.Write(Resources.StreamPlayer);
    }
}

然后,我们将 DLL 模块加载到调用进程的地址空间。

_hDll = LoadLibrary(_dllFile);
if (_hDll == IntPtr.Zero)
{
    throw new Win32Exception(Marshal.GetLastWin32Error());
}

并将 DLL 模块函数绑定到类实例方法。

private delegate Int32 StopDelegate();
private StopDelegate _stop;

// ...

IntPtr procPtr = GetProcAddress(_hDll, "Stop");
_stop =
    (StopDelegate)Marshal.GetDelegateForFunctionPointer(procPtr, 
     typeof(StopDelegate));

当控件被释放时,我们将卸载 DLL 模块并将其删除。

private void Dispose()
{    
    if (_hDll != IntPtr.Zero)
    {
        FreeLibrary(_hDll);
        _hDll = IntPtr.Zero;
    }

    if (File.Exists(_dllFile))
    {
        File.Delete(_dllFile);
    }    
}

顶层

顶层由 StreamPlayerControl 类实现,具有以下接口:

/// <summary>
/// Asynchronously plays a stream.
/// </summary>
/// <param name="uri">The url of a stream to play.</param>
/// <exception cref="ArgumentException">
/// An invalid string is passed as an argument.</exception>
/// <exception cref="Win32Exception">Failed to load the FFmpeg facade dll.</exception>
/// <exception cref="StreamPlayerException">Failed to play the stream.</exception>
public void StartPlay(Uri uri)

/// <summary>
/// Retrieves the image being played.
/// </summary>
/// <returns>The current image.</returns>
/// <exception cref="InvalidOperationException">
/// The control is not playing a video stream.</exception>
/// <exception cref="StreamPlayerException">Failed to get the current image.</exception>
public Bitmap GetCurrentFrame();

/// <summary>
/// Stops a stream.
/// </summary>
/// <exception cref="InvalidOperationException">
/// The control is not playing a stream.</exception>
/// <exception cref="StreamPlayerException">Failed to stop a stream.</exception>
public void Stop();

/// <summary>
/// Gets a value indicating whether the control is playing a video stream.
/// </summary>
public Boolean IsPlaying { get; }

/// <summary>
/// Gets the unstretched frame size, in pixels.
/// </summary>
public Size VideoSize  { get; }

/// <summary>
/// Occurs when the first frame is read from a stream.
/// </summary>
public event EventHandler StreamStarted;

/// <summary>
/// Occurs when there are no more frames to read from a stream.
/// </summary>
public event EventHandler StreamStopped;

/// <summary>
/// Occurs when the player fails to play a stream.
/// </summary>
public event EventHandler StreamFailed;

线程

该控件创建两个线程:一个用于读取流,另一个用于解码。

流是存储在队列中的一系列数据包,该队列在线程之间共享。

用法

打开程序包管理器控制台,并将 nuget 程序包添加到您的项目中。

Install-Package WebEye.Controls.WinForms.StreamPlayerControl

首先,我们需要通过右键单击然后选择“选择项…”菜单项将控件添加到 Visual Studio 设计器工具箱。然后,我们将控件放置在窗体上所需的位置和大小。控件实例变量的默认名称将是 streamPlayerControl1

以下代码使用提供的地址异步播放流。

streamPlayerControl1.StartPlay
(new Uri("rtsp://184.72.239.149/vod/mp4:BigBuckBunny_115k.mov"));

还可以选择指定连接和流超时以及底层传输协议。

streamPlayerControl1.StartPlay(new Uri("rtsp://184.72.239.149/vod/mp4:BigBuckBunny_115k.mov"),
    TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(5), 
    RtspTransport.UdpMulticast, RtspFlags.None);

要获取正在播放的帧,只需调用 GetCurrentFrame() 方法。帧的分辨率和质量取决于流的质量。

using (Bitmap image = streamPlayerControl1.GetCurrentFrame())
{
    // image processing...
}

要停止流,请使用 Stop() 方法。

streamPlayerControl1.Stop();

您可以使用以下代码始终检查播放状态:

if (streamPlayerControl1.IsPlaying)
{
    streamPlayerControl1.Stop();
}

此外,还可以使用 StreamStartedStreamStoppedStreamFailed 事件来监视播放状态。

为了报告错误,使用异常,所以不要忘记将您的代码包装在 try/catch 块中。以上就是使用方法。要查看完整示例,请参阅演示应用程序源代码。

WPF 版本

FFmpeg facade 需要一个 WinAPI 窗口句柄 (HWND) 才能将其用作渲染目标。问题是,在 WPF 世界中,窗口不再有句柄。VideoWindow 类可以解决这个问题。

<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>

要将控件的 WPF 版本添加到您的项目中,请使用以下 nuget 命令:

Install-Package WebEye.Controls.Wpf.StreamPlayerControl

GitHub

该项目在以下页面上有一个 GitHub 存储库:

欢迎提问、评论和反馈。

许可

  1. FFmpeg facade 源代码,以及 FFmpeg 框架,均在LGPL 许可下授权。
  2. .NET 控件源代码和演示源代码根据Code Project 开源许可 (CPOL) 授权。

您可以在您的商业产品中使用该控件,唯一的要求是您必须在此处提及您的产品使用了 FFmpeg 库(此处为详细信息)

历史

  • 2015 年 3 月 19 日 - 初始版本
  • 2015 年 8 月 22 日 - 添加了 x64 平台支持
  • 2015 年 10 月 25 日 - 添加了异步流启动和流状态事件
  • 2015 年 11 月 8 日 - 添加了本地文件播放支持
  • 2015 年 11 月 30 日 - 添加了流连接超时
  • 2017 年 10 月 17 日 - 使用新的 FFmpeg 解码 API
  • 2019 年 8 月 31 日 - 添加了流超时参数
© . All rights reserved.