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





5.00/5 (2投票s)
此代码演示了如何使用支持 P/Invoke 调用本地代码的 Silverlight 5 离线+提升信任来播放本地视频(.avi)。
- 下载源代码 - 78.15 KB(仅限
MediaElement
) - 下载 CMSS_S5_plusWriteableBitmap - 86.79 KB(
MediaElement
加WriteableBitmap
)
引言
Silverlight 4 不支持通过 P/Invoke 调用本地函数;Silverlight 5 支持(有关更多详细信息,请参阅 Pete Brown 的博客)。本教程的目标与我之前一篇标题几乎完全相同的文章 《上一篇文章》 别无二致。那篇文章涉及 COM 支持。首先阅读那篇文章很重要——其中涵盖的大部分步骤在这里将不再赘述。
简而言之,正如上一篇文章一样,本教程旨在通过支持 P/Invoke 调用本地代码以播放 AVI 视频的 MediaElement
和 MediaStreamSource
类来展示其强大功能。
******************************************************
请注意,第二个下载(CMS_S5_plusWriteableBitmap
)包括使用 WriteableBitmap
显示相同的视频样本。此处未对代码进行解释。
*******************************************************
仅 MediaElement
MediaElement(MediaStreamSource)和 WriteableBitmap 实现
背景
截至 2011 年 9 月 1 日,Silverlight 5 RC 已可下载(请参阅 Pete 的博客 - 上方链接)。Silverlight 5 附带平台调用支持,可用于调用本地代码(以及许多其他出色功能)。
回顾上一篇文章,我们需要首先派生我们的自定义类,使其继承自 System.Windows.Media.MediaStreamSource
。这将要求我们重写多个方法。不详细介绍,这些方法是 OpenMediaAsync
、GetSampleAsync
、CloseMedia
、SeekAsync
、GetDiagnosticsAsync
和 SwitchMediaStreamAsync
。我不会深入研究这些方法的定义,但我们将在示例代码中使用的方法是:
OpenMediaAsync
:我们重写此方法,并在其中通过调用ReportOpenMediaCompleted()
方法来初始化和报告有关媒体的一些元数据。GetSampleAsync
:我们重写此方法,并检索MediaElement
请求的下一个样本。MediaElement
每次需要样本时都会调用此方法。为了向MediaElement
报告样本已准备好,我们调用ReportGetSampleCompleted()
方法。
本文的主要目标是编写一个简单的 Silverlight 应用程序来播放 AVI 视频。要播放视频(*.avi),您必须首先在计算机上安装相应的编解码器。
我们将使用以下简单步骤来实现我们的目标:
- 准备一个简单的 UI,包含一个
MediaElement
控件、两个button
和一个checkbox
。 - 编写我们的自定义类,使其派生自
System.Windows.Media.MediaStreamSource
并重写所有必需的方法。 - 在后台代码中,将我们的自定义类设置为 UI 中
MediaElement
控件的源流。
此示例使用 Silverlight 5 RC 进行了测试?
步骤 1
- 创建一个新的 Silverlight 5 项目(VB.NET),并为它取任何您喜欢的名称。
- 请确保按照上面的项目属性设置,启用“脱机运行”并确保选中“需要提升的信任...”复选框。
- 打开名为 MainPage.xaml 的默认创建的
UserControl
。 - 将 XAML 代码修改为如下所示,其中包含
MediaElement
(名为mediaPlayer
)和两个button
(名为OpenStream
和CloseStream
)。
<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 文件并获取指向文件本身、AVIStream
和 Stream
信息的内存 Pointer
的代码,那么上面这些是不可能实现的。这通过使用本机函数 AVIFileOpen
、AVIFileGetStream
、AVIStreamInfo
等来实现。
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 文件,并且您必须在计算机上安装相应的编解码器才能解压缩媒体。
我希望这个简单的教程对大家有所帮助。