众所周知,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 static readonly byte[] startCode = new byte[] { 0, 0, 1 };
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原始流,不要假装不认识然后直接报错>< 首先是音频信息:

sample = new FlvTag(this.mediaStream, ref fileOffset);
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;
    }
}

然后是视频信息:

else if (sample.TagType == TagType.Video)
{
    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通知系统解码器。

//Audio
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视频格式的支持

» 转载请注明来源及链接:未来代码研究所

Related Posts:

12 Responses to “实现在Windows Phone Silverlight应用中播放FLV视频(上)”

  • bobby says:

    大大求下文

  • 玉米 says:

    楼主好棒o(≧v≦)o~~,让我燃起动力了!

  • 玉米 says:

    很冒昧打扰,有一个问题想咨询下楼主,在播放优酷早期的视频的时候(最近上传的视频可以正常播放,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%)
    这样的信息,我不太懂这些,是否会对解析产生影响,任何提示,不胜感激!

    • 暗影吉他手 says:

      能不能给一个b站av号做测试?可能是获取的某个length值有误导致分配内存时溢出

      • 玉米 says:

        /(ㄒ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,如果能帮忙看看感激不尽。

        • 玉米 says:

          额,刚才激动了没说清楚,是tag读取完成后,都放到那个audioSamples和videoSamples以后,只获取音频样本播放是正常的,但是,获取到的视频sample,在GetSampleAsync里面获取第一个就出现上面的问题了,第一个样本我有吧codecPrivateData写进去,但是仍然报错,不写也是会报错。

        • 暗影吉他手 says:

          好,等我有时间研究一下。不保证能解决你的问题,因为总会有些个别的奇葩封装的视频的……

  • chinanoahli says:

    @chinanoahli:@暗影吉他手 wpbili 如果在线看的话,缓存的视频会无法自动删除释放内存,必须手动删除客户端重新安装,建议已经下载了的视频可以发弹幕,或者下载区和缓存区的数据弄到一起,方便删除或清理缓存?过年这几天看的比较多,一下子没了一g内存才发觉233

    • 暗影吉他手 says:

      清理不了缓存是技术障碍,目前无法解决,就指望WP8.1出现以后能有新的办法了。

  • 玉米 says:

    楼主在b站工作啊,好羡慕,我这个问题已经搞定了哈哈,是取nalu片段的时候长度搞错了。。

  • wcavell says:

    找到这些东西,不知道以后在wp8播放flv有没有用。
    http://phonesm.codeplex.com
    http://vlcwp7.codeplex.com
    http://playerframework.codeplex.com

Leave a Reply

World Line
Time Machine
Online Tools