问题背景:
项目中通过调用设备SDK获取到设备的实时视频流数据,经解码库解码后是YUV数据,需要在Silverlight客户端播放这些数据。
参考资料(主要):
在 Silverlight 应用程序中实现对 FLV 视频格式的支持 silverlight的MediaElement控件如何播放YV12格式的视频数据?
涉及对象:
MediaElement MediaStreamSource具体实践:
首先使用这样一个类来存放从设备获取来的流数据信息,包括了流数据,视频宽高,视频时间。
publicclass StreamData
{
[DataMember]
publicbyte[] Streams;
[DataMember]
publicint Width;
[DataMember]
publicint Height;
[DataMember]
publicint Stamp;
}
MediaElement提供了SetSource方法,有两个重载。MSDN中提到已具有可用作流的媒体的情形,请使用此方法,而不将Source设置为 URI。
而我们需要用的就是第二个重载,通过继MediaStreamSource类,按我们自己的需求重写其中的方法来处理我们得到的流,就能播放流了。
以下就重写的类的代码:
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.IO;
using System.Collections.Generic;
using System.Globalization;
namespace SilverlightApplication1
{
publicclass YV12MediaStreamSource : MediaStreamSource
{
Stream _mediaStream;
Dictionary<MediaSampleAttributeKeys, string> _mediaSampleAttributes =new Dictionary<MediaSampleAttributeKeys, string>();
Dictionary<MediaStreamAttributeKeys, string> _mediaStreamAttributes =new Dictionary<MediaStreamAttributeKeys, string>();
List<MediaStreamDescription> _mediaStreamDescriptions =new List<MediaStreamDescription>();
privateint _stamp;
privateint _width;
privateint _height;
privatelong _offect;
privatelong _count;
privatelong _timeStamp;
privatelong _duration;
public YV12MediaStreamSource(Stream stream, int width, int height, int stamp)
{
this._mediaStream = stream;
this._width = width;
this._height = height;
this._stamp = stamp;
}
privatevoid InitData()
{
_offect =0;
_count = _mediaStream.Length;
_timeStamp = TimeSpan.FromMilliseconds(0).Ticks;
_duration = TimeSpan.FromMilliseconds(_stamp).Ticks;
}
#region Override
protectedoverridevoid OpenMediaAsync()
{
InitData();
//描述媒体示例
_mediaSampleAttributes[MediaSampleAttributeKeys.FrameHeight] = _height.ToString();
_mediaSampleAttributes[MediaSampleAttributeKeys.FrameWidth] = _width.ToString();
//描述媒体流
_mediaStreamAttributes[MediaStreamAttributeKeys.Height] = _height.ToString();
_mediaStreamAttributes[MediaStreamAttributeKeys.Width] = _width.ToString();
_mediaStreamAttributes[MediaStreamAttributeKeys.CodecPrivateData] ="";
_mediaStreamAttributes[MediaStreamAttributeKeys.VideoFourCC] ="YV12";
//详尽描述媒体流
_mediaStreamDescriptions.Add(new MediaStreamDescription(MediaStreamType.Video, _mediaStreamAttributes));
//描述媒体源
Dictionary<MediaSourceAttributesKeys, string> mediaSourceAttributes =new Dictionary<MediaSourceAttributesKeys, string>();
mediaSourceAttributes[MediaSourceAttributesKeys.CanSeek] ="false";
mediaSourceAttributes[MediaSourceAttributesKeys.Duration] = _duration.ToString(CultureInfo.InvariantCulture);
this.ReportOpenMediaCompleted(mediaSourceAttributes, _mediaStreamDescriptions);
}
protectedoverridevoid GetSampleAsync(MediaStreamType mediaStreamType)
{
MediaStreamSample mediaSample = GetVideoSample();
ReportGetSampleCompleted(mediaSample);
}
private MediaStreamSample GetVideoSample()
{
MediaStreamDescription msd =new MediaStreamDescription(MediaStreamType.Video, _mediaStreamAttributes);
//详尽描述媒体示例
MediaStreamSample mediaSample =new MediaStreamSample(
msd,
_mediaStream,
_offect,
_count,
_timeStamp,
_mediaSampleAttributes);
return mediaSample;
}
protectedoverridevoid SeekAsync(long seekToTime)
{
this.ReportSeekCompleted(seekToTime);
}
protectedoverridevoid CloseMedia()
{
}
protectedoverridevoid SwitchMediaStreamAsync(MediaStreamDescription mediaStreamDescription)
{
}
protectedoverridevoid GetDiagnosticAsync(MediaStreamSourceDiagnosticKind diagnosticKind)
{
}
#endregion
#region Public
publicvoid AddStream(Stream stream)
{
this._mediaStream = stream;
}
#endregion
}
}
以下是对重写的那几个方法的解释:
OpenMediaAsync:收集实例化 MediaStreamDescription 对象的集合所需的元数据,然后对其进行实例化
GetSampleAsync:导致 MediaStreamSource 准备一个 MediaStreamSample,它描述要由媒体管线呈现的下一个媒体示例。可以通过 ReportGetSampleCompleted 和 ReportGetSampleProgress 响应此方法。
SeekAsync:采用给定的偏移量,并确保对 GetSampleAsync 的将来调用将返回在该点开始的示例。
其中需要特别指出的就是这句话
_mediaStreamAttributes[MediaStreamAttributeKeys.VideoFourCC] = "YV12";
通过这里支持的媒体格式、协议和日志字段大家能够查看到Silverlight支持的媒体格式和协议,我们这里用到的"YV12"就是其中之一,如果大家需要使用Silverlight内置的其他解码库,只需要设置相应的属性即可。
(FourCC全称Four-Character Codes,是由4个字符(4 bytes)组成,是一种独立标示视频数据流格式的四字节。http://www.fourcc.org/)
因为这里所要处理的流数据相对简单,MediaStreamSource可能很多其他问题并没考虑到,也不在这次处理的问题范围内,有兴趣的朋友可以仔细研究下,我也还在摸索。
数据有了,解码工作也做好了,就剩下播放啦:
{
StreamData sources = sender as StreamData;
Stream stream =new MemoryStream(sources.Streams);
if (sources.Stamp ==0)
return;
streamSource =new YV12MediaStreamSource(stream, sources.Width, sources.Height, sources.Stamp);
//element即MediaElement对象
element.SetSource(streamSource);
}
OK,可以顺利看到图像了!
等会,怎么图像闪的厉害呢!?这就出现问题了,也到了我们使用技巧的时候了(之所以说是技巧是因为在对这个问题处理时取了巧,并没有从根源上解决问题,看哪位朋友能有更好的办法么!?)
之所以会出现这种现象,个人认为是因为每调用一次SetSource方法,播放器都会重新加载一次,而实时流数据可能不到1秒就会更新一次,就相当于在不停的调用SetSource方法,出来的效果就是屏幕狂闪,这样操作的效果就类似于我们在普通播放器里不停双击希望不间断的打开不同的文件一样。
细心的朋友可能会主要到了,代码最末有一个公开的AddStream方法,我就是通过它来取巧的,最后播放的方法就变成了这样:
{
StreamData sources = sender as StreamData;
Stream stream =new MemoryStream(sources.Streams);
if (sources.Stamp ==0)
return;
if (isFirst)
{
object obj =newobject();
lock (obj)
{
streamSource =new YV12MediaStreamSource(stream, sources.Width, sources.Height, sources.Stamp);
ShowStream(streamSource, element);
isFirst =false;
}
}
else
{
try
{
ShowStreamToo(stream, element);
}
catch (System.Exception ex)
{
}
}
}
privatevoid ShowStream(MediaStreamSource streamSource, MediaElement element)
{
element.SetSource(streamSource);
}
privatevoid ShowStreamToo(Stream stream, MediaElement element)
{
element.Pause();
streamSource.AddStream(stream);
element.Play();
}
因为涉及到专业的设备,写的Demo大家拿去也用不了,关键代码文章里也都贴出来了,付结果图一张:
至此想要播放YUV视频流的目的算是达到了,但是实际项目中并没有采用这种方式,首先YUV格式数据量太大,用来实际传输并不现实;其次可以考虑在服务端重新编码,但是这样一来服务端压力也随之增大,反倒成了累赘了,也一直在想有没有更好的实现方式,暂时还没想到,希望对这方面有想法的朋友多多指教。
文章旨在解决问题,技术方面的东西并没有讲解的十分清楚,因为我自己也还在学习阶段,有讲的的不对的或者有问题的地方欢迎大家指出。
应朋友们的需要,现提供示例代码供大家下载。
还可参见我的后续文章MediaElement应用之顺畅播放实时流 。
谢谢支持!