ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CEF win64+VS2019:H.264/H.265硬解接入与发布排查

CEF win64+VS2019:H.264/H.265硬解接入与发布排查

简介:面向 Windows 64 位应用开发者的 CEF 5563 编译版本,基于 Visual Studio 2019 构建,在原生 Chromium 内核基础上补齐了 H264/H265 视频编解码支持,适合在线教育、直播、视频会议等对播放性能要求较高的桌面客户端嵌入场景,也适用于需要在 C++ 工程中集成浏览器控件的项目。压缩包共 1055 个文件、约 278.47MB,以 539 个 C++ 头文件与 359 个 cc 源文件为主体,配合 58 个 pak 界面资源包、14 个 dll 运行库及 lib、bin 等二进制依赖,另含 CMakeLists 与 gypi 路径配置、README 说明、Doxyfile 文档配置和 tests 测试用例,目录层次清晰,便于按模块定位。已有 486 人学习下载。开发者可直接获得可编译、可调试的完整工程结构,通过 CMake 调整编译选项完成定制集成,并借助现成测试代码验证功能正确性,减少从零拉取与配置 Chromium 源码的时间成本。

1. CEF win64 带 H.264/H.265:先分清「编进去了」和「这台机器能解」两件事

搜 CEF vs2019 H264/265 的人,大多会卡在同一个地方:同一段<video>代码,在别人机器上放得好好的,搬到这套 CEF win64 包里就是黑屏、只有声音,或者控制台只给一句「不支持的源」。这不是你的前端写法有问题,而是两件事被混在了一起——编解码器有没有被编进产物,和这台机器能不能把它解得动。

这份资源是 Windows x64 下、用 VS2019 工具链编出来的 CEF 发行包,打包时把私有编解码开关打开了,H.264 和 H.265 的解码通路是通的。适合正在做桌面客户端内嵌网页、又要放监控流或本地 MP4 的团队;不适合只想套个壳跑静态页的人,那用不带编解码的普通包更省体积。接下来的顺序是:先把它接进工程跑起来,再验解码,最后讲发布和排查。

2. 用 VS2019 接住这份 win64 包:目录、四个工程属性与最小可跑骨架

CEF 的接入工作一半在写代码之前。发行包解压出来是一堆 dll、pak、dat 和一个 include 目录,很多人上来就Add Reference,然后被链接错误和启动崩溃轮流教育。这一章先把「哪些文件进工程、哪些文件进发布目录」分开,再把最小骨架跑通。

2.1 发行包里哪些文件是运行时必需,哪些只进链接期

先用二分法把包里的东西分成两堆:链接期用的(进工程配置、不拷进发布目录)和运行期用的(跟 exe 放在一起)。搞混这两堆,最常见的后果是本地调试一切正常、拷贝到测试机就白屏。

文件阶段说明
include/ 目录链接期头文件,只加到「附加包含目录」
libcef.lib链接期导入库,不要拷进发布目录
cef_sandbox.lib链接期开沙箱时才需要,静态库
libcef.dll运行期主体,体积最大,必须与 exe 同位数
chrome_elf.dll运行期缺了会在进程启动阶段直接退出,几乎没有日志
cef.pak / cef_100_percent.pak / cef_200_percent.pak运行期内置 UI 资源,缺了控件样式全丢
resources.pak运行期通用资源
icudtl.dat运行期国际化数据,缺了字符串处理异常
v8_context_snapshot.bin运行期V8 快照,缺了 JS 起不来;老一点的包里叫 natives_blob.bin / snapshot_blob.bin
locales/*.pak运行期至少保留一个,通常 zh-CN.pak + en-US.pak
libEGL.dll / libGLESv2.dll / d3dcompiler_47.dll运行期ANGLE 渲染链路,做硬解时更不能缺

提示:不同批次打出来的包,文件清单会略有出入(比如有没有 vk_swiftshader.dll、有没有 snapshot_blob.bin)。以你手上这份的实际内容为准,别照抄某篇几年前的清单去删文件。

判断标准很简单:include和两个.lib之外的东西,基本都要跟着 exe 走。

2.2 四个必须改的工程属性:x64、/MD、C++17、子系统

新建一个空的 C++ 工程,然后在项目属性里把这四项改掉,顺序别乱:

  1. 平台改成x64。CEF win64 包里的 libcef.dll 是 64 位,工程留在 Win32 上,链接期就会报一堆LNK2038或模块计算机类型冲突。
  2. C/C++ → 代码生成 → 运行库,Release 用「多线程 DLL (/MD)」,Debug 用「多线程调试 DLL (/MDd)」。libcef.lib 是按动态运行库编的,你这边切成 /MT,会在CefInitialize里以莫名其妙的方式崩。
  3. C/C++ → 语言 → C++ 语言标准,选/std:c++17。CEF 头文件里用了不少 C++14/17 特性,太老的标准会直接在编译期报错。
  4. 链接器 → 系统 → 子系统,调试阶段可以先留控制台,方便看--enable-logging的输出;正式发布改成 Windows。

另外两处容易漏:附加包含目录填到包根目录(因为代码里写的是#include "include/cef_app.h"),附加依赖项加libcef.lib,需要沙箱再补cef_sandbox.lib。

如果你用 CMake 管工程,等价配置大致是这样:

cmake_minimum_required(VERSION 3.16) project(cef_shell CXX) set(CEF_ROOT "${CMAKE_SOURCE_DIR}/third_party/cef" CACHE PATH "CEF 发行包根目录") set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 关键:与 libcef.lib 的 /MD 对齐,否则运行期会在初始化阶段崩 set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL") add_executable(cef_shell WIN32 src/main.cpp) # WIN32 对应 wWinMain target_include_directories(cef_shell PRIVATE "${CEF_ROOT}") target_link_libraries(cef_shell PRIVATE "${CEF_ROOT}/libcef.lib" "${CEF_ROOT}/cef_sandbox.lib" comctl32 shlwapi user32) # 调试时把运行期文件拷到输出目录,省得每次手动复制 add_custom_command(TARGET cef_shell POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${CEF_ROOT}/Release" "$<TARGET_FILE_DIR:cef_shell>" COMMAND ${CMAKE_COMMAND} -E copy_directory "${CEF_ROOT}/Resources" "$<TARGET_FILE_DIR:cef_shell>")

CMAKE_MSVC_RUNTIME_LIBRARY这一行是整段里最值钱的一行,它替代了手工去戳每个 target 的编译选项;copy_directory那两条只是让调试期少一次手工劳动,正式发布还得走 4.3 的清单校验,别指望它会替你删掉多余文件。

2.3 主程序骨架:三段式初始化与子进程分叉

CEF 的进程模型是「一个 exe、两种身份」。浏览器进程和渲染/GPU 子进程跑的是同一个可执行文件,靠CefExecuteProcess的返回值区分。这一步写错,表现是任务管理器里看不到子进程、页面一片空白。

#include <windows.h> #include "include/cef_app.h" #include "include/cef_browser.h" #include "include/cef_command_line.h" class SimpleApp : public CefApp, public CefBrowserProcessHandler { public: CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler() override { return this; } void OnBeforeCommandLineProcessing(const CefString& process_type, CefRefPtr<CefCommandLine> cmd) override { if (!process_type.empty()) return; // 只改浏览器进程,子进程沿用父进程的命令行 if (!cmd->HasSwitch("autoplay-policy")) cmd->AppendSwitchWithValue("autoplay-policy", "no-user-gesture-required"); // 窗口被遮挡时不要降频,否则监控画面会「看起来卡住了」 if (!cmd->HasSwitch("disable-features")) cmd->AppendSwitchWithValue("disable-features", "CalculateNativeWinOcclusion"); cmd->AppendSwitch("enable-logging"); cmd->AppendSwitchWithValue("v", "1"); } IMPLEMENT_REFCOUNTING(SimpleApp); }; int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int) { CefMainArgs main_args(hInstance); CefRefPtr<SimpleApp> app(new SimpleApp()); // 第一段:子进程在这里直接 return,永远不会走到下面的 CefInitialize int exit_code = CefExecuteProcess(main_args, app.get(), nullptr); if (exit_code >= 0) return exit_code; CefSettings settings; settings.no_sandbox = false; settings.windowless_rendering_enabled = false; settings.log_severity = LOGSEVERITY_INFO; // 缓存目录必须可写,不能指向安装目录 CefString(&settings.cache_path) = L"C:\\Users\\Public\\cef_shell_cache"; CefString(&settings.locale) = "zh-CN"; if (!CefInitialize(main_args, settings, app.get(), nullptr)) return 1; // 初始化失败,先去看 debug.log CefRunMessageLoop(); // 第二段:消息循环 CefShutdown(); // 第三段:必须成对出现 return 0; }

三个参数值得单独说:no_sandbox = false表示启用沙箱,前提是链接了cef_sandbox.lib且包里有对应头文件;cache_path一定要指向有写权限的位置,装在Program Files下还用它当缓存目录是新手高频翻车点;locale只是期望值,实际能不能生效取决于locales目录里有没有对应的 pak。

OnBeforeCommandLineProcessing里那个process_type判断不要省。省了之后你会发现自己的开关被子进程重复追加,某些开关重复出现会互相覆盖,排查起来很费时间。

2.4 第一次跑起来的验证动作

骨架跑通不等于接入成功。按这个顺序验一遍,能提前拦住大部分问题:

  1. 启动后看任务管理器,进程数应该大于 1(浏览器进程 + 至少一个子进程)。只有一个,说明CefExecuteProcess没被正确调用或提前返回。
  2. 用--enable-logging --v=1起的进程会在缓存目录旁边生成日志,先确认里面有CefInitialize成功的痕迹,而不是一堆Failed to load resource。
  3. 加载一个data:text/html,<h1>ok</h1>,能渲染说明资源文件齐了;白屏但进程正常,八成是 pak 或 locales 没拷。
  4. 最后再上file://或本地 http 服务,加载一个 MP4 页面,这一条留到第 3 章验。

3. H.264/H.265 解码链路:GN 开关、硬解回落与三行探测代码

「包里带了 H.264/H.265」这句话,业内其实包含两层意思。一层是产物里有没有对应的解码器实现,另一层是运行这台机器时,解码请求最终落到 GPU 还是 CPU。两层都通了,视频才既播得出来又不卡。

3.1 proprietary_codecs 与 ffmpeg_branding 决定有没有,GPU 决定快不快

上游 Chromium 默认构建是「干净」的:proprietary_codecs = false、ffmpeg_branding = "Chromium",产出的媒体组件里不带 AVC/HEVC 相关实现。要出带编解码的包,打包方得把这两个 GN 参数改掉重新编——proprietary_codecs = true加上ffmpeg_branding = "Chrome",媒体模块才会包含 H.264、H.265 那部分。

这条链路在运行期大致是三段:

  • 解封装:从 MP4/MKV 里把视频轨拆出来,认得avc1、hvc1、hev1这些 codec 标签。
  • 解码:优先走平台硬件解码(Windows 上是 D3D11VA / Media Foundation 那条路),拿不到再回落到软件解码。
  • 渲染:解码出的帧交给 ANGLE/D3D11 上屏。

三段的失败表现完全不同。解封装不认标签,<video>直接触发error事件、video.error.code是 4(不支持的源);解码器缺失,表现为canPlayType返回空串;只有解码没有硬解,则是能播但 CPU 打满、4K 掉帧。

注意:H.265 比 H.264 更容易「挑机器」。它的硬件解码通路依赖系统侧提供 HEVC 解码能力,消费级系统默认不一定带这块组件;有些机器上 H.264 硬解正常,H.265 却掉回软解甚至完全不可解,属于预期内现象,不是包坏了。

所以拿到任何一份号称「支持 H.264/265」的包,第一件事不是写播放器,而是探测。别假设,先测。

3.2 mediaCapabilities 探测:supported 不等于 powerEfficient

最省事的验证方式是在页面里跑一段能力查询,不用装任何插件。navigator.mediaCapabilities.decodingInfo()返回三个字段,含义差异很大:supported是能不能解,smooth是会不会掉帧,powerEfficient才是「是不是走了硬解」。

// 三档典型配置:1080p H.264、4K H.265、720p H.264 低码率 const cases = [ { name: 'H.264 1080p30', cfg: { type: 'file', video: { contentType: 'video/mp4; codecs="avc1.640028"', width: 1920, height: 1080, bitrate: 4_000_000, framerate: 30 } } }, { name: 'HEVC 4K60', cfg: { type: 'file', video: { contentType: 'video/mp4; codecs="hvc1.1.6.L150.90"', width: 3840, height: 2160, bitrate: 20_000_000, framerate: 60 } } }, ]; (async () => { for (const c of cases) { const r = await navigator.mediaCapabilities.decodingInfo(c.cfg); console.log( c.name.padEnd(14), 'supported=' + r.supported, 'smooth=' + r.smooth, 'powerEfficient=' + r.powerEfficient ); } })();

读结果的顺序是:supported=false就别往下查了,包或系统根本不认这个 codec;supported=true但powerEfficient=false,说明在软解,1080p 还能扛,4K 一定卡;三个都 true 才算真正落地。

参数上有两个坑。一是contentType必须带引号,codecs="avc1.640028"少一个引号就从「支持」变成「不支持」,这种低级错误每年都有人踩。二是hvc1和hev1是两个不同的标签,前者表示参数集放在 samples 里、后者表示可以放在带宽内,有的设备只认其中一种,探测时两种都试一遍更稳妥。

再补一个canPlayType,它更粗糙但启动成本最低:

const v = document.createElement('video'); console.log('AVC :', v.canPlayType('video/mp4; codecs="avc1.42E01E"')); // probably / maybe / "" console.log('HEVC:', v.canPlayType('video/mp4; codecs="hvc1.1.6.L93.B0"'));

返回"probably"表示有把握解,"maybe"表示可能解,空串表示不支持。它只看解码器在不在,完全不反映硬解,所以在 3.2 的第一段代码面前只能当快速筛查用。

3.3 命令行开关的注入顺序,以及不要随手关掉的 GPU

CEF 里改命令行只有一个正确入口:CefApp::OnBeforeCommandLineProcessing。写在这个回调外面的开关,可能被子进程继承得乱七八糟,或者在CefInitialize之后才生效,等于没写。

顺序上有三条硬规则。第一,process_type非空直接返回,理由在 2.3 说过。第二,追加前先HasSwitch判断,同一个 key 重复追加,后写的会覆盖先写的,你以为是叠加、其实是替换。第三,不要在代码里写死--disable-gpu。

第三条值得展开。--disable-gpu是很多人遇到白屏时的第一反应,加上之后页面确实出来了,于是这个开关就留在了代码里。代价是硬件解码通路被一并掐掉,所有视频回落到软件解码,1080p 的机器风扇开始转,4K 直接变成幻灯片。如果确实怀疑 GPU 侧有问题,正确做法是先用--disable-gpu-compositing这类更窄的开关定位,而不是一上来关整个 GPU 进程。

如果你要显式打开某些媒体特性,常见写法是这样:

// 只在浏览器进程注入,且不覆盖外部已经传入的同名开关 if (!cmd->HasSwitch("enable-features")) { cmd->AppendSwitchWithValue("enable-features", "PlatformHEVCDecoderSupport"); }

这里必须说清楚:PlatformHEVCDecoderSupport这类特性名会随上游版本变化,有的版本默认开、有的版本改名、有的版本压根不需要。所以在你的包上,判断依据永远是 3.2 测出来的powerEfficient,不是某个开关名。先测,再决定加不加;加了没变化,说明它本来就是这个状态,别硬留着当心理安慰。

4. 多进程与资源目录:打包发布时最容易漏的 6 个文件

本地调试跑通只是及格线,真正让人半夜爬起来的是发布环节。CEF 的目录结构敏感度比普通桌面程序高一个量级——exe 旁边少一个 dat 文件,用户看到的就是白屏,而且不给任何提示。

4.1 CefExecuteProcess 的分叉点与子进程路径

有两种组织方式,选一种,别混着用。

第一种是单 exe:不设browser_subprocess_path,子进程复用当前可执行文件。优点是发布简单,缺点是这个 exe 里同时塞了 UI 和浏览器逻辑,启动参数校验要做好,否则用户双击带参数的快捷方式会触发意外行为。

第二种是双 exe:单独编一个极简的子进程程序,在设置里指定绝对路径。

// 子进程路径必须是绝对路径,且与主程序位数一致 CefString(&settings.browser_subprocess_path) = L"C:\\Program Files\\YourApp\\cef_sub.exe";

第二种更干净,但有两个约束。一是路径必须绝对,相对路径在不同工作目录下会解析失败;二是这个子进程 exe 不要做任何业务初始化,它的职责就是CefExecuteProcess加return。把主程序逻辑复制一份进去,是常见的错误做法,会导致子进程里又起一个浏览器进程,进程数指数级增长。

multi_threaded_message_loop和external_message_loop这两个开关也别随手开。前者要求你把CefInitialize放在主线程之外,后者要求你自己驱动消息循环。开了却没配套改,表现是界面卡死或者CefRunMessageLoop立刻返回。

4.2 resources_dir_path 与 locales_dir_path 的区别

这两个字段名字很像,作用完全不同,而且都不影响编译,只在运行期爆。

  • resources_dir_path:指向存放*.pak的目录,默认是与 exe 同级。它指向的是目录,不是某个 pak 文件。
  • locales_dir_path:指向locales子目录,里面放语言 pak。

默认情况下两个都留空,CEF 会去 exe 同级找。一旦你把资源挪到别处(比如做绿色版放在resources/下),必须两个都设,只设一个的结果是界面出现一堆IDS_开头的原始字符串,或者语言切换完全无效。

// 绿色版布局:资源统一放进 resources 子目录 CefString(&settings.resources_dir_path) = app_dir + L"\\resources"; CefString(&settings.locales_dir_path) = app_dir + L"\\resources\\locales"; CefString(&settings.locale) = "zh-CN"; // 对应 locales/zh-CN.pak

顺带说一句缓存。cache_path指向安装目录在管理员权限下也许能跑,普通用户下必然失败,表现是页面能开但登录态丢失、localStorage 写不进。放到%LOCALAPPDATA%下就没事了,这不是玄学,就是权限问题。

4.3 一份发布目录树与自动校验脚本

一个能用的发布目录长这样:

YourApp/ ├─ YourApp.exe ├─ libcef.dll ├─ chrome_elf.dll ├─ icudtl.dat ├─ v8_context_snapshot.bin ├─ resources.pak ├─ cef.pak ├─ cef_100_percent.pak ├─ cef_200_percent.pak ├─ libEGL.dll ├─ libGLESv2.dll ├─ d3dcompiler_47.dll ├─ locales/ │ ├─ zh-CN.pak │ └─ en-US.pak └─ resources/ # 仅当改了 resources_dir_path 才需要

每次出包前跑一遍校验脚本,比人工对着清单数文件可靠得多:

import os, sys REQUIRED = [ "libcef.dll", "chrome_elf.dll", "icudtl.dat", "resources.pak", "cef.pak", "cef_100_percent.pak", "cef_200_percent.pak", "libEGL.dll", "libGLESv2.dll", "d3dcompiler_47.dll", ] # 快照文件新老版本命名不同,二选一即可 SNAPSHOT = ["v8_context_snapshot.bin", "natives_blob.bin", "snapshot_blob.bin"] def check(root): missing = [f for f in REQUIRED if not os.path.isfile(os.path.join(root, f))] if not any(os.path.isfile(os.path.join(root, f)) for f in SNAPSHOT): missing.append("v8_context_snapshot.bin(或等价的快照文件)") loc = os.path.join(root, "locales") paks = [f for f in os.listdir(loc) if f.endswith(".pak")] if os.path.isdir(loc) else [] if not paks: missing.append("locales/*.pak") total = sum( os.path.getsize(os.path.join(dp, f)) for dp, _, fs in os.walk(root) for f in fs ) print(f"体积: {total / 1024 / 1024:.1f} MB") print(f"语言包: {len(paks)} 个") if missing: print("缺失:", *missing, sep="\n - ") return 1 print("校验通过") return 0 sys.exit(check(sys.argv[1] if len(sys.argv) > 1 else "."))

脚本里的二选一逻辑是特意留的:不同批次的包对快照文件命名不一样,硬编码成某一个名字,换包就误报。体积那一行也不是装饰,某次打出来的包只有 20 MB,一看就知道 libcef.dll 被压缩工具替换成了占位文件。

5. 排查清单:启动即崩、只有声音、HEVC 挑机器这五类怎么定位

线上反馈永远是模糊的:「打不开」「没画面」「卡」。下面这五类是我在这类项目里反复遇到的,按「现象 → 原因 → 解决」写,遇到新问题可以先往上套。

5.1 双击就退出,事件查看器里是 0xc000007b 或直接无记录

现象:进程一闪而过,没有任何窗口,日志目录也是空的;有的机器提示「应用程序无法正常启动」。

原因:九成是位数或依赖 dll 不匹配。工程切了 x64 但发布目录里混进了 32 位的 libEGL.dll / libGLESv2.dll,或者打包工具把 GPU 相关的几个 dll 当成无用文件裁掉了。chrome_elf.dll缺失时表现更极端——进程在极早期就退出,连日志都来不及写。

解决:先用依赖检查工具确认 exe 是 x64、每个被加载的 dll 也是 x64;再用 4.3 的脚本对一遍清单。注意 x86 和 x64 的 dll 长得一样,别靠文件名判断。

5.2 本机 Debug 能跑,Release 一启动就崩在 CefInitialize

现象:Debug 配置正常,切到 Release 后崩在初始化内部,调用栈指向 libcef.dll,符号对不上。

原因:运行库配置和 libcef.lib 不匹配。常见组合是工程用了/MT,或者 Debug 配置链接了 Release 版 libcef.lib。CEF 的二进制发行包是按动态运行库编的,/MT会在进程内造出两份运行库实例,字符串和容器跨边界传递时直接崩。

解决:把 Release 的「运行库」改回/MD、Debug 改回/MDd,并确认 Debug 配置指向的是 Debug 版 lib,Release 指向 Release 版。改完记得全量重编,别只重编一个文件——编译缓存会骗人。

5.3 视频只有声音没有画面,或者 error.code 是 4

现象:<video>的play()不报错,音频正常,画面区域一片黑;或者干脆触发error事件,video.error.code等于 4。

原因:两种情况要分开看。error.code = 4是「不支持的源」,通常意味着包里的媒体组件没带这个 codec,或者codecs参数字符串写错(avc1写成avc、hvc1和实际文件里的hev1不一致)。只有声音没有画面,往往是解码成功但渲染侧出问题,比如 GPU 进程崩了之后没有正常回落。

解决:先跑 3.2 的探测代码确认 codec 在不在;在的话检查播放源的 codec 标签和实际文件是否一致(用媒体信息工具看一下真实标签)。渲染侧的问题,把 3.3 里被写死的--disable-gpu撤掉再看一次,很多「只有声音」在软解路径下就正常了,反过来说明是 GPU 侧问题。

5.4 同一份包,H.265 在 A 机器正常、B 机器不行

现象:功能在开发机上一直好好的,到了某台客户机上 4K H.265 播不动,甚至直接不可解;换 H.264 一切正常。

原因:H.265 的硬件解码通路依赖系统侧提供解码能力,不同系统版本、不同驱动、不同 OEM 预装策略下这块能力差别很大。开发机往往装了一堆开发工具,把缺的组件顺手补齐了,所以「我这里能跑」。

解决:先接受这个事实,然后在产品层做降级。探测到 HEVC 不可用或powerEfficient=false时,主动切到 H.264 流或者降低分辨率,而不是让用户对着黑屏。这一条如果在需求评审阶段没提,后期改起来会牵动整条转码链路。

5.5 关掉主窗口后进程残留,缓存目录写入失败

现象:窗口关了,任务管理器里还有若干个进程;或者日志里反复出现缓存写入失败、登录态每次启动都丢。

原因:进程残留通常是CefShutdown()没被调到,或者CefLifeSpanHandler::DoClose里返回了true却没有真正退出消息循环。缓存写入失败则是cache_path指向了安装目录,普通用户没写权限。

解决:把CefShutdown()放在消息循环退出之后的必经路径上,不要放在某个可能被跳过的分支里;DoClose里返回true表示「我来处理关闭」,那就要紧接着触发自己的退出流程。缓存路径统一挪到用户可写的目录,装机和绿色版都一样处理。

6. 把播放能力做成一个自检页:升级换包后五分钟验完

换包、升版本、换驱动,这三个动作任意一个发生,解码能力都可能变。靠人工点视频验证太慢,我一般会在客户端里留一个隐藏的自检页,输入特定 URL 才打开。

6.1 自检页要查的四件事

四件事按顺序查,任一项失败就停在那一步,不做后续判断:

顺序检查项判断依据失败含义
1进程模型页面里读一个由主进程注入的标记子进程没起来,或 JS 桥没通
2codec 支持canPlayType返回值包里的媒体组件有问题
3硬解能力decodingInfo的powerEfficient回落软解,性能预期要下调
4实际播放用一个 3 秒的本地样本文件跑一次前面的声明和实际不符

第四项是关键。前三个都是「声明」,声明能过、实际播不动的组合我见过不止一次——尤其是驱动版本和声明不符的情况。

6.2 一份可以直接抄的自检脚本

async function selfCheck(sampleUrl) { const out = {}; const v = document.createElement('video'); out.avc = v.canPlayType('video/mp4; codecs="avc1.640028"'); out.hevc = v.canPlayType('video/mp4; codecs="hvc1.1.6.L150.90"'); if (navigator.mediaCapabilities) { const r = await navigator.mediaCapabilities.decodingInfo({ type: 'file', video: { contentType: 'video/mp4; codecs="hvc1.1.6.L150.90"', width: 3840, height: 2160, bitrate: 20_000_000, framerate: 60 } }); out.hevcHardware = r.supported && r.powerEfficient; } // 真实播放:3 秒样本,看能不能推进到第 2 秒 out.playable = await new Promise((resolve) => { const el = document.createElement('video'); el.muted = true; el.src = sampleUrl; const done = (ok) => { el.remove(); resolve(ok); }; el.addEventListener('error', () => done(false)); el.addEventListener('timeupdate', () => { if (el.currentTime > 2) done(true); }); setTimeout(() => done(false), 8000); el.play().catch(() => done(false)); }); console.table(out); return out; }

自检页不用做得好看,能打印出一张表就够了。这个脚本我一般在换包当天跑一遍,把结果贴进变更记录。

从那以后我每次换 CEF 包,都会先把这份自检页跑一遍再谈功能验证。血泪经验是:视频类问题里,真正出在业务代码上的比例很低,绝大多数是包、目录、系统能力三者之一。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进