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

使用 MediaElement 和 MediaStreamSource 在 Silverlight 4 中播放 AVI 文件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (7投票s)

2011 年 4 月 25 日

CPOL

11分钟阅读

viewsIcon

67544

downloadIcon

4551

此代码演示了如何使用 Silverlight 与 OOB+elevated trust(脱机模式+提升的信任)来播放本地视频(.avi)。

引言

本文尝试演示 MediaElementMediaStreamSource 类的强大功能。在本文中,我们将尝试编写一些代码来播放位于您计算机本地的 AVI 视频文件。

MediaStreamImg1.jpg

背景

借助 Silverlight 4 中引入的新功能,我一直想尝试编写一个简单的应用程序来播放 AVI 视频文件。为此,我不得不花费相当多的时间来研究这个问题。起初,我尝试了 WriteableBitmap,但后来发现了 MediaStreamSource 类提供的强大功能和特性。

本文仅触及了 MediaStreamSource 类为开发人员提供的这些功能的一小部分。因此,本文不深入探讨视频文件的解码,它只演示了如何使用派生自 MediaStreamSource 类的自定义类来缓冲样本并将它们提供给 MediaElement 控件。解码由 DLL(AVIDll.dll)处理,该 DLL 也包含在我们将用于将视频样本作为字节数组返回的示例中。此 DLL 的来源未包含在本文中。它只是使用 P/Invoke 方法的简单包装器,并且是用 VB6 编写的 ActiveX DLL。有很多文章,包括 CodeProject 的一些文章,都涉及打开 AVI 文件(使用 avifil32.dll 和其他 DLL),例如 https://codeproject.org.cn/KB/audio-video/avifilewrapper.aspx,还有一个非常古老但仍然非常有用的网站 http://www.shrinkwrapvb.com/avihelp/avihelp.htm

在我们的示例代码中,我们需要首先从 System.Windows.Media.MediaStreamSource 派生我们的自定义类。这将要求我们覆盖许多方法。不深入细节,这些方法是 OpenMediaAsyncGetSampleAsyncCloseMediaSeekAsyncGetDiagnosticsAsyncSwitchMediaStreamAsync。我将不深入研究这些方法的定义,但我们在示例代码中将使用的部分是

  • OpenMediaAsync:我们覆盖此方法,并在其中初始化并报告媒体的一些元数据,方法是调用 ReportOpenMediaCompleted() 方法。
  • GetSampleAsync:我们覆盖此方法并检索 MediaElement 请求的下一个样本。MediaElement 每次需要样本时都会调用此方法。要向 MediaElement 报告样本已准备好,我们调用 ReportGetSampleCompleted() 方法。

有关该主题的一些好书包括《Silverlight 4 in Action》和《Silverlight Recipes - A Problem Solution Approach》。

我们在本文中的主要目标是编写一个简单的 Silverlight 应用程序来播放 AVI 视频。好吧,要播放视频(.avi),您必须首先在计算机上安装相关的编解码器。

我们将使用以下简单步骤来实现我们的目标

  • 准备一个带有 MediaElement 控件、两个按钮和一个复选框的简单 UI。
  • 编写派生自 System.Windows.Media.MediaStreamSource 的自定义类,并覆盖所有必需的方法。
  • 在 UI 的代码隐藏中,将我们的自定义类设置为 MediaElement 控件的源流。

此示例使用 Silverlight 4 进行测试。

步骤 1

  • 创建一个新的 Silverlight 项目(C#)并将其命名为 MediaStreamSrc(或您喜欢的任何名称)。
  • 打开默认创建的名为 MainPage.xamlUserControl
  • 添加一个 MediaElement 控件并命名为 mediaPlayer
  • 添加两个按钮并命名为 OpenStreamCloseStream
  • 添加一个复选框并命名为 chkFlip

MediaStreamImg2.jpg

确保启用“脱机”模式(out-of-browser)并勾选“需要提升的信任”(Require elevated trust)。

<Grid x:Name="LayoutRoot" Background="black">

<MediaElement x:Name="MediaPlayer"
        AutoPlay="True"
        Stretch="Uniform"
        Margin="5,35,5,5"
        Opacity="1"
        Width="640"
        Height="480" />

<Button Content="Open AVI File" Height="23" Margin="29,12,0,0" 
  Name="OpenStream" HorizontalAlignment="Left" 
  Width="123" VerticalAlignment="Top"/>

<Button Content="Close AVI File" Height="23" 
  Margin="175,12,0,0" Name="CloseStream" 
  HorizontalAlignment="Left" Width="123" 
  VerticalAlignment="Top"/>

<CheckBox Content="FLIP IMAGE" Height="16" 
  Margin="0,21,12,0" Name="chkFlip" 
  VerticalAlignment="Top" Foreground="#FFD4D4D4" 
  IsChecked="True" HorizontalAlignment="Right" Width="104" />

</Grid>

MediaElement 控件 mediaPlayer 将用于显示我们的视频。OpenStream 按钮将用于初始化我们的自定义 MediaStreamSource 对象并将其作为媒体流分配给 mediaPlayerCloseStream 按钮将用于关闭和停止流。chkFlip 是一个用于翻转样本的标志。

我们将回到 UI 的代码隐藏部分,连接其余的代码。

第二步

创建一个新类并命名为 MyDerivedMediaStreamSource,并覆盖所有必需的方法。

public class MyDerivedMediaStreamSource : MediaStreamSource
{
    // Declare some variables
    // ...

    protected override void OpenMediaAsync() { }

        protected override void GetSampleAsync() { }

        protected override void SeekAsync( long seekToTime ) { }

        protected override void GetDiagnosticAsync( 
                  MediaStreamSourceDiagnosticKind diagnosticKind ) { }

        protected override void SwitchMediaStreamAsync( 
                  MediaStreamDescription mediaStreamDescription ) { }

        protected override void CloseMedia() { }

        // Other supporting methods ...
}

然后,我们在派生类中声明一些成员变量

// Media Stream Description
MediaStreamDescription _videoDesc; 

// best to set these after retrieving actual values from the video metadata
private const int _frameWidth = 640; 
private const int _frameHeight = 480;
public static long _speed = 30; // 30fps

// Rendering time in the media
private long _timeStamp = 0;
                
// Number of bytes of each pixel (4 bytes - RGBA)
// for the samples we supply to MediaElement
private const int _framePixelSize = 4;

// Size in bytes for each Sample of type RGBA (4 bytes per pixel)
private const int _count = _frameHeight * _frameWidth * _framePixelSize;

// Size in bytes of the stream (same as the Sample size in bytes above)
private const int _frameStreamSize = _count;

// Stream to contain a Sample
private MemoryStream _stream = new MemoryStream( _frameStreamSize );

// The Offset into the stream where the actual sample data begins
private int _offset = 0;

// Buffer to hold a collection of type Sample. 
private Queue<Sample> sampleBufferList = new Queue<Sample>();

// Timeout period (fps from video is used). 
private TimeSpan timeout = TimeSpan.FromSeconds((double)_speed);

// Empty Dictionary used in the returned sample.
private Dictionary<MediaSampleAttributeKeys, string> emptyDictionary = 
    new Dictionary<MediaSampleAttributeKeys, string>();

// Total number of Samples to buffer.
// I set to 15 as an example, but you can increase or decrease
private const int numberOfSamplesBuffer = 15;

上面不是变量的完整集合,但我们将声明更多与稍后代码中定义的支撑方法相关的变量。此支撑方法主要用于处理样本并将其放入缓冲区(上面声明的 sampleBufferList)。

OpenMediaAsync() 方法(请参见下面的代码)是我们将执行媒体初始化的地方,并通过覆盖的方法 GetSampleAsync() 通知 MediaElement 我们已准备好为其提供媒体样本。现在,让我们在覆盖的 OpenMediaAsync() 方法中添加一些代码,如下所示。

protected override void OpenMediaAsync()
{

    Dictionary<MediaStreamAttributeKeys, string> streamAttributes = 
       new Dictionary<MediaStreamAttributeKeys, string>();

    // We are going to convert our video frame/sample from RGB to RGBA
    streamAttributes[MediaStreamAttributeKeys.VideoFourCC] = "RGBA";

    streamAttributes[MediaStreamAttributeKeys.Height] = _frameHeight.ToString();

    streamAttributes[MediaStreamAttributeKeys.Width] = _frameWidth.ToString();

    _videoDesc = new MediaStreamDescription( MediaStreamType.Video, streamAttributes );

    List<MediaStreamDescription> availableStreams = 
            new List<MediaStreamDescription>();

    availableStreams.Add(_videoDesc);

    Dictionary<MediaSourceAttributesKeys, string> sourceAttributes = 
      new Dictionary<MediaSourceAttributesKeys, string>();

    sourceAttributes[MediaSourceAttributesKeys.Duration] =  
            TimeSpan.FromSeconds(0).Ticks.ToString();

    sourceAttributes[MediaSourceAttributesKeys.CanSeek] = false.ToString();
    ReportOpenMediaCompleted(sourceAttributes, availableStreams);

    return;
}

在初始化并设置必要的元数据后,我们通过使用 ReportOpenMediaCompleted() 方法来通知 MediaElement 控件我们已准备好开始提供样本。此方法接受两个参数:第一个是描述整个媒体流功能的 Dictionary,第二个是描述每个音频/视频流的描述。在我们的示例中,我们只演示一个流:视频。

请注意,在代码中,我们将字符串值“RGBA”传递给属性键 VideoFourCCAVIDll.dll 返回一个包含 RGB 类型样本的字节流。我们将添加一个额外的字节,该字节将代表 RGB => RGBA 的额外 alpha 通道,表示一个四通道未压缩视频样本。这被定义为实例化视频编解码器所需的数据,也称为 FourCC(四字符值)。

接下来的两个属性 WidthHeight 是不言自明的。然后,我们创建一个新的 MediaStreamDescription 实例并将其添加到 availableStream 列表中。完成后,我们调用 ReportOpenMediaCompleted() 并传入流属性和可用的流。

调用 ReportOpenMediaCompleted() 后,MediaElement 将开始播放我们的媒体,通过覆盖的 GetSampleAsync() 方法请求视频样本。所以,这就是我们需要以某种方式获取视频样本的地方,每次我们的 MediaElement 请求一个样本时,并通知 MediaElement 完成。

现在让我们编写 GetSampleAsync() 方法的代码。在我们的实现中,我们只是启动一个新线程来处理获取样本的过程,并立即从方法返回。

protected override void GetSampleAsync( MediaStreamType mediaStreamType )
{
    if (mediaStreamType == MediaStreamType.Video)
    {
        // start a thread to get the sample  
        Thread thread = new Thread(new ThreadStart( this.retrieveSampleThread));
        thread.Start();

        return;
    }
}

该线程将继续检查样本。请注意,我们在 OpenMediaAsync() 方法中初始化媒体时只定义了一个 MediaStreamType“Video”,但为了说明起见,我们检查 MediaElement 请求的媒体流的类型。如果您添加了多个流(例如视频和音频),则需要检查请求的媒体流类型。

上面的线程将负责在样本可用时通过调用 ReportGetSampleCompleted() 将样本返回给 MediaElement,或者在无法及时返回任何样本时,通过调用 ReportGetSampleProgress() 通知 MediaElement 我们仍在缓冲。

请注意,在我的示例实现中,我决定创建一个一次只包含一个样本的流。您可以决定创建一个完整的流并从文件中读取,因此每次请求样本时可能需要将流指针移到开头。

private void retrieveSampleThread()
{
    // seek to the beginning of the stream
    _frameStream.Seek(0, SeekOrigin.Begin);
    _frameStreamOffset = 0;    // Instantiate a Sample
    Sample _sample = null;

    // try to lock our object (basically the sampleBufferList)
    lock (this)
    {
        // check if our sampleBufferList is empty
        if (this.sampleBufferList.Count == 0)
        {
            // Release the object and try to reacquire
            if (!Monitor.Wait(this, this.timeout))
            {

                // We are busy buffering ...
                this.ReportGetSampleProgress(0);

                return;

            }
        }

        // therefore dequeue first Sample in the buffer
        _sample = this.sampleBufferList.Dequeue();

        // immediately notify a waiting thread in the queue
        Monitor.Pulse(this);

    }

    // write the retrieved Sample into the stream
    _frameStream.Write(_sample.sampleBuffer, 0, _frameBufferSize);

    MediaStreamSample mediaSample = new MediaStreamSample(
        _videoDesc,
        _stream,
        _offset,
        _count,
        _timeStamp,
        this.emptyDictionary);

    // Increment _currentTime
    // Notice that I multiply by 2. I cannot explain yet why
    _currentTime += (int)TimeSpan.FromSeconds((double)1 /_speed).Ticks * 2;
    // report back a successful Sample
    this.ReportGetSampleCompleted(mediaSample);

    return;
}

上面代码中的样本是一个带有两个成员变量 TimeBuffer 的类。我没有在代码的任何地方使用 Time。另一方面,Buffer 是实际的字节流,已从 RGB 转换为 RGBA。

sampleBufferList 的类型是 Queue<Sample>,它是一个先进先出的 Sample(对象)集合。我们首先尝试锁定我们的共享对象 sampleBufferList,以便可以弹出 Sample。如果列表为空,我们释放并尝试重新获取 sampleBufferList 的锁定,并希望它可能已经有一个 Sample。如果我们在定义的超时时间内无法重新获取锁定,我们只需通过调用 ReportGetSampleProgress() 方法来通知 MediaElement 我们正在忙于缓冲。

然而,如果成功了,我们只需从列表中出队一个 Sample,并将出队的 Sample 分配给我们的临时变量 _sample。然后,我们将我们的 Sample(字节数组)写入流(_frameStream),并实例化并初始化一个 MediaStreamSample,该对象接受媒体描述、媒体流、流中的起始位置、流中缓冲的大小以及当前时间。

我们需要增加 _currentTime,根据文档,这是媒体开始时应渲染样本的时间,以 100 纳秒为增量表示。最后,通过调用 ReportGetSampleCompleted() 方法向 MediaElement 报告我们已完成。

每次 MediaElement 请求一个新 Sample 时,都会创建此线程,这也意味着 Sample 必须在某个地方创建。然后我们问如何生成我们的样本。

为了回答这个问题,我们需要创建一个新方法,该方法将启动一个新线程,负责检索、处理和缓冲我们的媒体样本,如下面的代码所示。但首先,让我们创建与该方法相关的变量。

// flag to kill this thread
private static bool _done = true;

// Instantiate a background  Worker Thread to process Samples
private BackgroundWorker _worker = new BackgroundWorker();

// Full Path to the video file (.avi)
// Supply your own full path - please only avi files
public string _filepath = "Video_File_Full_Path_Here.avi";

// Represents the total number of frames in the video
int numFrames = 0;

// Byte Array for a single Sample (RGB format, hence * 3 bytes for each pixel)
private byte[] RGB_Sample = new byte[_frameHeight * _frameWidth* 3];

// Byte Array for a single frame (RGBA format)
private byte[] RGBA_Sample = new byte[_count];

// Set to true to rotate Sample to normal view
private bool _rotate = true;
请将“Video_File_Full_Path_Here.avi”更改为您自己的文件。它必须是一个 .avi 文件,并且您必须在计算机上安装了相关的编解码器,媒体才能被解压缩 - 否则 DLL 将无法提取帧或样本。

让我们继续编写负责生成样本的方法。此方法将在实例化我们的派生类后首先调用,但该功能将在步骤 3 中进行连接。

// Method that retrieves a Sample from an AVI Stream
public Boolean startStreamThread()
{
    if (AutomationFactory.IsAvailable)
    {
        _done = false;

        //_worker.WorkerReportsProgress = true;

        _worker.DoWork += (s, ex) =>
        {
            // instantiate a COM Object (included in the zip file)
            // you have to register the COM using the command below:
            // regsvr32 aviDLL.dll
            // ProgID = myAVI.myAVIcls
            dynamic obj;
            obj = AutomationFactory.CreateObject("myAVI.myAVIcls");

            bool success;

            // openAVIFile: 
            success = obj.openAVIFile(_filepath);

            // getStream: 
            success = obj.getStream();

            // videoFPS: 
            // get the fps of the video
            _speed = obj.videoFPS();

            // if not specified, the default it to 30 frames per second
            if (_speed == 0)
                _speed = 30;

            int j = 0;
            int RGBByteCount = 3;
            int RGBAByteCount = 4;
            int pixelPos;

            // loop until the user explicitly stops
            while (!_done)
            {

                // getFrameRGBBits: returns a byte array
                // for a Sample of type RGB (3 bytes per pixel)
                // ignore the first parameter 0
                RGB_Sample= obj.getFrameRGBBits(0, _speed);

                j = 0;

                // here we first loop through each vertical lines from the bottom upwards
                // where each is _frameWidth * 3 bytes long
                //
                // The picture will appear correctly
                for (int verticalCount = RGB_Sample.Length - 1; 
                       verticalCount > -1; verticalCount -= _frameWidth * RGBByteCount)
                {
                    // next we loop through each pixel
                    // (3 bytes) of the vertical line (_frameWidth)
                    for (int horizontalCount = 0; horizontalCount 
                              < _frameWidth; horizontalCount += 1)
                    {
                        // Calculate the next pixel position from the original Sample
                        // based on the outer loop, it is calculated from bottom-right
                       pixelPos = verticalCount - (_frameWidth * RGBByteCount) + 
                         (horizontalCount * RGBByteCount) + 1;

                        RGBA_Sample[j] = RGB_Sample[pixelPos];
                        RGBA_Sample[j + 1] = RGB_Sample[pixelPos + 1];
                        RGBA_Sample[j + 2] = RGB_Sample[pixelPos + 2];

                        // Assign 1 byte for the Alpha Channel
                        RGBA_Sample[j + 3] = 0xFF;

                        //jump 4 bytes for the RGBA byte counter
                        j += RGBAByteCount;
                    }
                }

                // Instantiate and initialize a new Sample
                Sample localSample = new Sample();
                localSample.sampleBuffer = RGBA_Sample; // new RGBA Sample
                localSample.sampleTime = DateTime.Now; // Not used in this sample code

                lock (this)
                {
                    // if the buffer is full, remove one sample
                    if (this.sampleBufferList.Count == numberOfSamplesBuffer)
                    {
                        this.sampleBufferList.Dequeue();
                    }

                    // add a new sample to the buffer
                    this.sampleBufferList.Enqueue(localSample);

                    Monitor.Pulse(this);
                }

            }

            // call our COM object to releases any resources and close the AVI File
            obj.closeFrames();

        };        // start the thread
        _worker.RunWorkerAsync();

    }
    else
    {
        return false;
    }

    return true;
}

在我们的 BackgroundWorker _workerDoWork() 方法中,我们首先使用 System.Runtime.InteropServices.Automation.AutomationFactoryCreateObject 和 ProgID“myAVI.myAVIcls”来实例化我们的 COM 对象。

如前所述,此 DLL 的详细信息不属于本文的范围 - 上面提供了一些链接,以帮助您打开 AVI 文件并提取帧等。

您需要调用 COM 对象 obj 的公开函数 openAVIFile(),并传入视频文件的完整路径。该函数将返回一个布尔值以指示成功或失败。

然后调用 getStream() 函数,该函数实际上不返回任何流。它只打开流并返回成功或失败。closeStream()obj 中执行一些清理工作。

如果您有兴趣获取视频中的帧数,请使用 getTotalNumberOfFrames() 函数。

我们调用 objvideoFPS() 函数来获取视频的每秒帧数(FPS),并将其分配给我们的变量 _speed

然后,将我们的线程放入一个连续循环中,以确保我们的 Samples 缓冲区始终填充有样本,直到用户明确停止 - 变量 _done 将设置为 true,并且处理 _worker 线程将停止。

要从我们的视频中获取一个样本,我们调用 getFrameRGBBits() 函数,并传入两个参数。第一个实际上不执行任何操作 - 我曾打算用它来返回特定帧。第二个参数告诉对象播放速度的快慢;值越高,播放越慢。在我们的例子中,我们传入视频的实际 FPS - 这意味着播放速度将是该 FPS。通过更改第二个参数的值进行实验。

此函数返回一个原始字节数组,表示我们的样本,未压缩,类型为 RGB。所以基本上它是每像素三个字节。我们传回给 MediaElement 流的样本也是未压缩的,但类型为 RGBA,即每像素四个字节。因此,我们需要通过添加一个额外的字节来表示 Alpha 通道,从 RGB 转换为 RGBA。

如果您直接将 RGB_Sample 的第一个字节分配给 RGBA_Sample 的第一个字节,图像将显示为颠倒的 - 至少我是这样。为了将图像翻转到其正确的方向,我们开始将 RGB_Sample 的最后一个字节分配给 RGBA_Sample 的第一个字节,并且对于 RGBA_Sample 的每四个字节,我们将 Alpha 通道设置为 0xFF。

// assume N number of pixels in the original RGB Sample
// and M pixels in RGBA Sample
RGBA_Sample[Pixel 1] = frameBytes[Pixel N]
RGBA_Sample[Pixel 2] = frameBytes[Pixel N-1]
RGBA_Sample[Pixel 3] = frameBytes[Pixel N-2]
RGBA_Sample[Pixel 4] = 0xFF
...
RGBA_Sample[Pixel M - 3] = frameBytes[Pixel 3]
RGBA_Sample[Pixel M - 2] = frameBytes[Pixel 2]
RGBA_Sample[Pixel M - 1] = frameBytes[Pixel 1]
RGBA_Sample[Pixel M] = 0xFF

下载中的示例代码包含了 UI 中添加的翻转复选框的功能。

步骤 3

现在,让我们编写一些将 UI 连接到我们的自定义派生类的代码,以完成我们的目标。

在我们的 MainPage.xaml 代码隐藏中,我们首先实例化我们的自定义派生 MediaStreamSource 类。为了将所有内容投入运行,请初始化我们的自定义 MediaStreamSource 并调用其公共方法 startStreamThread() 来打开我们的视频并开始缓冲,这样当我们的 MediaElement 请求第一个样本(以及后续样本)时,我们的派生对象将准备好满足这些请求。最后,将我们的自定义 MediaStreamSource 对象设置为 MediaElement 的媒体源,瞧,我们的应用程序就可以渲染 .avi 视频了。

// Instantiate our derived MediaStreamSource class
Classes.MyDerivedMediaStreamSource _mediaSource;

public MainPage()
{
    InitializeComponent();
    Loaded += new RoutedEventHandler(MainPage_Loaded);
}

void MainPage_Loaded(object sender, RoutedEventArgs e)
{
    OpenStream.Click += new RoutedEventHandler((s, ex) =>
    {
        // initialize our media stream object
        _mediaSource = new Classes.MyDerivedMediaStreamSource();

        if (_mediaSource.startStreamThread())
        {
            // set flag to true - media has been opened
            mediaOpen = true;

            // set the source of our media stream to the MediaElement
            mediaPlayer.SetSource(_mediaSource);
        }
    });

    CloseStream.Click += new RoutedEventHandler((s, ex) =>
    {
        if (mediaOpen)
        {
            mediaPlayer.Stop();
            _mediaSource.closeStream();
            _mediaSource = null;
            mediaOpen = false;
        }
    });

    chkFlip.Checked += new RoutedEventHandler((s, ex) =>
    {
        _mediaSource.flipped(true);
    });

    chkFlip.Unchecked += new RoutedEventHandler((s, ex) =>
    {
        _mediaSource.flipped(false);
    });
}

在 Silverlight 5 出现之际,我们预期开发者将拥有更多的权力和控制权,例如能够从受信任的 Silverlight 应用程序调用非托管代码(使用 P/Invoke)等等。

希望这篇简单的文章对大家有所帮助!

请记住在运行应用程序之前使用 regsvr32 注册 DLL。
© . All rights reserved.