- 音视频
- 直播
- 移动开发
【免费下载链接】pure_live
纯粹直播:哔哩哔哩/虎牙/斗鱼/快手/抖音/网易cc/YY直播/Twitch直播/SOOP直播/M38自定义源应有尽有。
导读
本文基于 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 ms | 0 B | 否 | 失败 |
| 700 ms | 0 B | 否 | 失败 |
| 1200 ms | 262,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中实现:
- 同一 session 的重复停止共用
session.stopRequest ??= ...的一个 Future,租约结束后用户再点停止只更新用户意图,不会并行发第二次 native 取消; - 先调用
FFmpegInputDrain.tryFinish(finishInput: relay.finish, completion: session.completion.future):relay.finish()置位_finishing,让已转发的完整 AVC 访问单元继续下发直至图片到达; - 排空预算默认3 秒(
FFmpegInputDrain.tryFinish的deadline = Duration(seconds: 3)),输入结束或原生排空超过预算仍未完成,才降级使用旧 native cancel 作故障终止,并继续等待真实原生完成——超时不等于资源已经退出; - 终止事件记录
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 默认缓冲会让少量数据等待,已显式禁用 response
bufferOutput,并用停在半个 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、错误日志为空 |
| 最终 MP4 | 133,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自定义源应有尽有。
相关推荐
pure_live 虎牙录制凭据租约审计实战:原生 FLV 免取消刷新与手动停止收尾完整性契约
pure_live 虎牙录制凭据租约审计实战:原生 FLV 免取消刷新与手动停止收尾完整性契约 本篇以 pure_live 开源仓库 2026 09 05 的录
音视频直播移动开发Pure Live FLV 录制停止边界修复实战:AVC 访问单元完整性、终止证据与源保留审计
Pure Live FLV 录制停止边界修复实战:AVC 访问单元完整性、终止证据与源保留审计 导读 本文基于 Pure Live 仓库中 docs/FLV_A
音视频直播移动开发pure_live Android AVC 停止边界修复:从 FLV 尾部缺帧到 3.1.8 候选构建验证
pure_live Android AVC 停止边界修复:从 FLV 尾部缺帧到 3.1.8 候选构建验证 导读 本文围绕 pure_live(纯粹直播)录制链
音视频直播移动开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考