WinForm 4.8 + WebView2 劫持直播流:CDP 抓包与 FFmpeg 落盘实战

话说那天下午,测试同学丢过来一个工单:“XX 直播平台网页版在 IE 里直接崩,你们 WinForm 壳能不能救?” 我心想,WinForm 4.8 套个 WebView2 不就完事了?结果产品经理补了一刀:“顺便把直播流录下来,要本地 mp4。”

好家伙。直接录屏?糊成狗。让用户开 OBS?那还要我写什么桌面应用。

于是就有了这次折腾:用 WebView2 的 CDP (Chrome DevTools Protocol) 把 WebSocket 里的直播帧捞出来,再喂给 FFmpeg 合成视频。整个过程踩了数据分片、进程死锁、音画不同步三个大坑,今天一次性抖干净。

⚠️ 先泼盆冷水:这套方案只适合 B 站、抖音网页版这类走 WebSocket 传 FLV 或 TS 分片的直播。如果平台走的是 MSE (Media Source Extensions) 或 WebRTC,CDP 抓不到完整流,趁早换方案。

一、CDP 不是“抄”页面,是“接”水管

很多人一听 DevTools Protocol 就以为是搞自动化爬虫,其实它本质上是 Chrome 对外暴露的 调试控制总线。我们通过它干两件事:

  1. 给 WebView2 开一个远程调试端口(9222)
  2. 订阅 Network.webSocketFrameReceived 事件,拿到每一帧裸数据

代码就三行,但有个版本雷区:WebView2 SDK 1.0.2210.55 以下不支持 DevToolsProtocolHelper。我项目用的 1.0.2651.41,稳。

// 初始化时带调试端口
var env = await CoreWebView2Environment.CreateAsync(null, null, 
    new CoreWebView2EnvironmentOptions 
    { 
        AdditionalBrowserArguments = "--remote-debugging-port=9222" 
    });
await webView21.EnsureCoreWebView2Async(env);

// 启用 Network 域并挂事件
var helper = webView21.CoreWebView2.GetDevToolsProtocolHelper();
await helper.Network.EnableAsync();

helper.Network.WebSocketFrameReceived += (sender, e) =>
{
    // 只抓二进制帧 (Opcode = 2),文本帧通常是 JSON 控制消息
    if (e.Frame.Opcode == 2)
    {
        byte[] rawData = Convert.FromBase64String(e.Frame.PayloadData);
        // 这里先攒着,后面讲怎么喂给 FFmpeg
        _frameBuffer.Enqueue(rawData);
    }
};

输出? 没输出。WebSocketFrameReceived 每秒能触发 100~200 次,你 Console.WriteLine 直接卡死 UI。建议用 ConcurrentQueue 攒数据,另起一个线程消费

有个坑:PayloadData 是 Base64 编码的,不是原始二进制。上面 Convert.FromBase64String 这步不能省,否则 FFmpeg 吞进去全是乱码。

二、流式数据不是“攒”完再转,是“边收边喂”

一开始我犯了个新手错误:把所有帧攒成一个 byte[],再一次性写文件。结果直播 5 分钟,内存飙到 1.2GB,GC 直接罢工。

正确的姿势是 管道式消费

  • 消费线程从 ConcurrentQueue 里取帧
  • 判断帧类型 (H.264 NAL 单元 / AAC ADTS 头)
  • 按 FLV 或 TS 容器格式封装后,通过 stdin 喂给 FFmpeg

这里我选 FLV 封装,因为 FFmpeg 对 pipe: 输入 FLV 的支持最稳定。

// 启动 FFmpeg 子进程,从标准输入读 FLV
var ffmpegStartInfo = new ProcessStartInfo
{
    FileName = "ffmpeg",
    Arguments = "-i pipe:0 -c copy -f mp4 -y output.mp4",
    UseShellExecute = false,
    RedirectStandardInput = true,
    RedirectStandardOutput = true,
    CreateNoWindow = true
};
_ffmpegProcess = Process.Start(ffmpegStartInfo);

// 消费线程
while (_isRecording)
{
    if (_frameBuffer.TryDequeue(out byte[] frame))
    {
        // 这里需要把 frame 封装成 FLV 标签,篇幅所限省略具体封装逻辑
        // 参考:flv 规范,H.264 用 AVCDecoderConfigurationRecord + NALU
        var flvTag = BuildFlvTag(frame); 
        _ffmpegProcess.StandardInput.BaseStream.Write(flvTag, 0, flvTag.Length);
        _ffmpegProcess.StandardInput.BaseStream.Flush();
    }
    else
    {
        Thread.Sleep(10);
    }
}

关键数字Thread.Sleep(10) 不能省。如果队列空时忙等,CPU 占用会飙到 25% 以上。实测 10ms 休眠,CPU 稳定在 3%~5%。

三、典型翻车:FFmpeg 进程“假死”与 stdin 阻塞

你以为上面代码能跑?天真。

跑着跑着发现 _ffmpegProcess.StandardInput.BaseStream.Write 在写了几千帧之后 卡住不返回了。用 Process Explorer 一看,FFmpeg 的 stdin 管道缓冲区满了,而 FFmpeg 因为输出(stdout/stderr)没被消费,导致整个管道死锁。

解决方法:异步消费 FFmpeg 的标准输出和错误输出,哪怕不处理也要 Read。

_ = Task.Run(() =>
{
    while (_ffmpegProcess.StandardOutput.ReadLine() != null) { }
});
_ = Task.Run(() =>
{
    while (_ffmpegProcess.StandardError.ReadLine() != null) { }
});

加上这两行后台读取任务后,stdin 写入再也没卡过。

四、音画不同步?那是 PTS 没处理好

直播流里视频帧和音频帧是分开的 WebSocket 通道(或同一通道不同 opcode)。我抓到的数据里,视频帧每 40ms 左右一个,音频帧每 100ms 左右一个。

如果不做时间戳(PTS/DTS)对齐,直接按到达顺序喂给 FFmpeg,出来的 mp4 会越看越“声画分离”。

我的做法是:用 WebSocket 帧自带的时间戳字段(如果协议有)或者用本地 Stopwatch 打时间戳,在封装 FLV 时写入 PTSDTS 字段。

// 封装 FLV 视频标签时,强制设置 PTS 为相对时间
uint pts = (uint)(_stopwatch.Elapsed.TotalMilliseconds * 90); // 90kHz 时钟
flvTag[5] = (byte)(pts >> 16);
flvTag[6] = (byte)(pts >> 8);
flvTag[7] = (byte)pts;

音频同理,但 AAC 的 PTS 要跟视频的参考时钟对齐,不能独立计时。我踩坑后用的是 视频 PTS 作为主时钟,音频帧到达时取最近的视频 PTS 赋值。这样导出的 mp4 用 MediaInfo 看,音视频 duration 差值小于 20ms。

五、性能压测:CDP 抓包 vs 直接录屏

方案 CPU 占用 (i5-8250U) 内存峰值 帧同步误差 是否依赖窗口可见
CDP + FFmpeg 流式 12%~18% 280 MB < 20 ms
gdigrab 录屏 35%~40% 150 MB 200~500 ms
DXGI 抓屏 (ddagrab) 25%~30% 180 MB 100~200 ms

CDP 方案 CPU 赢在 不解码不渲染,直接转发原始码流。代价是内存略高(因为有队列缓冲),但可以接受。

如果你只是想快速交差,不 care 画质和同步,用 ffmpeg -f gdigrab -i title="窗口名" 确实省事。但产品要的是“可后台录制”,那就只能走 CDP 这条窄路。

六、最终落地方案与避坑清单

  1. WebView2 版本:必须 ≥ 1.0.2210.55,推荐最新稳定版(我用的 1.0.2651.41)
  2. FFmpeg 参数-i pipe:0 -c copy -f mp4 -y output.mp4,不要加 -re,那是实时推流用的
  3. 进程管理CreateNoWindow = true + RedirectStandard* = true + 异步消费 stdout/stderr,缺一不可
  4. 数据封装:FLV 标签必须写对 TagType (8=音频, 9=视频, 18=脚本) 和时间戳,否则 FFmpeg 报 Non-monotonous DTS
  5. 退出清理:录制结束后,_ffmpegProcess.StandardInput.Close()WaitForExit(),否则 FFmpeg 收不到 EOF 会一直等

最终代码跑了 2 周,每天 4 小时直播录制,没有崩溃,没有音画不同步。产品满意,测试闭嘴,我也终于能安心下班。

至于那些问“为什么不直接调浏览器 API 拿流”的人——你猜怎么着?WebView2 的 CoreWebView2 压根不暴露 MediaRecorder,CDP 是唯一官方后门。

如果哪天 Chrome 把这个 CDP 事件也给禁了……那咱们再想别的歪招。

分享到:

本文链接:https://www.biyeyuanma.cn/post/169.html

猜你喜欢

随机文章
热门标签
图片名称

服务热线

加我微信

加我微信