众所周知,Windows Phone SDK自带的MediaElement不支持FLV容器。但是根据MSDN所言[1],实际上Windows Phone在硬件上支持AAC音频和H264视频的播放。恰巧大部分网络FLV视频都是AAC+H264编码的,于是理论上我们就可以通过实现MediaStreamSource来让MediaElement认识和播放FLV视频。我在开发哔哩哔哩客户端的时候就采用了这个技术。
本文同时适用于Windows Phone 7/7.1和Windows Phone 8。在本文之前,你不会在互联网上获得任何关于如何在Windows Phone上使用MediaStreamSource播放FLV视频的完整关键代码。
既然我们不需要考虑播放AAC流和H264流的问题,那唯一的问题只剩下如何拆解FLV文件、提取音频和视频数据,然后传回给系统的解码器了。
分析FLV Header
网上有很多分析FLV文件格式的文章,我参考了这篇[2]。另外,Scott Wong大大写的这篇文章[3]也非常有帮助,对相关理论的叙述非常深入和透彻,可惜的是提供的示例代码无法直接在Windows Phone上面运行,不过弄明白了原理,之后的工作就没什么障碍了。
实现OpenMediaAsync()
实现MediaStreamSource.OpenMediaAsync()是最重要的一步,它直接决定了是否能成功开始播放视频,至于能否正常完整播放的关键将在下一篇中讲述。
首先我们需要在MediaStreamSource中定义一些私有成员变量:
private MediaStreamDescription audioStreamDescription;
private MediaStreamDescription videoStreamDescription;
private Stream mediaStream;
private List<FlvTag> audioSamples;
private List<FlvTag> videoSamples;
private int audioStreamIndex = 1;
private int videoStreamIndex = 1;
private long fileOffset = 13;
private Dictionary<MediaSampleAttributeKeys, string> emptyDict = new Dictionary<MediaSampleAttributeKeys, string>();
在OpenMediaAsync()中,我们需要正确初始化一些数据,以便让系统的解码器知道我们即将提供AAC和H264原始流,不要假装不认识然后直接报错>< 首先是音频信息:
if (sample.TagType == TagType.Audio)
{
audioSamples.Add(sample);
if (!flagA)
{
if (sample.AudioData.SoundFormat != SoundFormat.AAC)
{
MessageBox.Show("不支持该音频编码:" + sample.AudioData.SoundFormat, "错误", MessageBoxButton.OK);
this.ReportOpenMediaCompleted(new Dictionary<MediaSourceAttributesKeys, string>(), new List<MediaStreamDescription>());
return;
}
WaveFormatExtensible wfx = new WaveFormatExtensible();
wfx.FormatTag = 0x00FF;
wfx.Channels = short.Parse(sample.AudioData.SoundChannel.Substring(0, 2));
wfx.BlockAlign = 8;
wfx.BitsPerSample = 16;
string samplingrate = sample.AudioData.SoundRate.Substring(2, 2) + sample.AudioData.SoundRate.Substring(0, 2);
wfx.SamplesPerSec = int.Parse(samplingrate, NumberStyles.HexNumber);
wfx.AverageBytesPerSecond = wfx.SamplesPerSec * wfx.Channels * wfx.BitsPerSample / wfx.BlockAlign;
wfx.Size = 0;
string AverageBytesPerSecondStr = string.Format(CultureInfo.InvariantCulture, "{0:X8}", wfx.AverageBytesPerSecond).ToLittleEndian();
codecPrivateDataA = "FF00" + sample.AudioData.SoundChannel + sample.AudioData.SoundRate + "0000" + AverageBytesPerSecondStr + "080010000000";
Debug.WriteLine(codecPrivateDataA);
flagA = true;
}
}
然后是视频信息:
{
videoSamples.Add(sample);
if (!flagV)
{
if (sample.VideoData.CodecID != CodecID.AVC)
{
MessageBox.Show("不支持该视频编码:" + sample.VideoData.CodecID, "错误", MessageBoxButton.OK);
this.ReportOpenMediaCompleted(new Dictionary<MediaSourceAttributesKeys, string>(), new List<MediaStreamDescription>());
return;
}
dataV = sample.VideoData.AVCVideoPacket.AVCDecoderConfigurationRecord;
int lengthSPS = 0, lengthPPS = 0;
if (dataV != null)
{
lengthSPS = (int)(((int)dataV[6]) << 8) + (int)dataV[7];
byte[] sps = new byte[lengthSPS];
Array.Copy(dataV, 8, sps, 0, lengthSPS);
int ppsIndex = 8 + lengthSPS + 1;
lengthPPS = dataV[ppsIndex] << 8 | dataV[ppsIndex + 1];
byte[] pps = new byte[lengthPPS];
Array.Copy(dataV, ppsIndex + 2, pps, 0, lengthPPS);
StringBuilder sb = new StringBuilder(sps.Length * 2);
sb.Append("00000001");
foreach (byte b in sps)
sb.AppendFormat("{0:X2}", b);
sb.Append("00000001");
foreach (byte b in pps)
sb.AppendFormat("{0:X2}", b);
codecPrivateDataV = sb.ToString();
}
flagV = true;
}
}
最后,我们需要设置好MediaSourceAttributesKeys和MediaStreamDescription,并调用ReportOpenMediaCompleted通知系统解码器。
Dictionary<MediaStreamAttributeKeys, string> audioStreamAttributes = new Dictionary<MediaStreamAttributeKeys, string>();
audioStreamAttributes[MediaStreamAttributeKeys.CodecPrivateData] = codecPrivateDataA;
this.audioStreamDescription = new MediaStreamDescription(MediaStreamType.Audio, audioStreamAttributes);
//Video
Dictionary<MediaStreamAttributeKeys, string> videoStreamAttributes = new Dictionary<MediaStreamAttributeKeys, string>();
videoStreamAttributes[MediaStreamAttributeKeys.VideoFourCC] = "H264";
videoStreamAttributes[MediaStreamAttributeKeys.CodecPrivateData] = codecPrivateDataV;
videoStreamAttributes[MediaStreamAttributeKeys.Width] = info.Width.ToString();
videoStreamAttributes[MediaStreamAttributeKeys.Height] = info.Height.ToString();
this.videoStreamDescription = new MediaStreamDescription(MediaStreamType.Video, videoStreamAttributes);
//Media
Dictionary<MediaSourceAttributesKeys, string> mediaSourceAttributes = new Dictionary<MediaSourceAttributesKeys, string>();
mediaSourceAttributes[MediaSourceAttributesKeys.Duration] = (info.Duration * TimeSpan.TicksPerSecond).ToString(CultureInfo.InvariantCulture);
if (this.mediaStream.CanSeek)
mediaSourceAttributes[MediaSourceAttributesKeys.CanSeek] = true.ToString();
else
mediaSourceAttributes[MediaSourceAttributesKeys.CanSeek] = false.ToString();
List<MediaStreamDescription> mediaStreamDescriptions = new List<MediaStreamDescription>();
mediaStreamDescriptions.Add(this.audioStreamDescription);
mediaStreamDescriptions.Add(this.videoStreamDescription);
this.ReportOpenMediaCompleted(mediaSourceAttributes, mediaStreamDescriptions);
关于如何获取info.Width和info.Height请参考FLV格式的相关文章。
至此OpenMediaAsync()函数结束。在Windows Phone 7/7.1的模拟器中,视频有很大可能性无法播放,但是在真机上面基本都可以播放。另外,无论模拟器还是真机,可能存在播放几个视频后再播放任意视频都报3100错误的问题(甚至是曾经成功播放的视频),遇到这种情况只能重启模拟器/机器,暂时不知道原因所在,可能是系统bug。在Windows Phone 8上暂时未发现该问题。
其余内容请期待下一篇。
参考资料:
[1] Supported media codecs for Windows Phone
[2] FLV文件格式解析
[3] 在Silverlight应用程序中实现对FLV视频格式的支持
» 转载请注明来源及链接:未来代码研究所
大大求下文
楼主好棒o(≧v≦)o~~,让我燃起动力了!
很冒昧打扰,有一个问题想咨询下楼主,在播放优酷早期的视频的时候(最近上传的视频可以正常播放,2年前的好像都不行),OpenMediaAsync没有问题,状态正常切换到playing,在获取第一个Videosample的时候报
“System.OutOfMemoryException”类型的异常在 mscorlib.ni.dll 中发生,并且未在托管/本机边界之前进行处理
“System.UnauthorizedAccessException”类型的第一次机会异常在 System.Windows.ni.dll 中发生
而单独只播放音频是可以的,我用一些软件,打开未能播放的视频,发现他们的视频编码提示确实是avc,但是里面会多出来一些,像
write library:x264 core 54
Encodeing setting:caba=1 / ref=6
Stream size: 5.40 MiB (84%)
这样的信息,我不太懂这些,是否会对解析产生影响,任何提示,不胜感激!
能不能给一个b站av号做测试?可能是获取的某个length值有误导致分配内存时溢出
/(ㄒoㄒ)/~~这类视频都是很久以前上传的渣画质,b站总是提示跟其他的视频冲突发不上去,像这个视频http://v.youku.com/v_show/id_XMjc1NDI0Njcy.html,
上面说的报错是MediaStreamSource里面的这个函数output的
protected override void GetDiagnosticAsync(MediaStreamSourceDiagnosticKind diagnosticKind)
{
System.Diagnostics.Debug.WriteLine(diagnosticKind.ToString());
}
并不会导致程序异常终止,但是GetSampleAsync会停止获取,不再执行下去了,videotag和audiotag的解析似乎并没有出现问题,因为如果一个tag长度读取错误,可能会导致后面所有的tag都混乱,但是tag解析完成后,单独只播放音频tag是正常的,所以我觉得可能是这类视频的videotag的解析方式,是不是和Scott Wong大大说的那种有些不同,这个视频只有一个flv格式,我发到115上一个,http://115.com/lb/5lbb1d5l1ihb,如果能帮忙看看感激不尽。
额,刚才激动了没说清楚,是tag读取完成后,都放到那个audioSamples和videoSamples以后,只获取音频样本播放是正常的,但是,获取到的视频sample,在GetSampleAsync里面获取第一个就出现上面的问题了,第一个样本我有吧codecPrivateData写进去,但是仍然报错,不写也是会报错。
好,等我有时间研究一下。不保证能解决你的问题,因为总会有些个别的奇葩封装的视频的……
@chinanoahli:@暗影吉他手 wpbili 如果在线看的话,缓存的视频会无法自动删除释放内存,必须手动删除客户端重新安装,建议已经下载了的视频可以发弹幕,或者下载区和缓存区的数据弄到一起,方便删除或清理缓存?过年这几天看的比较多,一下子没了一g内存才发觉233
清理不了缓存是技术障碍,目前无法解决,就指望WP8.1出现以后能有新的办法了。
楼主在b站工作啊,好羡慕,我这个问题已经搞定了哈哈,是取nalu片段的时候长度搞错了。。
找到这些东西,不知道以后在wp8播放flv有没有用。
http://phonesm.codeplex.com
http://vlcwp7.codeplex.com
http://playerframework.codeplex.com
这些资料都是关于播放ts流的吧,用来看直播的