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






4.67/5 (7投票s)
此代码演示了如何使用 Silverlight 与 OOB+elevated trust(脱机模式+提升的信任)来播放本地视频(.avi)。
引言
本文尝试演示 MediaElement
和 MediaStreamSource
类的强大功能。在本文中,我们将尝试编写一些代码来播放位于您计算机本地的 AVI 视频文件。
背景
借助 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
派生我们的自定义类。这将要求我们覆盖许多方法。不深入细节,这些方法是 OpenMediaAsync
、GetSampleAsync
、CloseMedia
、SeekAsync
、GetDiagnosticsAsync
和 SwitchMediaStreamAsync
。我将不深入研究这些方法的定义,但我们在示例代码中将使用的部分是
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.xaml 的
UserControl
。 - 添加一个
MediaElement
控件并命名为mediaPlayer
。 - 添加两个按钮并命名为
OpenStream
和CloseStream
。 - 添加一个复选框并命名为
chkFlip
。
确保启用“脱机”模式(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
对象并将其作为媒体流分配给 mediaPlayer
。CloseStream
按钮将用于关闭和停止流。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”传递给属性键 VideoFourCC
。AVIDll.dll 返回一个包含 RGB 类型样本的字节流。我们将添加一个额外的字节,该字节将代表 RGB => RGBA 的额外 alpha 通道,表示一个四通道未压缩视频样本。这被定义为实例化视频编解码器所需的数据,也称为 FourCC(四字符值)。
接下来的两个属性 Width
和 Height
是不言自明的。然后,我们创建一个新的 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;
}
上面代码中的样本是一个带有两个成员变量 Time
和 Buffer
的类。我没有在代码的任何地方使用 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;
让我们继续编写负责生成样本的方法。此方法将在实例化我们的派生类后首先调用,但该功能将在步骤 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 _worker
的 DoWork()
方法中,我们首先使用 System.Runtime.InteropServices.Automation.AutomationFactory
的 CreateObject
和 ProgID“myAVI.myAVIcls
”来实例化我们的 COM 对象。
如前所述,此 DLL 的详细信息不属于本文的范围 - 上面提供了一些链接,以帮助您打开 AVI 文件并提取帧等。
您需要调用 COM 对象 obj
的公开函数 openAVIFile()
,并传入视频文件的完整路径。该函数将返回一个布尔值以指示成功或失败。
然后调用 getStream()
函数,该函数实际上不返回任何流。它只打开流并返回成功或失败。closeStream()
在 obj
中执行一些清理工作。
如果您有兴趣获取视频中的帧数,请使用 getTotalNumberOfFrames()
函数。
我们调用 obj
的 videoFPS()
函数来获取视频的每秒帧数(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)等等。
希望这篇简单的文章对大家有所帮助!