结论:直播会话最值得记录的不是「能否连上」,而是「参数真值与会话存在」会脱钩:驻车路径先写 IsLiveStreaming=True,若本机服务在生成 Answer 时超时或抛异常,代码会移除会话并把错误交回 HTTP 线程,却不安排 5 秒清参数,全局布尔位可能保持为真。此外 HTTP 本机入口直接信任 Content-Length,没有请求体大小或读取 deadline 上限。以下基于本地 source/ 快照 c8fb906 的隔离探针与静态追踪,未运行真实 WebRTC 连接或 HTTP 压测。
唤醒与接入:athenad 到 webrtcd
驻车时 athenad.startStream() 先写 IsLiveStreaming=True,等本地 webrtcd 可用再 POST;普通汽车驻车路径由 manager 按该参数启用 webrtcd 和相关直播服务。请求侧 wait_for_webrtcd() GET /schema,仅 ConnectionError 增加重试计数并休眠;post_stream_request() POST /stream 并解析 JSON,却不检查 HTTP 成功状态,4xx/5xx 的 JSON 也能作为普通返回值到上层。
服务侧用 ThreadingHTTPServer、本机默认端口 5001;HTTP 线程用 run_coroutine_threadsafe(...).result() 等待 asyncio loop,路由仅接收三个路径。stream_lock 串行化会话替换:若已有启用会话,禁用型预热请求返回 HTTP 200 的 busy JSON;否则通知并停止旧会话,再构建新会话,Answer 生成限时 30 秒,成功后启动 run_task。
一个可追踪的失败窗口
这条结论不依赖网络是否暴露:驻车 startStream() 先写 IsLiveStreaming=True;若本机服务已启动但 handle_get_stream() 在生成 Answer 时超时或抛异常,代码会移除会话并把错误交回 HTTP 线程,不会安排 5 秒清参数。正常的清参数仅由已启动 run_task 的完成回调触发。也就是说「客户端拿到 500」与「直播参数已清除」是两件独立的事。
另一个独立边界是 wait_for_webrtcd(max_retries=10):计数只在连接异常时增加。服务持续返回非 2xx 时不会达到最大重试次数,这不是「最多十次 GET」的实现;ReadTimeout 等其他异常也不是这里专门处理的重试路径。
时限与状态转移
整条链路的关键时限散落在不同函数里,把它们放在一起看才能估计最坏等待。
| 阶段 | 时限 / 机制 |
|---|---|
| Answer 生成 | 限时 30 秒 |
| 会话连线 | 限时 15 秒 |
| 普通会话 | 最长 300 秒 |
testJoystick body 会话 |
等待断开,不适用 300 秒上限 |
| 清参数 | run_task 完成后 5 秒,若会话字典仍空才写 IsLiveStreaming=False |
| 重试计数 | 仅 ConnectionError 计数,每次休眠 0.5 秒 |
StreamSession.run() 连接后启动数据桥和码率任务;普通会话 300 秒超时,testJoystick body 会话则等待断开。清理由锁和 _cleanup_done 避免重复执行;若清理中途抛错,完成标记已设置,后续调用会跳过剩余资源,而且清理自身也不清 IsLiveStreaming。
HTTP 本机边界:Content-Length 没有上限
内容类型必须严格等于 application/json,附 charset 的值在解析 body 前即返回 415;未知路径和错误方法在读体前分别返回 404/405;/notify 畸形 JSON 返回 400,未捕获的路由异常返回含异常类名与消息的 500 JSON。
真正的边界在读取:_read_body() 直接信任 Content-Length,没有请求体大小或读取 deadline 的本地上限;隔离探针确认 Content-Length=-1 被传给 rfile.read(-1),十亿长度也原样传入读取。默认仅监听 127.0.0.1,但本地调用方、端口转发和自定义 --host 仍是部署权限边界。
硬件与插件验收
建议把直播能力改为由受信任 supervisor 授予的有租期会话 lease,包含发起者、允许摄像头/主题、最大时长和清理责任;失败、取消、替换与服务进程崩溃均需释放 lease。IsLiveStreaming 这种全局布尔位不应作为唯一所有权记录。_shutdown() 会先停 HTTP 接入、再逐会话通知并停止、最后停事件循环,但关机路径不显式写 IsLiveStreaming=False,仍依赖 manager/参数生命周期接管。
失败路径与清理顺序
清理只在特定路径上被正确注册:remove_finished_session() 回调只在 Answer 成功、session.start() 之后才注册,所以生成 Answer 失败或入口在此之前异常,都不走这条退场门。正常清理则按固定顺序执行。
| 顺序 | 清理动作 |
|---|---|
| 1 | 写 LivestreamRequestKeyframe=False |
| 2 | 停止码率任务与出站桥 |
| 3 | 停止轨道 |
| 4 | 停止媒体流 |
若码率任务的 stop() 抛错,_cleanup_done 已为真,出站桥、轨道和媒体流都不会再执行停止,随后重试也直接返回;连接失败时 run() 的 finally 仍会尝试清理。普通会话等待超时会发送 disconnect,而 testJoystick body 分支没有同一套超时/通知处理,显式 stop() 则会取消活动任务后再清理。
常见问题
为什么客户端拿到 500,直播参数还可能是真?
因为 Answer 失败分支会移除会话,但不调用 schedule_teardown();清参数只由已启动 run_task 的完成回调安排,正常退场门在此路径上没有被触发。
wait_for_webrtcd 真的最多试十次吗?
不是。重试计数只在 ConnectionError 时增加;服务持续返回非 2xx 时循环不递增次数也不休眠,所以这不等于「最多十次 GET」。
服务端会限制请求体大小吗?
不会。_read_body() 直接信任 Content-Length,没有请求体大小或读取 deadline 的本地上限;Content-Length=-1 会被原样传给读取。
Content-Type 带了 charset 会怎样?
内容类型必须严格等于 application/json;附 charset 的值在解析 body 之前就会返回 415。
延伸阅读
- 远程 RPC 与 WebRTC 的权限边界:本机入口为什么也要认证 —— 直播入口的权限与所有权设计
- Athena 上传队列:持久化、重试与进程重启后的续传 —— 同一类「失败后参数与状态是否收敛」的问题
- UDS 诊断工具为什么不能随手调:只读、会话与写 ECU 的分权设计 —— 外部入口必须显式分权
- 设备身份注册:会话、证书与唯一性约束 —— 有租期会话与设备身份的关联
参考资料
- 项目 02 ——
openpilot-research/WEBRTC_SESSION_LIFECYCLE_HTTP_BOUNDARY_DEEP_DIVE.md(athenad→webrtcd→manager 的会话生命周期与 HTTP 边界,含源码行号索引) - 项目 02 ——
openpilot-research/probes/test_webrtc_stream_session_replacement_contract.py(Answer 失败与会话替换的五项异步探针) - 项目 02 ——
openpilot-research/probes/test_webrtc_http_handler_boundary_contract.py(路径、方法与Content-Length边界的七项探针)
说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。
更新记录
首次发布 2026-10-11 最近核对:2026-10-11
发表回复