一个 DirectShow 播放问题的排查记录

播放方案

在 Windows 平台,基于 ActiveMovie 播放。 ActiveMovie 是 Windows Media Player 的一个组件,底层使用了 DirectShow 框架。

问题现象

问题的现象是:一旦点击播放按钮,在加载一会儿之后(加载完成),整个程序就卡死了,UI 上不响应任何输入,播放也没有开始,没有图像出来。(如下图)

不过大部分的用户那儿,该功能还是正常的。但是出现问题的个体数占比也超过了 3%。并且越真实的用户,占比越高,所以不得不硬着头皮解决这个疑难杂症。

经过各种尝试分析,运用了多种方法,虽然因为经验不足,很多方法都走入了死胡同,但是在耗了整整 3 天后,终于抓到了他的尾巴。

这其中的历程和方法,值得记录下来与大家分享。所谓“世上无难事,只怕有心人”,最重要的心得就是不放弃,知难而行。

下面我整理了排除这个问题用到的方法,但是其实在 3 天的排查分析中,我也是到处尝试,碰运气,并没有太多章法。这里是后来分析总结,才有了一些相对清晰的逻辑。希望能够对解决后面遇到的疑难问题有所帮助。

初步分析

既然无响应,那就看看卡住代码哪里了。发现问题时没有开启调试,所以需要在任务管理器里面,导出进程转储文件(xxx.DMP)。在 Visual Studio 里面打开,运行。主线程的栈如下:

主线程卡在 d3d9.dll 的 CLockD3D 中,而且提示等待一个锁,这个锁被另一个线程 6612 持有。那么 6612 线程在搞什么呢?下面是 6612 线程的调用栈:

其实没有在干什么,Nt..Wait..Ex 这个函数是比较底层的,一般在这里只是等待消息,不会有什么任务要处理。

在 Visual Studio 的线程栈里面显示的“[外部代码]”是可以展开的,展开的锁死线程的调用栈如下:(另一次运行的卡死状态)

     [正在等待线程 锁定 拥有的 17368,双击或按 Enter 可切换到线程]        已批注的帧
     ntdll.dll!NtWaitForAlertByThreadId()    未知    非用户代码。已加载符号。
     ntdll.dll!RtlpWaitOnAddressWithTimeout()    未知    非用户代码。已加载符号。
     ntdll.dll!RtlpWaitOnAddress()    未知    非用户代码。已加载符号。
     ntdll.dll!RtlpWaitOnCriticalSection()    未知    非用户代码。已加载符号。
     ntdll.dll!RtlpEnterCriticalSectionContended()    未知    非用户代码。已加载符号。
     ntdll.dll!RtlEnterCriticalSection()    未知    非用户代码。已加载符号。
     d3d9.dll!CLockD3D::CLockD3D(class CLockOwner *,int)    未知    非用户代码。已加载符号。
     d3d9.dll!CDxva2Container::GetVideoProcessorDeviceGuidCount(struct _DXVA2_VideoDesc const *,unsigned int *)    未知    非用户代码。已加载符号。
     dxva2.dll!CVideoAccelerationService::GetVideoProcessorDeviceGuids()    未知    非用户代码。已加载符号。
     evr.dll!CMFVideoMixer9::AllocateResources()    未知    非用户代码。已加载符号。
     evr.dll!CMFVideoMixer::MFTProcessMessage()    未知    非用户代码。已加载符号。
     evr.dll!CMFVideoMixer9::MFTProcessMessage(enum _MFT_MESSAGE_TYPE,unsigned __int64)    未知    非用户代码。已加载符号。
     evr.dll!CEVRFilter::ProcessMessageMixer(enum _MFT_MESSAGE_TYPE,unsigned __int64)    未知    非用户代码。已加载符号。
     evr.dll!CEVRFilter::StartStreaming(void)    未知    非用户代码。已加载符号。
     evr.dll!CEVRFilter::Pause(void)    未知    非用户代码。已加载符号。
     quartz.dll!CFilterGraph::Pause(void)    未知    非用户代码。已加载符号。
     quartz.dll!CFGControl::Cue(void)    未知    非用户代码。已加载符号。
     quartz.dll!CFGControl::CueThenRun(void)    未知    非用户代码。已加载符号。
     quartz.dll!CFGControl::CImplMediaControl::StepRun(void)    未知    非用户代码。已加载符号。
     quartz.dll!CFGControl::CImplMediaControl::Run(void)    未知    非用户代码。已加载符号。
     wmp.dll!00007ff8f390c3b3()    未知    非用户代码。无法查找或打开 PDB 文件。
     wmp.dll!00007ff8f390d9d0()    未知    非用户代码。无法查找或打开 PDB 文件。
     wmp.dll!00007ff8f37a32a3()    未知    非用户代码。无法查找或打开 PDB 文件。
     wmp.dll!00007ff8f37a2834()    未知    非用户代码。无法查找或打开 PDB 文件。
     wmp.dll!00007ff8f37a27ae()    未知    非用户代码。无法查找或打开 PDB 文件。
     wmp.dll!00007ff8f37a2568()    未知    非用户代码。无法查找或打开 PDB 文件。
     wmp.dll!00007ff8f372e327()    未知    非用户代码。无法查找或打开 PDB 文件。
     wmp.dll!00007ff8f372e70f()    未知    非用户代码。无法查找或打开 PDB 文件。
     wmp.dll!00007ff8f372e83c()    未知    非用户代码。无法查找或打开 PDB 文件。
     wmp.dll!00007ff8f37c6d3f()    未知    非用户代码。无法查找或打开 PDB 文件。
     wmp.dll!00007ff8f37c6bb8()    未知    非用户代码。无法查找或打开 PDB 文件。
     wmp.dll!00007ff8f37c6c9f()    未知    非用户代码。无法查找或打开 PDB 文件。
     wmp.dll!00007ff8f3908388()    未知    非用户代码。无法查找或打开 PDB 文件。
     wmp.dll!00007ff8f390861c()    未知    非用户代码。无法查找或打开 PDB 文件。
     wmp.dll!00007ff8f390d3c2()    未知    非用户代码。无法查找或打开 PDB 文件。
     wmp.dll!00007ff8f390d74e()    未知    非用户代码。无法查找或打开 PDB 文件。
     wmp.dll!00007ff8f390d0c4()    未知    非用户代码。无法查找或打开 PDB 文件。
     user32.dll!UserCallWinProcCheckWow()    未知    非用户代码。已加载符号。

调试分析

仅通过 DMP 分析,基本上看不出为什么卡死。好在我们也有一个 PC 有同样的现象。所以可以用实时调试来进一步探寻到底发生了什么。

在用 Visual Studio 实时调试时,会遇到一个异常,继续运行3次后,就到了卡死的状态了。异常栈是这样的:

>   d3d9.dll!CSwapChain::CreateWindowed(unsigned int,unsigned int,enum _D3DFORMAT,unsigned int,enum _D3DMULTISAMPLE_TYPE,unsigned long,int,int,int,int,int,int) 未知
    d3d9.dll!CSwapChain::Reset(struct D3DPRESENT_PARAMETERS *,struct D3DDISPLAYMODEEX const *,int,int)    未知
    d3d9.dll!CSwapChain::Init(struct D3DPRESENT_PARAMETERS *,struct D3DDISPLAYMODEEX const *,long *,int,int)  未知
    d3d9.dll!CBaseDevice::CreateAdditionalSwapChain(struct D3DPRESENT_PARAMETERS *,struct IDirect3DSwapChain9 * *)    未知
    evr.dll!CMFVideoPresenter9FlipMode::CreateFlipModeSwapChain()   未知
    evr.dll!CMFVideoPresenter9FlipMode::ProcessOutput() 未知
    evr.dll!CMFVideoPresenter::RepaintVideo()   未知
    rpcrt4.dll!Invoke()    未知
    rpcrt4.dll!Ndr64StubWorker(void *,void *,struct RPC_MESSAGE *,struct _MIDL_SERVER_INFO *,long (*const *)(void),struct _MIDL_SYNTAX_INFO *,unsigned long *)    未知
    rpcrt4.dll!NdrStubCall3()   未知
    combase.dll!CStdStubBuffer_Invoke(IRpcStubBuffer * This, tagRPCOLEMESSAGE * prpcmsg, IRpcChannelBuffer * pRpcChannelBuffer) 行 1458  C++
    rpcrt4.dll!CStdStubBuffer_Invoke()  未知
    [内联框架] combase.dll!InvokeStubWithExceptionPolicyAndTracing::__l6::<lambda_c9f3956a20c9da92a64affc24fdd69ec>::operator()() 行 1279    C++

异常信息提示内存访问异常,内存指针是一个很小的值,应该是 null 指针导致的。在 d3d9 这样重要的库中,竟然不处理 null 指针,真是不应该。但是对我们排查问题来说,这反而能够让我们观察到更多的细节信息,是好事情。能够看到这么细节的调用栈,不得不佩服微软的调试框架,以及 pdb 符号的完备性。

根据 D3D 的资料,CreateAdditionalSwapChain 好像是在创建 D3D 设备的交换 Buffer,在图像显示上使用。可是即使知晓原理,但是没有源代码,无法知道是哪一行代码有问题,更不清楚如何规避该问题。

对比分析

不同设备的对比

首先想到的手段是对比正常设备、异常设备的不同。我怀疑正常设备使用了不同的方式,所以程序没有运行到 d3d9 在块有问题的代码上。

我在逻辑先后次序上,在“渲染管道”、“调用栈”、“调用参数”上分别进行了对比,最后发现没有任何不同。虽然对解决问题来说全是无用功,但是在方法上还是值得记录学习的。

渲染管道

渲染管道就是视频播放所用到的各个组件的连接(一般是管道链结构,在 DirectShow 中称为 Filter Graph, Filter 就是组件,Graph 是连接图)。一般情况下,管道包含至少三个组件:源,解码器、渲染器)。要知道 DirectShow 使用了哪些组件,需要遍历 IFilterGraph 里面的所有 Filter,输出其信息,Filter 上还要多个 Pin,也要输出 Pin 的信息,Filter 通过 Pin 相互连接。

因为三个组件中的源组件是我们自己实现的,所以在源组件中插入一些代码,就得到了下面的信息:(对比正常情况是一样的)

DumpGraph [40bf3e10]
    Filter [0000028940BF4148]  'Enhanced Video Renderer'
          Pin [0000028940BF4478]  'EVR Input0' [Input]  Connected to pin [000002893FED0B08]
    Filter [0000028940B81E68]  'Microsoft DTV-DVD Video Decoder'
          Pin [0000028940B85688]  'Video Input' [Input]  Connected to pin [0000028942A586E8]
          Pin [40bf1fd8]  'Subpicture Input' [Input]
          Pin [000002893FED0B08]  'Video Output 1' [Output]  Connected to pin [0000028940BF4478]
Connection type  M type MEDIATYPE_Video  S type MEDIASUBTYPE_NV12
854x480x12 bit 'NV12'
Image size 616320
          Pin [3fed1198]  '~Line21 Output' [Output]
    Filter [0000028942AB8B00]  'bambu://1243543565748568'
          Pin [0000028942A586E8]  'Bambu Output' [Output]  Connected to pin [0000028940B85688]
Connection type  M type MEDIATYPE_Video  S type MEDIASUBTYPE_H264
     Format type FORMAT_MPEG2_VIDEO

调用栈

我们知道了异常栈,能不能在正常设备上也重现出相同的调用栈呢?

怎么重现,那当然是在同样的地方加上断点,看看能不能断下来就行了。可是我们没有源代码,不过 Visual Studio 提供了直接输入函数名称来设置断点的功能。

不过在 CSwapChain::CreateWindowed(...) 并不能设置断点,在调用栈上一级的 CSwapChain::Rest(...) 是可以的。原因不太清楚,可能是只有对模块外面可见的方法,才能设置断点。需要说明的是,输入方法名时不需要带参数信息。

在 Reset 断点暂停后,再经过很多次单步执行,可以进入 CreateWindowed 方法,此时可以在汇编代码中加上 CreateWindowed 的断点。然后再次运行,直接在 CreateWindowed 中断点暂停。

所以可以证明,正常情况的调用栈,与异常情况没有区别。

调用参数

既然调用栈是正常的,我就只能怀疑是输入参数的不同导致异常问题了。

通过对栈内存的分析,可以推断出传给 CreateAdditionalSwapChain 的参数。虽然 Visial Studio 不支持脱离源码的调用参数分析,但是我们也有一个笨而聪明的办法。在 x86_64 体系上,栈指针寄存器 rsp 指示调用栈帧的位置,在相同的调用栈上及指令位置,参数内存相当于 rsp 的偏移量是固定的。

那么这个偏移量是多少呢?我们可以通过已知的参数值来反推。先构造一个相同方式对调用(调用参数已知),在相同的指令位置断点暂停,然后在 rsp 向后的内存中找到那个已知的参数值,就知道偏移量了。

最终推断出传给 CreateAdditionalSwapChain 的参数是:

不同程序的对比

处理不同设备环境的对比,在问题设备上,也可以通过不同的程序的表现对比排查问题。

GraphStudioNext

要排查、验证 DirectShow 的播放问题,GraphStudioNext 是最常用的工具(没有之一)。

通过 GraphStudioNext 播放我们的源,在问题设备上是可以播放的。

虽然可以播放,但是在图像渲染方案上,有所不同。 GraphStudioNext 使用的是 VMR (Video Mixing Renderer),而我们的程序(也就是 Windwos Media Player)使用的是 EVR (Enhanced Video Renderer),EVR 是比 VMR 更新的技术方案。

在 GraphStudioNext 中,这不是问题。为了对比验证 EVR,只要删除 VMR,添加 EVR 就行了。可以验证,即使使用 EVR ,在问题设备上,GraphStudioNext 也是可以播放的。

简单程序

但是 GraphStudioNext 毕竟与 Windwos Media Player (WMP)还是有区别的,不排除问题是由 WMP 引起的。

与其一步步排查,干脆直接将我们程序里面的播放部分的代码独立出来,构建一个简单的程序,看看有什么不同的表现。

虽然中间有些反复,一开始不能播放,以至一度放弃这个思路。但是后来又确认了简单程序播放是正常的,这也促成了后面的排除法分析。

排除法分析

既然简单程序能够播放,自然就怀疑我们程序的其他功能影响了播放功能,因此排除法就这么蹦出来了。

排除法是解决很多疑难问题的终极武器,前提是需要有一个正常对照。但是排除法是很累的,要一步步去除部分功能,还要让整个程序能够跑起来。而且随着一次又一次的失败,信心越来越少,很考验忍耐力。

我逐步排除了下列功能影响,没想到问题仍然能够复现。

  1. OpenGL 功能(直接跳过 OpenGL 的初始化)
  2. WebView 功能(使用假的实现代替)
  3. 底层网络模块
  4. 无关界面(各个 Tab 页)
  5. 主界面(已经完全替换为简单程序的界面)
  6. 底层算法相关的功能库
  7. 程序入口初始化的大部分代码

接下来怀疑的是通过运行时加载 DLL的方式了。我们主要功能在 DLL 中实现,启动程序只是一个外壳。

我将简单程序的功能移动到启动程序中直接实现,增加了一些链接配置,跑起来了。但是还是有一样的异常。

没办法,既然走上了一条不归路,也只能硬着头皮继续下去。接下来只能怀疑编译配置了,将编译配置拿出来对比。不一样的也就是链接库和预编译宏了。弄成一样,竟然真的正常了,真实见鬼,这还是第一次遇到预编译宏引起的问题。

将怀疑的预编译宏逐个再加回来,定位到 SLIC3R_GUI 这个宏是罪魁祸首。进一步确认后,再看看它到底影响了哪些代码。

其中一段代码非常值得怀疑:

#ifdef SLIC3R_GUI
extern "C"
{
     // Let the NVIDIA and AMD know we want to use their graphics card
     // on a dual graphics card system.
     __declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001;     
     __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
}
#endif /* SLIC3R_GUI */

去掉这块代码果然 OK。

哈哈,终于找到问题根源了。看看这两个东东到底是什么鬼。

果然还是我经验不够,这两个导出符号是做游戏的开发经常会用到的,可以在有独立显卡的设备上,加速游戏运算,提升性能。

检查了一下问题设备的硬件配置,果然有了图形适配器(显卡),一个集成的,一个独立的。而我们没有出问题的设备,都是只有一个集成的显卡。使用我们程序的用户,他的设备,大都也会用比较好的配置,所以也从侧面说明了真实用户出问题比例更高的原因。

关于上面两个配置,大家有兴趣可以看看相关资料:

NVIDIA Optimus
Selecting the Best Graphics Device to Run a 3D Intensive Application

  • 1
    点赞
  • 1
    收藏
  • 打赏
    打赏
  • 1
    评论
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论 1

打赏作者

Fighting Horse

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

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值