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

使用 MediaElement 和 MediaStreamSource 在 Silverlight 5 中播放 AVI 文件 - P/Invoke

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2011 年 9 月 8 日

CPOL

5分钟阅读

viewsIcon

42226

downloadIcon

1774

此代码演示了如何使用支持 P/Invoke 调用本地代码的 Silverlight 5 离线+提升信任来播放本地视频(.avi)。

引言

Silverlight 4 不支持通过 P/Invoke 调用本地函数;Silverlight 5 支持(有关更多详细信息,请参阅 Pete Brown 的博客)。本教程的目标与我之前一篇标题几乎完全相同的文章 《上一篇文章》 别无二致。那篇文章涉及 COM 支持。首先阅读那篇文章很重要——其中涵盖的大部分步骤在这里将不再赘述。

简而言之,正如上一篇文章一样,本教程旨在通过支持 P/Invoke 调用本地代码以播放 AVI 视频的 MediaElementMediaStreamSource 类来展示其强大功能。


******************************************************
请注意,第二个下载(CMS_S5_plusWriteableBitmap)包括使用 WriteableBitmap 显示相同的视频样本。此处未对代码进行解释。
*******************************************************

CustomMediaStreamSourceS5Img1.JPG

仅 MediaElement

WriteableBitmapSL5.JPG

MediaElement(MediaStreamSource)和 WriteableBitmap 实现

背景

截至 2011 年 9 月 1 日,Silverlight 5 RC 已可下载(请参阅 Pete 的博客 - 上方链接)。Silverlight 5 附带平台调用支持,可用于调用本地代码(以及许多其他出色功能)。

回顾上一篇文章,我们需要首先派生我们的自定义类,使其继承自 System.Windows.Media.MediaStreamSource。这将要求我们重写多个方法。不详细介绍,这些方法是 OpenMediaAsyncGetSampleAsyncCloseMediaSeekAsyncGetDiagnosticsAsyncSwitchMediaStreamAsync。我不会深入研究这些方法的定义,但我们将在示例代码中使用的方法是:

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

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

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

  • 准备一个简单的 UI,包含一个 MediaElement 控件、两个 button 和一个 checkbox
  • 编写我们的自定义类,使其派生自 System.Windows.Media.MediaStreamSource 并重写所有必需的方法。
  • 在后台代码中,将我们的自定义类设置为 UI 中 MediaElement 控件的源流。

此示例使用 Silverlight 5 RC 进行了测试?

步骤 1

CustomMediaStreamSourceS5Img2.JPG

  • 创建一个新的 Silverlight 5 项目(VB.NET),并为它取任何您喜欢的名称。
  • 请确保按照上面的项目属性设置,启用“脱机运行”并确保选中“需要提升的信任...”复选框。
  • 打开名为 MainPage.xaml 的默认创建的 UserControl
  • 将 XAML 代码修改为如下所示,其中包含 MediaElement(名为 mediaPlayer)和两个 button(名为 OpenStreamCloseStream)。
<Grid x:Name="LayoutRoot" Background="Black">
        <MediaElement x:Name="mediaPlayer" 
                      AutoPlay="True"
                      Stretch="UniformToFill"
                      Margin="31,61,33,30"
                      Opacity="1" />
        <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" />
</Grid>        

MediaElement 控件 mediaPlayer 将用于显示我们的视频。OpenStream 按钮将用于初始化我们的自定义 MediaStreamSource 对象,并将其分配给 mediaPlayer 的媒体流。CloseStream 按钮将用于关闭和停止流。

我们将回到后台代码并连接剩余的代码。

第二步

请按照我之前文章的 第二步 操作,因为这里的代码与该步骤中的代码基本相同。

让我们修改重写的存根,并在 mediaElement 每次请求一个 Sample 时调用(retrieveSample)检索 Sample 的方法。当然,这不是最好的实现——您可能希望启动一个线程并立即返回,让线程在拥有 Sample 时通知 mediaElement

Protected Overrides Sub GetSampleAsync
	(mediaStreamType As System.Windows.Media.MediaStreamType)

        If mediaStreamType = mediaStreamType.Video Then

            retrieveSample()

            Return

        End If

End Sub    

同样,与上一篇文章一样,我决定创建一个一次只包含一个样本的 stream

现在让我们看看 retrieveSample() 方法。

Private Sub retrieveSample()

        '---------------------------------------------------------------
        ' I CANNOT REMEMBER WHERE I GOT THIS CALCULATION FROM ONLINE
        ' it basically calculates the frame number based on time passed
        ' since start of play
        '-------------------------**********----------------------------
        If initcapture = 0 Then
            timeexpended = timeGetTime() - initialtime
        Else
            frametime = 0
            timeexpended = 0

            initialtime = timeGetTime()
        End If

        If initcapture = 0 Then
            If timelapse > 1000 Then
                frametime = frametime + 1
            Else
                frametime = (timeexpended / 1000) * (1000 / _speed) 'timelapse)

                If frametime >= numFrames Then
                    initializeFrametime()
                End If
            End If
        Else
            initcapture = 0
        End If
        '-------------------------**********----------------------------
        '
        '---------------------------------------------------------------

        ' Get the requested Frame from the Stream - frame number "frametime" provided.
        ' Return a Pointer to the DIB
        pDIB = AVIStreamGetFrame(pGetFrameObj, frametime)


        If pDIB <> 0 Then

            'Copy the Frame bits into RGB_Sample
            'Call CopyMemory(RGB_Sample(0), pDIB + Marshal.SizeOf(bih), bih.biSizeImage)
            Marshal.Copy(pDIB + Marshal.SizeOf(bih), RGB_Sample, 0, bih.biSizeImage)

            If Not RGB_Sample Is Nothing Then

                For verticalCount As Integer = RGB_Sample.Length - 1 _
			To 0 Step _frameWidth * RGBByteCount * -1

                    For horizontalCount As Integer = 0 To _frameWidth - 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) = 255 '&FF

                        'jump 4 bytes for the RGBA byte counter
                        j += RGBAByteCount

                    Next
                Next

            End If

            j = 0

        End If

        ' Instantiate a Sample
        ' The Sample has two members (Time & Buffer)           
        Dim _sample As Sample = New Sample()
        _sample.sampleBuffer = RGBA_Sample
        _sample.sampleTime = DateTime.Now


        ' We always start at the beginning of the stream
        ' this is because we always reset the stream with one sample at a time
        ' if you decide to add more than one sample into the stream then you
        ' can modify the logic to increment this offset by the size of the sample
        ' everytime there is a call to return a sample
        _offset = 0
        _stream.Seek(0, SeekOrigin.Begin)

        ' write the retrieved Sample into the stream
        ' remember our stream is just one Sample
        _stream.Write(_sample.sampleBuffer, 0, _count)

        Dim mediaSample As MediaStreamSample = New MediaStreamSample(
                Me._videoDesc,
                _stream,
                _offset,
                _count,
                _timeStamp,
                Me.emptyDictionary)

        _timeStamp += TimeSpan.FromSeconds(1 / _speed).Ticks

        ' report back a successful Sample
        Me.ReportGetSampleCompleted(mediaSample)

        Return

End Sub  

上面这个方法会在每次 mediaElement 请求 Sample 时被调用。为了知道从视频流中提取哪个帧,我们根据自播放开始以来的当前时间进行计算。所以,简而言之,帧时间就是视频中的帧号。

我们使用本机函数 AVIStreamGetFrame,通过传递 Frames 对象和要提取的帧号,来返回一个指向打包 DIB 的 Pointer。如果一切顺利,我们将忽略 Bitmap Info Header,开始将帧的字节(信息头之后的大小)复制到 RGB_Sample 中。

现在我们有了代表未压缩样本的原始字节,每像素三个字节。我们将返回给 MediaElement stream 的样本也是未压缩的,但类型为 RGBA,每像素四个字节。因此,我们需要将 RGB 转换为 RGBA,添加一个额外的字节来表示 Alpha 通道。

如果您直接将 RGB_Sample 中的第一个字节赋给 RGBA_Sample 中的第一个字节,图像会是颠倒的——至少我是这样。为了纠正图像方向,我们从 RGB_Sample 的最后一个字节开始赋给 RGBA_Sample 的第一个字节,对于 RGBA_Sample 的每第四个字节,我们将 Alpha 通道设置为 0xFF 或 255

' 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] = 255
...
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] = 255     

如果没有打开 AVI 文件并获取指向文件本身、AVIStreamStream 信息的内存 Pointer 的代码,那么上面这些是不可能实现的。这通过使用本机函数 AVIFileOpenAVIFileGetStreamAVIStreamInfo 等来实现。

Public Function startVideo(fname As String) As Boolean

        ' Return a Pointer of the File in Memory assigned to pAVIFile
        Dim res As Int32 = AVIFileOpen(pAVIFile, fname, 32, 0)

        ' Return a Pointer of the Video Stream in Memory assigned to pAVIStream
        res = AVIFileGetStream(pAVIFile, pAVIStream, streamtypeVIDEO, 0)

        ' Return the First Video Frame Number
        firstFrame = AVIStreamStart(pAVIStream)

        ' Return the Total Number of Frames in the Stream
        numFrames = AVIStreamLength(pAVIStream)

        ' Return the Video Stream Information
        res = AVIStreamInfo(pAVIStream, streamInfo, Marshal.SizeOf(streamInfo))

        _speed = streamInfo.dwRate 'fps

        With bih
            .biBitCount = 24
            .biClrImportant = 0
            .biClrUsed = 0
            .biCompression = BI_RGB
            .biHeight = streamInfo.rcFrame.bottom - streamInfo.rcFrame.top
            .biPlanes = 1
            .biSize = 40
            .biWidth = streamInfo.rcFrame.right - streamInfo.rcFrame.left
            .biXPelsPerMeter = 0
            .biYPelsPerMeter = 0
            .biSizeImage = (((.biWidth * 3) + 3) And &HFFFC) * .biHeight
        End With

        _frameWidth = bih.biWidth
        _frameHeight = bih.biHeight

        _count = _frameHeight * _frameWidth * _framePixelSize
        _frameStreamSize = _count

        numScans = IIf(_frameHeight > _frameWidth, _frameHeight, _frameWidth)

        ' Retrieve the Frames Object from the stream byte by supplying 
        ' the Stream and the format we expect
        pGetFrameObj = AVIStreamGetFrameOpen(pAVIStream, bih)

        If pGetFrameObj = 0 Then Return False 'ERROR

        ' Resize our actual Samples in Bytes
        ReDim RGB_Sample(bih.biSizeImage - 1)

        ' Resize our final modified sample in Bytes
        ReDim RGBA_Sample(_count - 1)

        initializeFrametime()

        Return True

End Function    

步骤 3

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

在我们的 MainPage.xaml 后台代码中,我们首先实例化我们自定义派生的 MediaStreamSource 类。为了让一切生效,请初始化我们的自定义 MediaStreamSource 并调用其 public 方法 startVideo(_filename) 来打开我们的视频并开始缓冲,这样当我们的 MediaElement 请求第一个样本(以及后续样本)时,我们的派生对象将准备好满足这些请求。最后,将我们的自定义 MediaStreamSource 对象设置为 MediaElement 的媒体源,这样,我们的应用程序就可以渲染 *.avi 视频了。

Partial Public Class MainPage
    Inherits UserControl

    ' Instantiate our derived MediaStreamSource class
    Dim _mediaSource As MyDerivedMediaStreamSource

    '<summary>
    '    Flag to indicate if our media has been opened or not
    '</summary>
    Dim mediaOpen As Boolean = False

    Dim _filename As String = "Video_File_Full_Path_Here.avi"

    'Dim aviObj As cAVIDefs

    Public Sub New()

        InitializeComponent()

    End Sub

    Private Sub OpenStream_Click(sender As System.Object, _
	e As System.Windows.RoutedEventArgs) Handles OpenStream.Click

        ' initialize our media stream object
        _mediaSource = New MyDerivedMediaStreamSource()

        ' check if we succeeded in opening the AVI
        If _mediaSource.startVideo(_filename) Then

            ' set flag to true - media has been opened
            mediaOpen = True

            ' set the source of our media stream to the MediaElement
            mediaPlayer.SetSource(_mediaSource)

        End If

    End Sub

    Private Sub CloseStream_Click(sender As System.Object, _
	e As System.Windows.RoutedEventArgs) Handles CloseStream.Click

        If mediaOpen Then ' check if we still have the media open

            mediaPlayer.Stop()

            _mediaSource.closeStream()


            _mediaSource = Nothing

            mediaOpen = False

        End If
    End Sub

End Class 

请将“Video_File_Full_Path_Here.avi”更改为您自己的文件。它必须是一个 *.avi 文件,并且您必须在计算机上安装相应的编解码器才能解压缩媒体。

我希望这个简单的教程对大家有所帮助。

© . All rights reserved.