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

nVLC

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (210投票s)

2010年9月14日

GPL3

16分钟阅读

viewsIcon

12637550

downloadIcon

69967

一个用于libVLC接口的.NET API,以便在托管应用程序中使用绝大多数VLC功能

引言

自从我开始使用VLC Media Player以来,我一直对其功能印象深刻,尤其是其内置的解码器,无需进一步安装。在进一步探索VLC结构后,我发现了libvlc.dll模块,它是一个用于整个VLC引擎的API,包含丰富的渲染、流媒体和转码功能。Libvlc是一个本地DLL,公开了数百个C方法调用。本文的主要目的是为libVLC接口提供一个.NET API,以便在托管应用程序中使用绝大多数VLC功能。

VLC 1.1.x 引入了几个改进和修复,详情请参见此处;其中最引人注目的是GPU解码和简化的LIBVLC API(无异常处理)。1.1.1版本还增加了对Google WebM视频格式的支持。

P/Invoke

为了在托管应用程序中使用libvlc,它必须被某种互操作层包装。有三种方法可以实现这一点

  1. C++/CLI
  2. COM 互操作
  3. P/Invoke

由于libvlc是一个导出纯C方法的本地库,这里选择P/Invoke。

如果您计划丰富P/Invoke知识,libvlc是一个很好的起点。它拥有大量的结构、联合和回调函数,有些方法需要自定义封送处理来处理双指针和string转换。

我们需要下载VLC源代码以更好地理解libvlc接口。请遵循此链接。解压存档内容后,转到<您的路径>\vlc-1.1.4\include\vlc文件夹

这些是libvlc的头文件。如果您想在本地(C/C++)应用程序中直接使用它们,有一篇很棒的文章对此进行了说明。

自定义封送处理

libvlc接口的入口点是定义在libvlc.h中的libvlc_new API

VLC_PUBLIC_API libvlc_instance_t * libvlc_new( int argc , const char *const *argv );

argv是一个字符串数组的双指针,它控制VLC引擎的行为,例如禁用屏幕保护程序、不使用任何实现的UI等。

托管方法声明使用自定义封送属性来指示.NET运行时如何将System.String对象数组传递给预期的本地格式。

[DllImport("libvlc")]
public static extern IntPtr libvlc_new(int argc, 
  [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPStr)] string[] argv);

请注意,方法的返回类型是IntPtr,它保存了一个指向libvlc_instance_t结构的本地指针的引用。

结构体

这是从libvlc_structures.h中获取的libvlc_log_message_t定义。

typedef struct libvlc_log_message_t
{
    unsigned    sizeof_msg;   /* sizeof() of message structure, 
                                 must be filled in by user */
    int         i_severity;   /* 0=INFO, 1=ERR, 2=WARN, 3=DBG */
    const char *psz_type;     /* module type */
    const char *psz_name;     /* module name */
    const char *psz_header;   /* optional header */
    const char *psz_message;  /* message */
} libvlc_log_message_t;

这个结构的托管对应项非常直接。

[StructLayout(LayoutKind.Sequential)]
public struct libvlc_log_message_t
{
  public UInt32 sizeof_msg;
  public Int32 i_severity;
  public IntPtr psz_type;
  public IntPtr psz_name;
  public IntPtr psz_header;
  public IntPtr psz_message;
}

LayoutKind.Sequential表示结构的所有成员在本地内存中按顺序排列。

联合

联合类似于结构,但其通过类型定义声明的成员从相同的内存位置开始。这意味着布局必须通过封送运行时显式控制,这可以通过FieldOffset属性来实现。

这是从libvlc_events.h中获取的libvlc_event_t定义。

typedef struct libvlc_event_t 
{
    int   type;
    void *p_obj
    union
    {
        /* media descriptor */
        struct
        {
            libvlc_meta_t meta_type;
        } media_meta_changed;
        struct
        {
            libvlc_media_t * new_child;
        } media_subitem_added;
        struct
        {
            int64_t new_duration;
        } media_duration_changed;
            …
     }
}

它基本上是一个包含两个简单成员和一个联合的结构。使用LayoutKind.Explicit告诉运行时每个字段在内存中的确切位置。

[StructLayout(LayoutKind.Explicit)]
public struct libvlc_event_t
{
  [FieldOffset(0)]
  public libvlc_event_e type;

  [FieldOffset(4)]
  public IntPtr p_obj;

  [FieldOffset(8)]
  public media_player_time_changed media_player_time_changed;
}

[StructLayout(LayoutKind.Sequential)]
public struct media_player_time_changed
{
  public long new_time;
}

如果您打算使用其他值扩展libvlc_event_t定义,它们都必须用[FieldOffset(8)]属性进行装饰,因为它们都从偏移量8字节开始。

回调函数

当底层VLC引擎的内部状态发生变化时,它会使用回调函数通知订阅了此类更改的实体。订阅是通过定义在libvlc.h中的libvlc_event_attach API进行的。该API有四个参数:

  1. 指向事件管理器对象的指针
  2. libvlc_event_type_t enum值,指定需要回调的事件
  3. 指向libvlc_callback_t函数的指针
  4. 可选:其他用户数据

回调函数指针在libvlc.h中声明如下:

typedef void ( *libvlc_callback_t )( const struct libvlc_event_t *, void * );

它接受指向libvlc_event_t结构和可选的用户定义数据的指针。

托管端口是一个具有相同签名的委托。

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void VlcEventHandlerDelegate(
        ref libvlc_event_t libvlc_event, IntPtr userData);

请注意,我希望获得对libvlc_event_t结构的引用,以便在MediaPlayerEventOccured函数中访问其参数。与其他地方我只使用IntPtr在方法调用之间传递指针不同。

public EventBroker(IntPtr hMediaPlayer)
{
 VlcEventHandlerDelegate callback1 = MediaPlayerEventOccured;

 m_hEventMngr = LibVlcMethods.libvlc_media_player_event_manager(hMediaPlayer);

 hCallback1 = Marshal.GetFunctionPointerForDelegate(callback1);
 m_callbacks.Add(callback1);

 GC.KeepAlive(callback1);
}

private void MediaPlayerEventOccured(ref libvlc_event_t libvlc_event, IntPtr userData)
{
 switch (libvlc_event.type)
 {
    case libvlc_event_e.libvlc_MediaPlayerTimeChanged:
       RaiseTimeChanged(libvlc_event.media_player_time_changed.new_time);
       break;

    case libvlc_event_e.libvlc_MediaPlayerEndReached:
       RaiseMediaEnded();
       break;
 }       
}

.NET委托类型是C回调函数的托管版本,因此System.Runtime.InteropServices.Marshal类包含用于将委托转换为原生方法调用以及从原生方法调用转换回委托的例程。在委托定义被封送到可由原生代码调用的原生函数指针后,我们必须维护对托管委托的引用,以防止它被GC回收,因为原生指针无法“持有”对托管资源的引用。

nVLC API

IMediaPlayerFactory - 包装libvlc_instance_t句柄,用于创建媒体对象和媒体播放器对象。

  • IPlayer - 保存libvlc_media_player_t句柄,用于基本播放,当不需要音频或视频输出时,例如,媒体的流媒体或转码。
  • IAuidoPlayer – 扩展IPlayer,用于播放和/或流式传输音频媒体。
  • IVideoPlayer – 扩展IAudioPlayer,用于渲染和/或流式传输音频和视频媒体。
  • IEventBroker – 通过包装libvlc_event_manager_t句柄来封装VLC引擎引发的事件。
  • IMedia – 包装libvlc_media_t句柄,允许用户添加媒体选项。

这些接口的实现如下所示。

内存管理

由于每个包装器对象都持有对本地内存的引用,我们必须确保在托管对象被垃圾回收器回收时释放该内存。这可以通过用户代码隐式或显式调用Dispose方法,或者在对象被解除分配时通过终结器来完成。我将此功能包装在DisposableBase类中。

public abstract class DisposableBase : IDisposable
{
  private bool m_isDisposed;

  public void Dispose()
  {
     if (!m_isDisposed)
     {
        Dispose(true);
        GC.SuppressFinalize(this);

        m_isDisposed = true;
     }
  }

  protected abstract void Dispose(bool disposing);
  //      if (disposing)
  //      {
  //         // get rid of managed resources 
  //      }
  //      // get rid of unmanaged resources 

  ~DisposableBase()
   {
     if (!m_isDisposed)
     {
        Dispose(false);
        m_isDisposed = true;
     }
  }

  protected void VerifyObjectNotDisposed()
  {
     if (m_isDisposed)
     {
        throw new ObjectDisposedException(this.GetType().Name);
     }
  }
}

继承自DisposableBase的每个类都必须实现Dispose方法,该方法在由用户代码调用时会带有一个参数true,此时可以释放托管和非托管资源;或者带有一个参数false,表示它由终结器调用,此时只能释放非托管资源。

日志记录

VLC以日志迭代器的形式实现日志逻辑,因此我也决定使用迭代器模式来实现它,即使用yield return语句。

public IEnumerator<LogMessage> GetEnumerator()
{
    IntPtr i = LibVlcMethods.libvlc_log_get_iterator(m_hLog);

    while (LibVlcMethods.libvlc_log_iterator_has_next(i) != 0)
    {
       libvlc_log_message_t msg = new libvlc_log_message_t();
       msg.sizeof_msg = (uint)Marshal.SizeOf(msg);
       LibVlcMethods.libvlc_log_iterator_next(i, ref msg);
       
       yield return GetMessage(msg);
    }

    LibVlcMethods.libvlc_log_iterator_free(i);
    LibVlcMethods.libvlc_log_clear(m_hLog);
}

private LogMessage GetMessage(libvlc_log_message_t msg)
{
    StringBuilder sb = new StringBuilder();
    sb.AppendFormat("{0} ", Marshal.PtrToStringAnsi(msg.psz_header));
    sb.AppendFormat("{0} ", Marshal.PtrToStringAnsi(msg.psz_message));
    sb.AppendFormat("{0} ", Marshal.PtrToStringAnsi(msg.psz_name));
    sb.Append(Marshal.PtrToStringAnsi(msg.psz_type));

    return new LogMessage() { Message = sb.ToString(), 
       Severity = (libvlc_log_messate_t_severity)msg.i_severity };
}

这段代码在每次超时(默认1秒)时调用,遍历所有现有日志消息,并清理日志。实际写入日志文件(或其他目标)是通过NLog实现的,您应该在您的app.config中添加一个自定义配置节才能使其工作。

<configSections>
  <section name="nlog"
           type="NLog.Config.ConfigSectionHandler, NLog" />
</configSections>

<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

  <targets>
     <target name="file" xsi:type="File"
         layout="${longdate} ${level} ${message}"
         fileName="${basedir}/logs/logfile.txt"
         keepFileOpen="false"
         encoding="iso-8859-2" />
  </targets>

  <rules>
     <logger name="*" minlevel="Debug" writeTo="file" />
  </rules>
</nlog>

Using the Code

在运行任何使用nVLC的应用程序之前,您必须从此处下载最新的VLC 1.1.x或更高版本。运行安装程序后,转到C:\Program Files\VideoLAN\VLC并将以下项目复制到您的可执行路径:

  1. libvlc.dll
  2. libvlccode.dll
  3. plugins目录

如果在运行时缺少其中任何一个,您将收到一个DllNotFoundException

在您的代码中,添加对Declarations和Implementation项目的引用。您需要构造的第一个实例是MediaPlayerFactory,您可以从中通过调用CreateMedia函数来构造媒体对象,通过调用CreatePlayer函数来构造媒体播放器对象。

播放文件

最基本的使用方法是将文件播放到指定的面板。

IMediaPlayerFactory factory = new MediaPlayerFactory();
IMedia media = factory.CreateMedia<IMedia>(@"C:\Videos\Movie.wmv");
IVideoPlayer player = factory.CreatePlayer<IVideoPlayer>();
player.WindowHandle = panel1.Handle;
player.Open(media);
player.Events.MediaEnded += new EventHandler(Events_MediaEnded);
player.Events.TimeChanged += new EventHandler<TimeChangedEventArgs>(Events_TimeChanged);
player.Play();

播放DirectShow

VLC内置了对DirectShow捕获源过滤器的支持;这意味着如果您有一个具有DirectShow过滤器的网络摄像头或视频采集卡,可以使用libvlc API无缝地使用它。

IMedia media = factory.CreateMedia<IMedia>(@"dshow://", @"dshow-vdev=Trust Webcam 15007");

请注意,媒体路径始终设置为dshow://,实际的视频设备由选项参数指定。

播放网络流

VLC支持广泛的网络协议,如UDP、RTP、HTTP等。通过指定带有协议名称、IP地址和端口的媒体路径,您可以捕获流并以与打开本地媒体文件相同的方式进行渲染。

IMedia media = factory.CreateMedia<IMedia>(@"udp://@172.16.10.1:19005");

流式传输

除了令人印象深刻的播放功能外,VLC还扮演着同样令人印象深刻的流媒体引擎的角色。在我们深入了解实现细节之前,我将简要描述VLC Media Player的流媒体功能。

运行VLC后,转到媒体->流媒体,将打开“打开媒体”对话框,并指定您希望在网络上广播的媒体。

如上所示,您可以流式传输本地文件、磁盘、网络流或捕获设备。在这种情况下,我选择了一个本地文件并按了“流式传输”,然后在下一个选项卡中按“下一步”。

现在您可以选择先前所选流的目的地。如果选择了“文件”选项并且勾选了“启用转码”,您只需将媒体转码(或重新多路复用)为不同的格式。为简单起见,我选择UDP,按“添加”,然后指定127.0.0.1:9000,这意味着我希望在本地机器上流式传输到端口9000。

确保勾选了“启用转码”,然后按“编辑配置文件”按钮。

此对话框允许您选择封装(一种媒体容器格式)、视频编解码器和音频编解码器。这里的可能性非常大,请注意,并非所有视频和音频格式都与每种容器兼容,但同样,为简单起见,我选择使用MP4容器,以及h264视频编码器和AAC音频编码器。按“下一步”后,您将看到最终对话框,其中包含“生成的流输出字符串”。

这是最重要的部分,因为这个string应该被传递给媒体对象,这样您就可以简单地复制它并在API中使用,如下所示。

string output = 
  @":sout=#transcode{vcodec=h264,vb=0,scale=0,acodec=mp4a,"+ 
  @"ab=128,channels=2,samplerate=44100}:udp{dst=127.0.0.1:9000} ";

IMedia media = factory.CreateMedia<IMedia>(@"C:\Videos\Movie.wmv", output);
IPlayer player = factory.CreatePlayer<IPlayer>();
player.Open(media);
player.Play();

这将打开选定的电影文件,将其转码为所需的格式,并通过UDP流式传输。

内存渲染器

通常,您会在屏幕上渲染视频,传递一个窗口句柄,实际帧将根据媒体时钟在此窗口上显示。LibVLC还允许您将原始视频(像素数据)渲染到预先分配的内存缓冲区。此功能通过libvlc_video_set_callbackslibvlc_video_set_format API实现。IVideoPlayer有一个名为CustomRenderer的属性,类型为IMediaRenderer,它包装了这两个API。

/// <summary>
/// Enables custom processing of video frames.
/// </summary>
public interface IMemoryRenderer
{
  /// <summary>
  /// Sets the callback which invoked when new frame should be displayed
  /// </summary>
  /// <param name="callback">Callback method</param>
  /// <remarks>The frame will be auto-disposed after callback invocation.</remarks>
  void SetCallback(NewFrameEventHandler callback);

  /// <summary>
  /// Gets the latest video frame that was displayed.
  /// </summary>
  Bitmap CurrentFrame { get; }

  /// <summary>
  /// Sets the bitmap format for the callback.
  /// </summary>
  /// <param name="format">Bitmap format of the video frame</param>
  void SetFormat(BitmapFormat format);

  /// <summary>
  /// Gets the actual frame rate of the rendering.
  /// </summary>
  int ActualFrameRate { get; }
}

您有两种帧处理选项。

  1. 回调

    通过调用SetCallback方法,当新帧准备好显示时,将调用您的回调。传递给回调方法的System.Drawing.Bitmap对象仅在回调内部有效;之后它会被释放,因此如果您打算在其他地方使用它,必须克隆它。同时请注意,回调代码必须非常高效;否则,播放将延迟,帧可能会丢失。例如,如果您正在渲染一个每秒30帧的视频,那么帧之间的时隙大约为33毫秒。您可以通过比较IVideoPlayer.FPSIMemoryRenderer.ActualFrameRate的值来测试性能下降。以下代码片段演示了在RGB24格式下渲染4CIF帧。

    IMediaPlayerFactory factory = new MediaPlayerFactory();
    IVideoPlayer player = player = factory.CreatePlayer<IVideoPlayer>();
    IMedia media = factory.CreateMedia<IMedia>(@"C:\MyVideoFile.avi");
    IMemoryRenderer memRender = player.CustomRenderer;
    memRender.SetCallback(delegate(Bitmap frame)
    {
          // Do something with the bitmap
    });
    
    memRender.SetFormat(new BitmapFormat(704, 576, ChromaType.RV24));
    player.Open(media);
    player.Play();
  2. 获取帧

    如果您想以自己的节奏查询帧,应该使用CurrentFrame属性。它将返回最近一个计划显示的帧。您需要自己负责在使用完后释放其资源。

    IMediaPlayerFactory factory = new MediaPlayerFactory();
    IVideoPlayer player = player = factory.CreatePlayer<IVideoPlayer>();
    IMedia media = factory.CreateMedia<IMedia>(@"C:\MyVideoFile.avi");
    IMemoryRenderer memRender = player.CustomRenderer;
    memRender.SetFormat(new BitmapFormat(704, 576, ChromaType.RV24));
    player.Open(media);
    player.Play();
             
    private void OnTimer(IMemoryRenderer memRender)
    {
         Bitmap bmp = memRender.CurrentFrame;
          // Do something with the bitmap
         bmp.Dispose();
    }

    SetFormat方法接受一个BitmapFormat对象,该对象封装了帧大小和像素格式。每像素字节数、帧大小和行间距(或跨度)根据ChromaType值在内部计算。

    IVideoPlayer可以在屏幕渲染模式或内存渲染模式下运行。一旦通过调用CustomRenderer属性将其设置为内存渲染模式,您将不会在屏幕上看到任何视频。

高级内存渲染器

从libVLC 1.2.0开始,可以使用VLC引擎输出解码的音频和视觉数据用于自定义处理,即输入任何编码和多路复用的媒体,并输出为解码的视频帧和音频样本。音频和视频样本的格式可以在播放开始前设置,以及视频大小、像素对齐、音频格式、通道数等。当播放开始时,相应的回调函数将在其显示时间为每个视频帧调用,并在其播放时间为给定数量的音频样本调用。这为您作为开发人员提供了极大的灵活性,因为您可以应用不同的图像和声音处理算法,如果需要,还可以最终渲染音频视觉数据。

libVLC通过libvlc_video_set_***libvlc_audio_set_*** API集公开了这种高级功能。在nVLC项目中,视频功能通过ICustomRendererEx接口公开。

/// <summary>
/// Contains methods for setting custom processing of video frames.
/// </summary>
public interface IMemoryRendererEx
{
    /// <summary>
    /// Sets the callback which invoked when new frame should be displayed
    /// </summary>
    /// <param name="callback">Callback method</param>
    void SetCallback(NewFrameDataEventHandler callback);

    /// <summary>
    /// Gets the latest video frame that was displayed.
    /// </summary>
    PlanarFrame CurrentFrame { get; }

    /// <summary>
    /// Sets the callback invoked before the media playback starts
    /// to set the desired frame format.
    /// </summary>
    /// <param name="setupCallback"></param>
    /// <remarks>If not set, original media format will be used</remarks>
    void SetFormatSetupCallback(Func<BitmapFormat, 
                                BitmapFormat> setupCallback);

    /// <summary>
    /// Gets the actual frame rate of the rendering.
    /// </summary>
    int ActualFrameRate { get; }
}

音频样本可以通过IAduioPlayer对象的CustomAudioRenderer属性访问。

/// <summary>
/// Enables custom processing of audio samples
/// </summary>
public interface IAudioRenderer
{
    /// <summary>
    /// Sets callback methods for volume change and audio samples playback
    /// </summary>
    /// <param name="volume">Callback method invoked
    ///    when volume changed or muted</param>
    /// <param name="sound">Callback method invoked when
    ///    new audio samples should be played</param>
    void SetCallbacks(VolumeChangedEventHandler volume, NewSoundEventHandler sound);

    /// <summary>
    /// Sets audio format
    /// </summary>
    /// <param name="format"></param>
    /// <remarks>Mutually exclusive with SetFormatCallback</remarks>
    void SetFormat(SoundFormat format);
 
    /// <summary>
    /// Sets audio format callback, to get/set format before playback starts
    /// </summary>
    /// <param name="formatSetup"></param>
    /// <remarks>Mutually exclusive with SetFormat</remarks>
    void SetFormatCallback(Func<SoundFormat, SoundFormat> formatSetup);
}

为了简化渲染视频样本和播放音频样本的任务,我开发了一个名为Taygeta的小型库。它最初是作为一个nVLC功能的测试应用程序,但由于我非常喜欢它 :),所以我决定将其转换为一个独立的工程。它使用Direct3D进行硬件加速视频渲染,使用XAudio2进行音频播放。它还包含一个带有前面所述所有功能的示例应用程序。

内存输入

如前几节所述,VLC为您的媒体提供了许多访问模块。当其中任何一个满足您的需求时,并且您需要,例如捕获窗口内容或将3D场景流式传输到另一台机器,内存输入将完成这项工作,因为它提供了从内存缓冲区流式传输媒体的接口。libVLC包含2个内存输入的模块:invmemimem。问题是它们都没有通过libVLC API公开,而且必须付出一些真正的努力才能使其工作,尤其是在托管代码中。

Invmem在libVLC 1.2中已被弃用,所以我不会在这里描述它。它通过IVideoInputMedia对象公开,您可以在“评论和讨论”论坛中搜索使用示例。

另一方面,Imem仍然受支持,并通过IMemoryInputMedia对象公开。

    /// <summary>
    /// Enables elementary stream (audio, video, subtitles or data) 
    /// frames insertion into VLC engine (based on imem access module)
    /// </summary>
    public interface IMemoryInputMedia : IMedia
    {
        /// <summary>
        /// Initializes instance of the media object with stream information and frames' queue size
        /// </summary>
        /// <param name="streamInfo"></param>
        /// <param name="maxFramesInQueue">Maximum items in the queue. 
        /// If the queue is full any AddFrame overload 
        /// will block until queue slot becomes available</param>
        void Initialize(StreamInfo streamInfo, int maxItemsInQueue = 30);
 
        /// <summary>
        /// Add frame of elementary stream data from memory on native heap
        /// </summary>
        /// <param name="streamInfo"></param>
        /// <remarks>This function copies frame data to internal buffer, 
        /// so native memory may be safely freed</remarks>
        void AddFrame(FrameData frame);
 
        /// <summary>
        /// Add frame of elementary stream data from memory on managed heap
        /// </summary>
        /// <param name="data"></param>
        /// <param name="pts">Presentation time stamp</param>
        /// <param name="dts">Decoding time stamp. -1 for unknown</param>
        /// <remarks>Time origin for both pts and dts is 0</remarks>
        void AddFrame(byte[] data, long pts, long dts = -1);
 
        /// <summary>
        /// Add frame of video stream from System.Drawing.Bitmap object
        /// </summary>
        /// <param name="bitmap"></param>
        /// <param name="pts">Presentation time stamp</param>
        /// <param name="dts">Decoding time stamp. -1 for unknown</param>
        /// <remarks>Time origin for both pts and dts is 0</remarks>
        /// <remarks>This function copies bitmap data to internal buffer, 
        /// so bitmap may be safely disposed</remarks>
        void AddFrame(Bitmap bitmap, long pts, long dts = -1);
 
        /// <summary>
        /// Sets handler for exceptions thrown by background threads
        /// </summary>
        /// <param name="handler"></param>
        void SetExceptionHandler(Action<Exception> handler);
 
        /// <summary>
        /// Gets number of pending frames in queue 
        /// </summary>
        int PendingFramesCount { get; }
    }

该接口提供了3个AddFrame重载,它们从本地堆指针、托管字节数组或Bitmap对象获取帧数据。每个方法将数据复制到内部结构并存储在帧队列中。因此,在调用AddFrame之后,您可以释放帧资源。一旦您初始化了IMemoryInputMedia并对媒体播放器对象调用播放,VLC将启动一个播放线程,该线程运行一个无限循环。在该循环中,它会获取数据帧并尽快将其推送到下游模块。

为了支持这种范例,我创建了一个生产者/消费者队列来保存媒体帧。该队列是BlockingCollection,它完美地满足了这个模块的需求:当队列已满时,它会阻塞生产者线程;当队列为空时,它会阻塞消费者线程。队列大小默认为30,因此它缓存大约1秒的视频。此缓存允许平滑的视频播放。请注意,增加队列大小将影响您的内存使用量 – 高清视频(1920x1080)的1帧(BGR24)占用5.93 MB。如果您对媒体源有帧率控制,可以定期检查队列中的待处理帧数并增加或减少速率。

DTS和PTS值用于通知libVLC引擎何时应由解码器处理帧(解码时间戳),以及何时应由渲染器呈现帧(演示时间戳)。DTS的默认值为-1,表示不使用它,只使用PTS。这在处理原始视频帧(如BGR24或I420)时非常有用,因为它们直接用于渲染,无需解码。PTS是必须的值,如果您没有随媒体帧一起提供它,可以通过使用媒体源的FPS和帧计数器轻松计算它。

long frameNumber = 0;
long frameIntervalInMicroSeconds = 1000000 / FrameRate;
long PTS = ++frameNumber * frameIntervalInMicroSeconds;

这将为渲染帧的值提供微秒级的PTS值。

使用代码与任何其他媒体实例相同。

StreamInfo fInfo = new StreamInfo();
fInfo.Category = StreamCategory.Video;
fInfo.Codec = VideoCodecs.BGR24;
fInfo.FPS = FrameRate;
fInfo.Width = Width;
fInfo.Height = Height;
 
IMemoryInputMedia m_iMem = m_factory.CreateMedia<IMemoryInputMedia>(MediaStrings.IMEM);
m_iMem.Initialize(fInfo);
m_player.Open(m_iMem);
m_player.Play();
...
 
private void OnYourMediaSourceCallback(MediaFrame frame)
{

ar fdata = new  FrameData() 
{ Data = frame.Data, DataSize = frame.DataSize, DTS = -1, PTS = frame.PTS };             
m_iMem.AddFrame(fdata);  
frame.Dispose();
          
}

不要忘记在完成后处理媒体对象,因为它还会释放所有待处理帧的内存。

参考文献

历史

  • 14.9.2010
    • 首次发布
  • 27.9.2010
    • 修复了MediaEnded事件,使其在ThreadPool线程上调用(由debesta报告的问题)
    • IAudioPlayer对象中实现了缺失的VolumeMute属性。
    • 添加了MediaListMediaListPlayer功能实现。
  • 22.10.2010
    • 添加了Unicode支持。
    • 修复了TakeSnapShot方法(由Member 7477754报告的问题)。
    • 扩展了音频和视频播放器功能。
    • 添加了IDiskPlayer用于DVD、VCD和音频CD播放。
    • 添加了IMemoryRenderer用于自定义视频渲染(libvlc_video_set_callbackslibvlc_video_set_format)。
    • 添加了视频滤镜(裁剪、去隔行和调整)以及覆盖滤镜(Logo和Marquee)。
    • 添加了CHM文档。
  • 18.11.2010
    • 添加了IVideoInputMedia,用于使用invmem访问模块进行逐帧视频输入。
    • 添加了IScreenCaptureMedia,用于捕获整个屏幕或部分屏幕。
    • 修复了libvlc_media_get_tracks_info实现(由Member 2090855报告的问题)。
    • IMediaIPlayerIMediaListIMediaListPlayer对象扩展了异步事件功能。
    • 扩展了IMedia功能(部分成员移至IMediaFromFile接口)。
    • 添加了Windows Forms和WPF的示例应用程序。
  • 19.4.2011
    • 修复了WPF示例和去隔行滤镜。
    • 添加了DVD导航API(libvlc 1.2.0或更高版本)。
    • 解决方案已升级到VS 2010和.NET Framework 4.0。
  • 6.7.2011
    • 更改了P/Invoke签名,以防止PInvokeStackImbalance异常(感谢PABnet)。
    • 添加了VLM(Video LAN Manager)实现(感谢Mulltonne)。
    • 添加了对libvlc 1.2.0的支持,包括滤镜枚举和支持YUV420和YUV422的IMemoryRenderEx
    • 添加了内存音频渲染器。
  • 25.10.2011
    • 错误修复和次要更改。
  • 10.10.2012
    • 添加了音频输出模块和音频输出设备选择。
    • 添加了libVLC dll的自动发现(感谢Raben)。
    • 添加了基于D3DImage的WPF示例,以实现更好的WPF集成。
    • 添加了对x64平台libvlc 2.0.1或更高版本的支持(感谢John O'Halloran)。
    • 为视频/音频内存输入添加了imem模块支持。
    • IMemoryRenderEx中添加了对J420(MJPEG)色度类型支持。
  • 3.10.2013 
    • 解决方案已升级到VS 2012,仍以.NET Framework 4.0为目标。
    • DLL名称已更改为nVLC.Declarations.dllnVLC.Implementation.dllnVLC.LibVlcWrapper.dll
    • 要从文件播放媒体,必须使用IMediaFromFile。
    • 添加了自定义内存输入(imem)和内存输出(vmem)的示例应用程序。
    • 添加了在初始化库时使用自定义字符串封送器的配置(感谢aserqa34asdf)。
    • 创建了NuGet包“nVLC”(感谢ericnewton76提供的想法)。
    • 应大量用户投诉,默认禁用硬件加速(添加--ffmpeg-hw可启用)。
    • 添加了查询视频、音频、字幕、章节和标题轨道描述的功能(感谢Christian Knobloch)。
    • 添加了对libVLC 2.1.0的支持以及libVLC 2.2.0的音频均衡器。
  • 12.2.2018
    • libVLC 3.0.0 API的100%实现。
    • 从任何.NET System.IO.Stream实例播放媒体(IStreamSourceMedia)。
    • 内存输入媒体,具有视频、音频和字幕Elementary流,支持推拉模式帧传递(ICompositeMemoryInputMedia)。
    • Chromecast发现和渲染(IRendererDiscovery)。
    • 音频均衡器示例。
    • 错误修复和性能改进。
© . All rights reserved.