基于 DirectShow 实现 SourceFilter 常见问题分析

        很多年前记录了一篇实现 DirectShow SourceFilter 的文章(见 播放器插件实现系列 —— DirectShow 之 SourceFilter),那次只是简单实现验证了一下,并没有大规模上线使用。没想到多年之后,还有机会重拾之前的代码。

        这次功能开发上线还是遇到了一些问题,在这里记录一下,也让大家体会一下程序员的心路历程。

问题一:Windows SDK 不再包含 DirectShow 辅助库 

        之前在 MFC 时代,微软提供了 DirectShow sdk,可以方便我们开发各种 Filter ,但是现在微软已经不维护了,找不到了。

        后来根据网上资料,找到了 github 上 Windows 经典示例

Windows SDK DirectShow Samples adapted for Visual Studio 2019
DirectShow filter built with Visual Studio 2019 Preview to run on Windows Server 2012

问题二:DirectShow 找不到我们的 DLL

        这其实是时间长了,忘了之前怎么做的了。

        这是一个隐藏比较深的知识点,毕竟程序员写代码想要思考,对代码的记忆印象比较深刻;但是对一些容易的配置,通过鼠标点点就能完成的操作,反而容易忘记。

        幸好在之前的文章中,稍微在最后提了一下,才不至于花大力气去翻资料,毕竟 MSDN 啰里八嗦太多内容了。所以这也是我们特别列出这一项来的原因,而之前的文章说得太简单了。

        要让自定义协议被 DirectShow 识别,需要做注册表中注册协议。

[HKEY_CLASSES_ROOT\tutk]
"Source Filter"="{6A881765-07FA-404b-B9B8-6ED429385ECC}"

         如上,就是注册了 tutk 协议,用 DirectShow 播放 tutk:///12345abcde 这样的链接,就会使用我们的 SourceFilter 来获取数据。

问题三:处理 H264 ES(Annex B)流

        H264 或者 AVC 编码有两种传输格式,暂且称为 Packet 格式和 Stream 格式。前者在 Mp4 、MKV、FLV 这样的文件中被经常使用,后者常在 TS 等流式传输中使用。

        针对Packet 格式,一般在文件头部中有 AVCConfigHeader,另外每一帧由一个或者多个 Nalu 组成,每一个 Nalu 前面有 4个(也有可能 2个、3 个,具体在 Header 中指定)字节,表示 Nalu 到长度。

        针对 Stream 格式,一般没有特别的头部,在流中,每一个同步帧(IDR),都会额外包含 SPS、PPS 这些配置信息。同 Packet 格式一样,每一帧也是由一个或者多个 Nalu 组成(SPS、PPS 也分别是一种 Nalu),但是每个 Nalu 前面没有表示长度的数据,而是在前面用 0001 四个特殊字节(或者 001 三个字节)分割 Nalu。

        之前我们尝试过 Packet 格式, 在遇到 Stream 格式,没有继续深入研究。一开始我们把 Stream 格式转换为 Packet 格式,麻烦不说,最大的问题是一开始没有 SPS、PPS,不能正确配置解码器,只能等到第一帧数据。

        通过仔细阅读 MSDN H.264 Video Types,配置 MEDIASUBTYPE_H264 可以支持 Stream 格式。而 Format 类型使用 FORMAT_VideoInfo, FORMAT_VideoInfo2, FORMAT_MPEG2Video, or GUID_NULL 都可以,但是亲测 GUID_NULL 并不能工作。

        所以还是要使用 FORMAT_MPEG2Video,不过 cbSequenceHeader、dwSequenceHeader 可以填空。这样就不需要头部数据流了,Stream 格式的帧数据也不需要转换为 Packet 格式。

问题三:阻塞等待数据的同时响应用户操作

        这个问题需要了解 DirectShow 的线程模型,才能够很好的回答。

        一般每个 PushPin 都有自己的专属线程,在此线程从网络获取数据,并将一帧帧对 Sample 推给后面的解码器处理。同时,主控线程响应用户操作,给 PushPin 的线程发送控制命令。PushPin 线程需要抽空即时出来命令,并给主控现场予以应答。

        如果 PushPin 正在等待网络数据,由于网络卡顿,很长时间没有数据,那么他就没有机会处理并应答控制命令,那么主控线程就会一直在等待命令的应答,导致用户操作被卡住,看起来好像程序无响应。

        所以,PushPin 一般通过非阻塞模式,读取数据。每次读取数据之前,通过 CheckRequest 检查是否有控制命令,即时处理。

问题四:GraphEdit 图像不出来,还有运行时很多异常

        这么多年过去了,GraphEdit 已经不能适应变化了,恐怕 Windows8 之后 GraphEdit 就不能正常工作了。

        使用 GraphEdit 出不来如何图像(一片黑),点击 Filter 查看一下属性状态,一直抛异常。而之前另外一个基于 Direct Show 的播放器 WindowMediaPlayer 也不见了踪影。新的系统 MediaPlayer 怎么也找不到一个能够输入 URL 的地方。

        但是通过 IMediaPlayer 类还是能够播放出图形的。目前很多框架(包括 Qt、wxWidgets),在 Windows 上还是基于 IMediaPlayer 播放。

        后来找到 GraphStudioNext,这个就好用多了,不仅能够正常显示图像,对 Filter 的属性、状态,也展示的很充分,对后续开发带来了不少帮助。

问题五:图像花屏,频繁报告丢帧

        有了图像,但是花的,只有到关键帧,好像是正常的,其他帧就越来越花。

        试了 VLC 播放,也会有轻微的花屏,在日志里面不停的输出帧延迟、的错误信息。

        一直怀疑是数据出了问题,也怀疑是是时间戳的问题。毕竟我们播放的是裸的(raw)H246 流,并没有时间戳信息,按照帧率重构的时间戳,设置进去也没有什么效果。

        是不是网络数据到达延迟,导致解码器认为丢帧,那么我们就按照帧达到的时间,来构建时间戳,不过也没有效果。

        就这样折腾了两天,各种怀疑,各种尝试,就是没有用,就差要放弃 DirectShow 方案了。解码器怎么工作对我们来说是黑盒,不管我们怎么急,只能在外面团团转,都想换个解码器了,但是我们选择 DirectShow,就是想快速简单的把这个 H264 流对接播放起来,再折腾解码器就工作量大了。

        过了一个周末,周一准备最后一搏,再不行就放弃了。

        我想既然时间戳怎么计算都不对,干脆我就不设置时间戳了,死马当活马医。没想到还真的有效,图像显示正常了!

问题六:播放一会儿就卡住了

        没有开心多久,问题又来了。正常播放了一会儿,画面卡住了。

        一开始还以为是数据层的问题,是因为网络没有数据。但是网络只是卡顿一会儿,后面还是有数据的,但是播放器就是卡住了。不过,关键帧是能够显示的,这就导致画面很长时间才变一下。

        查看 Renderer Filter,它从解码器收到的帧也确实很少。应该是解码器把帧丢弃了。需要说明一下,上个问题我们使用的是本地数据,不存在网络延迟。所以当真正有网络延迟时,解码器还是会认为数据已经迟到了,直接丢弃了。

        我想,这应该是我们没有实现帧缓冲导致的问题,一般播放器都需要缓冲一定的数据,才开始播放。但是我们本来就是直播流,缓冲就会加大延迟,而且缓冲实现起来也比较复杂,完全没有必要花精力实现了。

        所以就直接寻找 DirectShow 对实时直播流的配置方案。因为一直觉得是没有时间戳的问题,所以奔着时间同步策略去研究,果然找到了一些蛛丝马迹。大家有兴趣的可以参考这里 Reference Clocks,这里 Live Sources 有另一种方案,看起来很复杂,没有去尝试。

IMediaFilter* pMediaFilter = NULL;
m_pGraph->QueryInterface(IID_IMediaFilter, (void**)&pMediaFilter);
pMediaFilter->SetSyncSource(NULL);

         把同步源设置为 NULL,就是不需要同步了。只要有数据,就会马上显示出来。看到长时间正常的播放,真实太棒了!

问题七:Windows Media Player 播放警告弹框

        这个应该不算是 DirectShow 的问题,而是 IMediaPlayer 一个行为。

IMediaPlayer 是一个更高层的接口类,可以直接播放视频,底层是用 DirectShow 来实现。

        现在 IMediaPlayer 播放这种自定义协议,会弹出一个警告对话框,如下:

        你可以勾选“不再提示”,后面就不会再弹出了。但是最好是不让用户看见这个警告。

        我们觉得,勾选“不再提示”后,Windows Media Player 应该在注册表中记录了用户选择,所以下次不再弹出。既然这样,我们主动写入这个注册表项目不就可以了吗?

        通过在注册表中搜索,还真让我们找到了相关的注册表项:

[HKEY_CURRENT_USER\SOFTWARE\Microsoft\MediaPlayer\Player\Extensions\.]
"Permissions"=dword:00000020

        终于清净了! 

问题八:下半部分花屏

在上线运行一段时间后,突然出现好几例视频图像下半部分花屏的问题。

因为不是所有摄像头都有问题,所以一开始以为是摄像头编码那边有问题,但是把收到的有问题的数据 dump 下来,用 vlc 播放器播放,又是正常的。没有头绪了,只能先放下继续观察规律。

直到有一个,一个本来正常的摄像头,突然不正常出现了下半部分花屏的问题,而唯一的变化就是在它前面放置了比较复杂的景物。去除这个景物,就正常了。用 graphnextstudio 播放,则下半部分完全是黑的。

看到这个,马上就觉得是视频 Buffer 太小了,丢失了一帧的后半部分数据。通过 review 代码,果然有一个 Alloc size 是固定的 48 K。

HRESULT CMyPin::DecideBufferSize(IMemAllocator *pAlloc, ALLOCATOR_PROPERTIES *pRequest)
{
    HRESULT hr;
    CAutoLock cAutoLock(m_pFilter->pStateLock());

    CheckPointer(pAlloc, E_POINTER);
    CheckPointer(pRequest, E_POINTER);

    // Ensure a minimum number of buffers
    if (pRequest->cBuffers == 0)
    {
        pRequest->cBuffers = 20;
    }
    if (m_info->type == StreamType::VIDE)
        pRequest->cbBuffer = 48 * 1024;
    else
        pRequest->cbBuffer = 2 * 1024;

    ALLOCATOR_PROPERTIES Actual;
    hr = pAlloc->SetProperties(pRequest, &Actual);
    if (FAILED(hr)) 
    {
        return hr;
    }

    // Is this allocator unsuitable?
    if (Actual.cbBuffer < pRequest->cbBuffer) 
    {
        return E_FAIL;
    }

    return S_OK;
}


把 48 改大点,比如改为 96,再次播放,果然就 OK 了。

问题九:开始播放卡死

这是这双显卡机器上容易出现的问题,当使用扩展显卡时,D3D9 创建渲染 Surface 会出现异常,并且导致一个 D3D9 的锁没有释放,从而引起主线程卡死。

解决方法是强制应用使用内置集成显卡。

我在这里记录了详细的排查分析过程 一个 DirectShow 播放问题的排查记录_Fighting Horse的博客-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fighting Horse

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值