news 2026/10/14 3:14:45

pure_live 录制停止的优雅排空:FLV 输入回环中转与 FFmpeg 原生输出完整性修复

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
pure_live 录制停止的优雅排空:FLV 输入回环中转与 FFmpeg 原生输出完整性修复
  • 音视频
  • 直播
  • 移动开发

【免费下载链接】pure_live

纯粹直播:哔哩哔哩/虎牙/斗鱼/快手/抖音/网易cc/YY直播/Twitch直播/SOOP直播/M38自定义源应有尽有。

项目地址:https://gitcode.com/gh_mirrors/pur/pure_live
点击查看免费下载

导读

本文基于 pure_live 项目(Flutter 跨平台直播客户端)录制模块的一次专项修复审计,剖析「用户手动停止直播录制时,原生 FFmpeg 取消同时截断输入与输出、导致末段 TS 损坏」这一integration-conflict类缺陷的完整修复链路:通过独立的 FLV 回环输入中转实现「输入正常结束」而非「输出强制取消」,配合有界排空预算、AVC 访问单元边界观测与多层级验证体系,最终保证停止后的原始 TS 分片 188 字节对齐、可完整解码且未强制取消。读完本文,你将理解 FFmpegKit 停止合同的底层缺陷、FLV 帧边界拆包与转发原理,以及如何在录制场景设计可复现、可门禁的停止完整性验收。

一、问题归属:原生取消合同的输入/输出连带中断

本批修复的基线提交为f66cff51a1522d160a6ec7fa683b5f0ef308e1e8,承接 虎牙录制租约审计,不合并上游、不连接手机。此前一批已修复「多余断流」与「损坏封装误报成功」,但未把「取消截断」标为已解决。

问题根源被归类为原生依赖与本地停止策略的integration-conflict:本项目锁定的原生构建为ffmpeg-kit-extended 0.11.1,其 session cancellation 回调同时作用于输入与输出两个上下文——native 的decode_interrupt_cb在 cancel 后直接返回中断标志,输出上下文也注册了同一中断回调,segment muxer 将输出回调继续传给分片。后果是:

  • 本项目原先直接用FFmpegKit.cancel停止录制,取消会同时阻止输入继续读取和输出尾部写入;
  • 原先仅按返回码判断成功,返回码 0 会被误当作封装成功;
  • 取消瞬间正在缓冲的 TS 尾部无法落盘,放大第三方原生中断合同的影响。

由于真实虎牙 CDN 复现中已观察到error writing trailer: immediate exit requested后跟pes packet size mismatch/corrupt input packet in stream 1,这一损坏不归因于直播平台、登录或 CDN,而是本地停止策略与原生中断合同冲突的直接结果。

二、固定输入隔离服务器:不依赖直播的可复现红测

在动手修复前,先建立固定输入隔离服务器,把问题从真实 CDN 中剥离出来。tool/probes/recorder_flv_stop_probe_test.dart 的探针设计如下:

  • 使用自行生成的 H.264/AAC FLV 夹具(PURELIVE_FLV_STOP_FIXTURE环境变量指定);
  • 通过HttpServer.bind(InternetAddress.loopbackIPv4, 0)绑定本机回环端口,按原始时间戳逐个分片发送 FLV tag;
  • 走实际生产链路:FFmpegManager、FFmpegService.start(..., liveRecording: true)、FFmpegCommandBuilder.buildRecordArguments,segment 时长 10 秒、rwTimeout5 秒、threadQueueSize128;
  • 每次只运行一个 native 录制,停止后对全部输出 TS 做独立全文件解码(ffmpeg -xerror -f concat ...)。

旧生产停止代码的原生红测记录20260905T113521899Z-quality-focused.json复现了三个停止时点的损坏:

开始产出后的停止延迟原始 TS 大小188 字节 TS 包对齐全文件解码
150 ms0 B否失败
700 ms0 B否失败
1200 ms262,144 B否失败

三次均报告error writing trailer: immediate exit requested。由于服务器是同一台机器的固定测试源,这处损坏与虎牙 CDN 无关,可在无网络、无账号、无 Android 设备的环境下稳定复现。现场摘要保留在 ignored 的local-artifacts/recording-probes/flv-stop-1788608089843663/summary.json。

三、修复架构:输入停止与输出取消彻底分离

修复的核心思想是:结束输入,而不是取消写盘。正常停止不再设置 native cancel 标志,而是让 FFmpeg 把已收到的完整媒体包和分片尾部写完后再自然结束会话。

3.1 独立回环输入通道

实现位于 lib/core/stream/ffmpeg_flv_input_relay.dart 的FFmpegFlvInputRelay,只为liveRecording的 HTTP(S).flv输入创建独立回环通道:

  • 播放器、观看模式、纯音频播放中转、文件封装任务都不进入此通道;
  • startForArguments解析 FFmpeg 参数中的-i上游地址,仅接受http/https协议;当站点声明该线路为 FLV 容器(declaredFlv)或 URL 以.flv结尾时启用(见 test/domains/recorder/record_input_relay_facts_test.dart 中「按声明容器而非后缀触发」的事实测试);
  • 回环服务只绑定127.0.0.1,使用随机独立路径(Random.secure()生成的 18 字节随机 secret)、单次读者和单个上游连接,不开放任意 URL 转发;私有回环 URL 形如http://127.0.0.1:port/<secret>/live.flv。

replaceFirstInput会把 FFmpeg 参数中的首个-i地址替换为回环 URL,并移除-reconnect、-reconnect_streamed、-reconnect_on_network_error、-reconnect_on_http_error、-reconnect_delay_max、-reconnect_at_eof等原生 HTTP 自动重连参数——因为私有回环 URL 是一次性输入,native 对它的重复 GET 会变成第二次请求并收到 410;真实断流继续交给录制控制器取新 URL。

3.2 帧边界:完整 tag 转发,不裁剪不缓存直播

转发层强调原样透传:转发完整 FLV 头和原始完整 tag,不解码、不重编码、不修改时间戳、比例或音轨。关键实现包括:

  • FlvInputFramer负责 FLV 拆包:校验FLV魔数与版本(0x46 0x4c 0x56 0x01)、头大小(9–65536 字节)、首 tag 的 PreviousTagSize 为 0,随后以11 字节 tag 头 + 24 位 DataSize + 4 字节尾字段划分边界;
  • PreviousTagSize 兼容:部分生产者的旧值/零值由 FFmpeg 兼容处理,本层以 DataSize 划分边界、原样保留尾字段,不抢在 native 前拒绝这些可解码流;
  • 不缓存整个直播:最大待收单个 tag 受 FLV 24 位 DataSize 限定,头扩展最多 64 KiB,转发等待写入进度(await downstream.response.flush()),不按直播时长累计缓冲;
  • 手动停止时正在接收但尚未组成完整 tag 的数据不会交给 native,避免裁剪已落盘文件;
  • 请求头处理:原始请求头用于上游,移除host、range及connection、content-length、transfer-encoding、keep-alive、proxy-authorization、proxy-authenticate、te、trailer、upgrade等逐跳头;上游 HTTPS 仍使用 Dart 系统证书校验;
  • 远程 403 等非 200 状态原样传给 native,不伪装成功;未发出响应头时上游失败回 502。

3.3 有界排空:3 秒预算 + 故障终止降级

停止流程在 lib/domains/recorder/data/services/ffmpeg_service.dart 的_requestSessionStop中实现:

  1. 同一 session 的重复停止共用session.stopRequest ??= ...的一个 Future,租约结束后用户再点停止只更新用户意图,不会并行发第二次 native 取消;
  2. 先调用FFmpegInputDrain.tryFinish(finishInput: relay.finish, completion: session.completion.future):relay.finish()置位_finishing,让已转发的完整 AVC 访问单元继续下发直至图片到达;
  3. 排空预算默认3 秒(FFmpegInputDrain.tryFinish的deadline = Duration(seconds: 3)),输入结束或原生排空超过预算仍未完成,才降级使用旧 native cancel 作故障终止,并继续等待真实原生完成——超时不等于资源已经退出;
  4. 终止事件记录inputDrained与forcedCancel两个标志。

terminalEvidence()给出可审计的生命周期快照:inputDrained = finishRequested && !forcedCancel && !hasPendingAccessUnit,同时输出inputDrainKind(flv/hls/none)、inputDrainBudgetMs(FLV 固定 3000 ms)、stopElapsedMs等字段。

3.4 AVC 访问单元边界:在 tag 边界之上再等一帧

lib/core/stream/ffmpeg_flv_input_relay.dart 中的FlvAvcAccessUnitBoundary是一个常量内存的 AVC 观测器(非编解码器),用于避免在 TS 解析器的访问单元中间切断:

  • 从 AVCDecoderConfigurationRecord 读取lengthSizeMinusOne,识别 AVC tag 内的 NAL 长度前缀;
  • 当看到 picture NAL(type 1/2/5)时清除 pending 状态,看到 SEI/SPS/PPS/AUD(type 6–9)时置为 pending;
  • finish()时若存在 pending 的访问单元,则继续等待其 picture 到达后再停止;已转发的前缀 NAL 从不被移除或重写;
  • 停滞的上游仍受 FFmpegService 现有 3 秒截止约束,close/cancel 可立即打断。

四、分层验证:从离线回归到真实虎牙生产复验

该批次建立了一套证据分层体系,明确区分「源码 / 离线回归 / Windows native / Android 实机」不同证据层。

4.1 离线回归与失败记录保留

字节拆包、跨块输入、缺半个 tag、旧 PreviousTagSize、累计缓冲、立即/重复停止、两个任务隔离、错误状态、随机路径、请求参数、输入/原生停止时限均有离线测试。过程中发现并修正两个问题:

  • 新中转的最初小包测试发现 HTTP 默认缓冲会让少量数据等待,已显式禁用 responsebufferOutput,并用停在半个 tag 的本地输入验证即时结束——不通过增加测试等待时间解决;
  • 首轮新停止方式的原始 TS 已全部对齐、完整解码退出 0 且 stderr 为空,但探针把输入结束的Error during demuxing: I/O error和输出写尾错误合成一个布尔值,整轮记为 FAIL。两份失败记录20260905T113757687Z-quality-focused.json、20260905T114114479Z-quality-focused.json保留,不重新标绿。

4.2 验收标准拆解:独立解码标准不降低

验收被拆分为独立断言,不再把退出码当作唯一证据(解码器会容忍部分截断,单独退出码不足):

  • 保留所有 native 日志(native.log),不隐藏输入结束的 demux I/O 诊断;
  • 单独断言没有输出写尾/写包失败(outputWriteError:诊断尾部不含error writing trailer/error muxing a packet);
  • 全部 TS 文件满足s > 0 && s % 188 == 0包对齐;
  • 独立 FFmpeg 全文件音视频解码退出 0 且错误日志为空;
  • forcedCancel == false、native 会话已结束(nativeRunning == false)、inputDrained == true。

原始旧代码的空文件/截断文件在这套标准下仍然失败。最终固定输入门禁20260905T114654798Z-quality-focused.json:15/15,包含 14 项离线回归及 1 项三时点实际 native 对照,均无强制取消、输出写尾错误或完整解码错误。

4.3 真实虎牙生产控制器复验

20260905T115346874Z-quality-focused.json:1/1 opt-in 原生探针通过。2026-09-05 19:47:22(UTC+8)开始,使用实际HuyaSite、录制控制器、FFmpegKit 和 MP4 收尾链路,不是仅抓 URL 或只看返回码:

检查项实际结果
持续录制观察325 秒,325 次采样均为 running
native 输入启动 / 定时轮转1 / 0
凭据解析2 次,跨过凭据刷新时点但不重启健康输入
原始 TS保留 2 片;独立全文件音视频解码退出 0、错误日志为空
最终 MP4133,122,288 B;独立全文件音视频解码退出 0、错误日志为空
收尾stage=complete,nativeStopped=true

上一批相同生产探针因末片截断导致源 TS 和 MP4 解码失败;新批直接修复输入/输出停止合同,而不是去掉解码门禁或删除失败原片。文档也明确指出:实时媒体本身不同,文件大小差异不作为吞吐或性能提升依据;原生合并进度日志仍有已知无效大时间戳,文件有效性以独立解码结果为准。

4.4 最终受影响模块回归

  • 20260905T120210846Z-quality-focused.json:31 个相关测试文件234/234通过;唯一一次 analyze 47.2 秒、No issues found;全仓自动结构扫描 4024 文件、0 错误、1 项既有 empty-catch 盘点提示;未 clean、未改变锁定依赖;
  • 随后仅修正 native 探针计时(在真实 native 完成后停止 Stopwatch,独立解码耗时单独排除),不修改生产业务代码;
  • 20260905T120400927Z-quality-focused.json:串行 2/2 原生探针通过,其中一项用隔离副本复验正常/截断 TS——正常输入提交 MP4 后删除副本,损坏输入报告失败、保留副本且不提交 MP4;
  • 最后固定输入三次正常停止耗时 19/4/3 ms(仅该样本,不外推所有媒体),inputDrained=true、forcedCancel=false、独立解码退出 0 且错误日志为空;摘要位于local-artifacts/recording-probes/flv-stop-1788609819812558/summary.json。

五、实现边界与明确不回填的范围

文档对本次修复的边界做了严谨声明:

  • 只覆盖 liveRecording 的 HTTP(S).flv输入。HLS、RTMP、非.flv的不透明地址保持现有路径,仍需独立停止收尾验收,不由本批结果推断正常;后续 HLS 的独立修复与验证见 HLS 排空审计(其排空预算为2 × 最大 TARGETDURATION + 2 秒,限定 3–20 秒,与 FLV 的 3 秒预算分开);
  • 这不是对全部 FFmpeg 原生中断 API 的修补:后续若依赖提供可靠的分离停止合同,应重新评估中转层;
  • 本次实际 native 证据来自 Windows、单个虎牙房间的一条 FLV 线路;Android 原生、其他平台/协议、完整 GUI 录制同时观看及长时间性能仍在总体验收中,没有以本轮通过覆盖历史 Android PiP 故障。

相关的生产代码关键位置:FFmpegService 停止入口(_requestSessionStop、FFmpegInputDrain、FFmpegMediaIntegrity完整性错误锁存)、FLV 回环中转(FFmpegFlvInputRelay、FlvInputFramer、FlvAvcAccessUnitBoundary)、录制命令构建(-segment_format_options flush_packets=1等分段选项),以及 VideoProcessorService 的 MP4 收尾链路(保留损坏源文件、不提交 MP4)。

六、回滚与迁移

本批无依赖、原生二进制、持久化数据或设置迁移。若需要回滚,只撤销本批录制输入中转及 FFmpegService 停止入口,保留上一批健康 FLV 不定时取消、损坏封装保留源文件的独立保护。文档明确要求:回滚会重新暴露固定输入已经复现的取消写尾问题,应在发布说明中明确,不悄悄恢复旧路径。

七、结语:停止也是一种必须验证的完整性操作

本次修复的核心方法论值得复用:直播录制没有自然成功 EOF,唯一成功的终止事件是用户停止——因此停止路径的完整性必须与录制本身同等对待。通过固定输入隔离复现、帧边界正确的回环转发、有界排空预算、以及「输出写尾 / 包对齐 / 独立全文件解码 / 未强制取消」的多维度验收,pure_live 在不降低解码门禁、不掩盖原生日志的前提下,修复了手动停止导致的末段 TS 损坏。所有原始媒体仅存 ignored 本地诊断目录;本批未产生新正式 APK、3.2.0 tag 或 Release,不作为完整客户端验收完成。

  • 音视频
  • 直播
  • 移动开发

【免费下载链接】pure_live

纯粹直播:哔哩哔哩/虎牙/斗鱼/快手/抖音/网易cc/YY直播/Twitch直播/SOOP直播/M38自定义源应有尽有。

项目地址:https://gitcode.com/gh_mirrors/pur/pure_live
点击查看免费下载

相关推荐

上一篇:Cayley 数据导出指南:深入解析 cayleyexport 命令与 API v2 底层原理
下一篇:CoffeeScript 2.3.0 发布详解:ES2018 异步迭代器、对象 Rest/Spread、幂运算符与正则 s 标志

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 3:14:45

AscendNPU-IR 入门指南:基于 MLIR 的昇腾亲和算子编译中间表示

编译器人工智能AscendCANN 【免费下载链接】AscendNPU-IR AscendNPU-IR是基于MLIR&#xff08;Multi-Level Intermediate Representation&#xff09;构建的&#xff0c;面向昇腾亲和算子编译时使用的中间表示&#xff0c;提供昇腾完备表达能力&#xff0c;通过编译优化提升昇腾…

作者头像 李华
网站建设 2026/10/14 3:14:24

Unity Windows原生窗口控制:无边框与状态栏适配实战

1. 项目概述&#xff1a;为什么在Unity里“动”Windows原生窗口是个刚需做Unity桌面应用的开发者&#xff0c;尤其是开发工具类、工业控制面板、数字孪生看板或者需要深度集成Windows系统行为的项目时&#xff0c;迟早会撞上这个墙&#xff1a;Unity默认的窗口管理太“温柔”了…

作者头像 李华
网站建设 2026/10/14 3:13:42

Python爬虫必备:同步与异步HTTP库全解析

在进行数据分析以及竞品调研的时候, 首先需要明确, 最基础也是最关键的一个步骤, 就是必须把数据从网络上面抓取下来。而在整个过程中里最常用的两个 HTTP 库是同步的和异步的这两个&#xff08;具体的库名称可能需要读者自行对应上下文填补空缺&#xff09;, 这两者加在一起就…

作者头像 李华
网站建设 2026/10/14 3:13:22

Coze智能体搭建实战:从零跑通工作流与Web应用

1. 这不是“又一个Coze教程”&#xff0c;而是一份能让你真正跑通第一个智能体的实操手记我带过不少刚接触Coze的朋友&#xff0c;从某高校实验室的研究生&#xff0c;到某公司转型做AI产品的运营同事&#xff0c;再到完全没写过代码的自由职业者。他们问得最多的问题从来不是“…

作者头像 李华
网站建设 2026/10/14 3:12:22

CSDN + AI:程序员的新生产力 (e 付费专栏)

1. 引言:AI 时代,程序员的生产力正在被重新定义 从代码补全到智能问答,从文档生成到代码审查,AI 正在以前所未有的速度渗透进程序员的日常工作流。作为国内最大的开发者社区,CSDN 也在积极拥抱这一趋势,将 AI 能力融入内容创作、技术学习和代码开发的全链路。本文将从多…

作者头像 李华