标签: WebRTC

  • WebRTC 直播码率与关键帧:0.5/1.5/5 Mbps 三档

    结论:直播码率自适应是三档电平控制:默认 0.5/1.5/5 Mbps,每 0.2 秒采样 RTCP 接收报告,丢包率 ≥10% 立即降一档、≥5% 累积五次降档,零丢包累积五次升档且降档后升档等待次数翻倍。关键帧请求与码率都是全局 Params 电平,不含相机 ID、请求 ID 或逐路确认,多路会话共享同一请求位。时间维度上,WebRTC PTS 取的是发送时的 monotonic_ns 换算为 90 kHz,并不等于相机采样的 SOF/EOF。以下为固定源码快照 c8fb906 及固定版本 teleoprtc 归档、并对原码率类与视频轨道做隔离探针的静态分析,未运行真实相机、编码器或网络丢包测试。

    关键函数与跨进程调用

    入口 源码行为 边界
    LivestreamBitrateController.__init__()、run() 默认三档 0.5/1.5/5 Mbps(最高档可由 STREAM_BITRATE 覆盖);初始化就把最高档写 LivestreamEncoderBitrate。每 0.2 秒按接收报告调档。 只采样 get_stats() 字典的第一份报告,并非多轨道最坏/加权网络状态;未观察或报告完全不变则保持当前控制状态。
    _sample()、set_quality() fraction_lost / 256 转换丢包比例;手动 high/med/low 直接写 Params 并停止自动控制,auto 恢复自动。 手动模式不更新内部 level、counter、up_samples;回到 auto 后首次调档仍以先前自动状态为基准。代码未核验编码器是否实际应用码率。
    StreamSession.run()、message_handler() 会话开始置 LivestreamRequestKeyframe=True;数据通道可切相机、调质量、暂停/恢复视频、启用计时 SEI。 livestreamCameraSwitch 直接更换唯一轨道的订阅源,没有重置轨道 _seen_keyframe 或显式新关键帧请求。
    LiveStreamVideoStreamTrack.recv()、switch_camera() 订阅主题采用 conflate=True;首次看到关键帧标记 _seen_keyframe=True 并清全局请求,但收到非空消息都会返回视频包。PTS 用发送时 monotonic_ns 换算为 90 kHz。 _seen_keyframe 是请求清除门,不是发帧门;入流可能先送非关键帧。多轨道共用一个关键帧 Params 位。
    _build_frame_data() 普通模式拼接 EncodeData.header + data;可选计时 SEI 额外放入 SOF→EOF、EOF→日志、日志→本机当前时间三段毫秒延迟及 wall-clock 毫秒。 SEI 里同时存在单调时间差与墙钟绝对时间;跨时钟域比较必须说明校时、漂移和跳变。

    码率状态机与探针结论

    run() 只有丢包率 ≥5% 或 ≤0% 两类调档分支;0% 到 5% 间的正丢包率不清零 counter。因此「连续五次」更准确地说是相关分支累计五次采样,不保证中间完全没有低丢包样本。_sample() 只把整个报告元组相同视为没有新样本,不能证明报告对应新的媒体时间窗口。

    五项原 LivestreamBitrateController 类隔离探针核对了:10% 报告立即降档并把升档窗口从 5 翻倍到 10;5% 报告中穿插 1% 样本仍累计到五次降档;降档后需十个零丢包样本才升档;手动 low 切回 auto 时内部仍在 high,后续零丢包不回写 high,Params 继续留在手动 low;完全相同的报告元组不会重复采样。这些探针用人工 RTCP 报告和 Params 记录器执行原类方法,不验证真实 WebRTC 统计刷新、编码器应用或网络体验。

    关键帧请求与轨道行为

    RTCP PLI 调轨道 request_keyframe(),写全局 Params;每个直播编码器每帧读取码率/关键帧参数再编码。Params 是全局电平触发控制,不含相机 ID、请求 ID、过期时刻或逐路 ACK;软件编码后端只记录不支持设置码率/请求关键帧。多路会话共享 LivestreamRequestKeyframe 和 LivestreamEncoderBitrate,而各轨道各自处理媒体,一个轨道清除全局关键帧请求可能使另一个仍未见关键帧的轨道失去持续请求。

    LiveStreamVideoStreamTrack.enable(False) 重置 _seen_keyframe,但 switch_camera() 不重置它。即使 _seen_keyframe=False,recv() 也不会阻挡非关键帧,只等待有消息;消费者能否解码取决于编码器/封装及新关键帧到达,不能由该标志保证。五项轨道与时间探针确认:订阅采用 conflate=True、切相机仅替换 socket、首次关键帧前的非关键帧仍返回、PTS 由本机取帧时单调时钟换算为 90 kHz、首次关键帧清全局请求。探针使用假基类、假帧与假时钟,不证明真实编码包可解码、相机切换连续或跨设备时钟已对齐。

    帧时间与 SEI 字段

    PTS 使用取帧时间计算,不使用 EncodeData.idx.timestampSof/Eof;网络等待、conflate 丢旧包或暂停/恢复都可能让媒体时间轴与相机采样时间轴不同。计时 SEI 中三个毫秒差依次为 SOF→EOF、EOF→日志、日志→读取时刻,第四项为墙钟毫秒。插件层需同时保留采样、编码、传输与显示时间。现有 WebRTC 会话测试检查单轨道 recv() 的 PTS 单调趋势与空包字节,并测试消息代理/非 JSON 请求;未见码率状态机、多轨道关键帧确认、切相机或 SEI 时间字段的原套件专门回归。

    时间字段 来源 不能代替什么
    WebRTC PTS 发送时 monotonic_ns 换算为 90 kHz 相机采样 SOF/EOF;反映的是取帧/发送节奏。
    SEI SOF→EOF 编码内部两段单调时间差 完整采样时间消息契约。
    SEI 日志→本机当前时间 单调时间差 跨设备已校时;仍需说明漂移与跳变。
    SEI 第四项 wall-clock 绝对毫秒 可直接与其他单调时钟字段比较。

    新硬件与插件接口验收

    编码插件至少应声明 codec/profile、每路输入时钟、最大分辨率/码率、关键帧间隔、可变码率是否受支持及实际应用后的回执;关键帧请求用 camera_id + request_id + deadline 逐路确认已输出可解码关键帧。码率控制器应以每条流的实际 RTCP 统计及编码器应用回执为依据,手动/自动切换时同步内部档位并明确计数器重置规则,无报告、报告停滞与网络失联要分开。回放/台架应覆盖多路同时启动、一条流无关键帧、相机切换、暂停恢复、PLI 连发、无接收报告、0/中/高丢包、软件编码后端不支持动态码率,以及 wall clock 跳变时 SEI 的解释。

    常见问题

    WebRTC 直播码率有哪几档,怎么切换?

    默认三档 0.5/1.5/5 Mbps,最高档可由 STREAM_BITRATE 覆盖。控制器每 0.2 秒按 RTCP 接收报告调档:丢包率 ≥10% 立即降一档、≥5% 累积五次降档,零丢包累积五次升档且降档后升档等待次数翻倍。

    关键帧请求是逐路确认的吗?

    不是。关键帧请求与码率都是全局 Params 电平,不含相机 ID、请求 ID 或逐路 ACK;多路会话共享同一个请求位,一个轨道清除全局请求可能使另一个仍未见关键帧的轨道失去持续请求。

    切相机会自动请求新关键帧吗?

    不会。switch_camera() 只更换唯一轨道的订阅源,不重置 _seen_keyframe,也没有显式新关键帧请求;而且即使 _seen_keyframe=False,recv() 也不会阻挡非关键帧。成功切换后的解码连续性需实测。

    WebRTC 的 PTS 就是相机采样时间吗?

    不是。PTS 用发送时的 monotonic_ns 换算为 90 kHz,不使用 EncodeData.idx.timestampSof/Eof;网络等待、conflate 丢旧包或暂停恢复都可能让媒体时间轴与采样时间轴不同。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/WEBRTC_BITRATE_KEYFRAME_TIMING_DEEP_DIVE.md(码率状态机、关键帧请求、PTS/SEI 时间字段与探针结论)。
    • 项目 02 —— openpilot-research/ 同目录远程 RPC 与视频编码专题(会话边界与编码路径作为相邻边界参考)。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 界面告警、驻车更新与远程服务:不在控制闭环内的进程边界

    结论:界面告警、驻车更新、云端连接与设备诊断这些进程大多不在控制闭环内,但它们决定驾驶员能否看见告警、设备能否更新、以及远程能否观测。源文档给出的一条清晰边界是:ui 只做呈现、soundd 只把上游状态转成音频、updated 只在驻车条件下由 manager 启动、webrtcd 默认只监听 127.0.0.1:5001 且 HTTP handler 本身没有展示独立认证检查。因此「替换显示或音频硬件」「开放本地端口」都不只是 UI 问题,而是部署安全边界。

    一、谁在控制闭环之外

    源文档用一张关系图概括了这些服务的输入输出:selfdriveState / onroadEvents 同时喂给 ui + UIState 与 soundd;deviceState / pandaStates 喂给 ui 和 updated;athenad 的云端连接与 webrtcd 的本地流媒体相连;camerad / encoderd 向 webrtcd 供流;proclogd / journald / tombstoned 汇入日志与上传;GNSS 侧 qcomgpsd 或 pigeond + ubloxd 共同供 timed。

    这些进程的启停条件写在 process_config.py:updated 使用 only_offroad,webrtcd 使用直播或 notCar 条件,soundd 使用驾驶员视角条件。具体启停仍由 manager 动态判定,配置声明不等于恒久运行的保证。

    二、UI 与声音告警

    组件 关键入口 作用与失效边界
    渲染进程 ui.main() 选择大屏 MainLayout 或小屏 MiciMainLayout,每轮刷新 ui_state 并发布 uiDebug。
    UI 数据聚合 UIState.update() 非阻塞收取模型、车况、标定、设备与自驾状态;刷新设备插拔与屏幕管理。
    行车/驻车切换 _update_state()、_update_status() deviceState.started 与点火共同决定 UI 的 started;selfdriveState 决定已使能/覆盖显示。
    声音输入 Mic.callback()、Mic.update() 采集原始音频、估计近似 A 加权声压并发布 rawAudioData;主循环发布 soundPressure。
    声音输出 Soundd.soundd_thread() 按 selfdriveState.alertSound 播放告警,关键警报逐步升音量。
    失联警报 check_selfdrive_timeout_alert() 此前处于使能状态而 selfdriveState 短时失联时发出立即警告。

    三层职责必须分清:ui 是呈现层,不是使能状态的裁决者;soundd 不负责决定警告事件,只把上游状态转成音频;micd 发布的是未校准的近似声压,用于自适应音量,不应当作精密声级计。替换显示或音频硬件时,应保留视觉/声音告警时延、失联提醒和看门狗行为,不能因插件崩溃而静默隐藏关键警告。

    三、驻车更新路径

    入口 源码行为
    updated.main() DisableUpdates 时退出;加排他锁,建立 staging overlay,循环检查/拉取,失败记录并退避。
    WaitTimeHelper SIGHUP 触发立即拉取,SIGUSR1 只做检查;定时等待避免高频网络请求。
    Updater.check_for_update() 查询远端分支,比较目标分支与当前提交;目标分支还受设备类型映射影响。
    Updater.fetch_update() 将 staging overlay 标成不一致,fetch 目标分支并更新子模块,必要时更新 AGNOS 后 finalize。
    set_consistent_flag() 用 .overlay_consistent 表示已完成可切换镜像;拉取前清除,完成后由 finalize 路径设置。

    需要特别记住:更新代码里的 checkout --force、reset --hard、clean -xdff 是应用到 staging overlay 的安装逻辑,不应拿来操作本地研究目录。updated 只在驻车条件下由 manager 启动;按流量计费的网络会推迟普通拉取,但长时间未更新或用户请求仍可进入拉取路径。安全相关插件包应同样采用可回退、版本一致、签名与发布管控的更新路径,不能在行车中热替换控制算法。

    四、云端连接与 WebRTC

    athenad.main() 用设备 token 建立 WebSocket,失败后指数退避重连,守护进程跨 manager 重启;jsonrpc_handler() 从接收队列取请求,按 dispatcher 执行 RPC。startStream() 在驻车时设置 IsLiveStreaming 唤醒相关进程,再向本地 webrtcd 提交 SDP,默认回传车况和设备状态。

    webrtcd 默认绑定 127.0.0.1:5001,以 HTTP 提供 stream/schema/notify;handle_get_stream() 中新会话替换旧会话,已有连接时预热请求返回 busy。StreamSession.message_handler() 只接受本会话声明的传入服务类型,再转成 cereal 主题;notCar 直播路径可声明 testJoystick。

    这里必须写明风险边界:webrtcd 默认仅监听本机,但 HTTP handler 本身没有展示独立的认证检查,而 /stream 的请求体含可声明的传入桥接服务。因此本地访问权限、端口暴露和上游 RPC 授权是部署边界,不能把此类桥接列表视为通用车辆控制插件 API;车载产品必须限制网络暴露与可桥接主题。此处是基于静态路径的风险提示,并非端到端安全审计结论。

    五、时间、GNSS、调制解调器与诊断

    timed 发布 clocks,仅在 GNSS 消息新鲜、有定位且时间落入合理范围时调整系统时间,启动阶段无 GNSS 的行为需另行核对。pigeond 负责 u-blox 串口设备的供电、初始化与原始报文接收,硬件 GPIO 和波特率强耦合;ubloxd 做 UBX 帧同步与校验和解析;qcomgpsd 是另一条 GNSS/蜂窝模组诊断路径,与 ubloxd 的选择由设备节点决定。

    modem 管理蜂窝数据连接及状态机,路由、DNS 和失败重试需逐状态分析。proclogd 收集 CPU/内存/进程统计并发布 procLog;journald 将系统 journal 的 JSON 行映射为 operatingSystemLog;tombstoned 收集崩溃报告/栈信息,需另核对隐私与上传边界。这些是当前源码级功能入口,不代表已完成 GNSS 协议、蜂窝状态机或系统日志隐私审计。

    常见问题

    UI 或声音进程崩溃会静默隐藏关键告警吗?

    这正是需要防护的场景。源文档强调 ui 只是呈现层、不是使能裁决者,soundd 只把上游状态转成音频;替换显示或音频硬件时应保留视觉/声音告警时延、失联提醒和看门狗行为,不能因插件崩溃而静默隐藏关键警告。

    更新代码里的 reset –hard 会不会动到本地研究目录?

    不会,前提是它们只作用于 staging overlay。源文档特别说明 checkout --force、reset --hard、clean -xdff 是应用到 staging overlay 的安装逻辑,不应拿来操作本地研究目录;updated 也只在驻车条件下由 manager 启动。

    webrtcd 的桥接列表是通用车辆控制 API 吗?

    不是。StreamSession.message_handler() 只接受本会话声明的传入服务类型,但 HTTP handler 本身没有展示独立认证检查,本地端口暴露与上游 RPC 授权才是部署边界。车载产品必须限制网络暴露与可桥接主题,确保远程消息无法直达 sendcan 或修改 panda 安全模式。

    没有 GNSS 时 timed 会怎样调整系统时间?

    源文档只说明 timed 仅在 GNSS 消息新鲜、有定位且时间落入合理范围时才调整系统时间,并明确「需核对启动阶段无 GNSS 的行为」。因此无 GNSS 时的具体回退行为尚未在源文档给出结论,不能想当然地推断。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/OPERATIONS_SERVICES_DEEP_DIVE.md(取用了服务边界、UI 与声音告警、驻车更新、云端连接与 WebRTC、GNSS 与诊断的服务清单)。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • WebRTC 直播会话:Answer 失败后直播参数为何仍为真

    结论:直播会话最值得记录的不是「能否连上」,而是「参数真值与会话存在」会脱钩:驻车路径先写 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。

    延伸阅读

    参考资料

    • 项目 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

  • Athena 远程 RPC 与 WebRTC:设备 JWT 不等于逐方法授权

    结论:设备身份 JWT 只是建立 Athena 长连接的「门票」,不是本机每个 RPC 方法的授权。本地方法映射没有逐方法角色/权限检查,getMessage() 只要求服务名存在于 SERVICE_LIST;而 WebRTC 本机服务默认监听 127.0.0.1:5001,其 /stream 请求体可自选数据通道名单。因此远程 RPC 与直播应归入独立、非实时的服务域,并把 sendcan 等控制路径排除在通用数据通道之外。

    连接、调度与故障路径

    manager_thread() 在 DongleId 缺失或为 UnregisteredDevice 时,不启动 manage_athenad/uploader。athenad.main() 用本地私钥签发身份 JWT,作为 jwt cookie 连接 ATHENA_HOST(默认 wss://athena.comma.ai),断开后以带随机抖动的退避重连。网络会话的服务端授权规则不在本地仓库中。

    组件 关键函数 边界
    接收 ws_recv() 文本/二进制帧进入共享 recv_queue;收到 ping 写 LastAthenaPingTime,超过 70 秒无 ping 触发重连。
    RPC jsonrpc_handler()、rpc.handle() 以方法名查全局 dispatcher,异常转 JSON-RPC 错误;本地方法映射没有逐方法角色/权限检查,也不验证 jsonrpc 版本字段。
    发送 send_queue_push()、ws_send() 回复高优先级、日志转发低优先级;按 4096 字符分 WebSocket 帧。共享队列在重连时仍是进程级对象,不能当成精确一次的 RPC 事务。
    活性 ws_manage() 根据 IsOffroad 调整 TCP keepalive;仅是连接活性参数,不决定 RPC 授权范围。

    rpc.handle() 对未识别方法返回 METHOD_NOT_FOUND,参数类型错误返回 INVALID_PARAMS,普通异常返回 SERVER_ERROR;这些错误映射是协议处理,不是权限策略。连接建立后 handle_long_poll() 会启动接收、发送、连接管理、四个上传、日志转发和可配置数量的 RPC worker;worker 将请求原文写事件日志,敏感载荷需另行评估。上传生命周期还有几处状态语义:HTTP 200/201/401/403/412 均结束该队列项,不能把队列移除等同于远端成功存储;cancelUpload() 只标记队列内 ID,正在执行的传输并未由该函数直接中止。此外片段创建在非 PC 平台要求 IsOffroad。

    RPC 能力清单

    能力 关键入口 源码约束与注意点
    设备与网络查询 getVersion()、getPublicKey()、getSimInfo() 查询版本、设备公钥、SIM/网络元数据;公开公钥不同于公开私钥,但返回数据仍须考虑设备隐私。
    消息读取 getMessage() 可订阅任一注册服务并等待一条消息。该 RPC 是广范围读能力,不能直接授予通用第三方插件。
    日志枚举 listDataDirectory()、scan_dir() 从 Paths.log_root() 递归列路径并按前缀筛选;递归时不跟随目录符号链接。
    指定 URL 上传 uploadFilesToUrls()、_do_upload() 队列入口拒绝空、绝对或含 .. 的文件名,检查文件存在,随后向请求中的 URL 发 PUT;没有目的域名 allowlist。
    上传生命周期 upload_handler()、cancelUpload() 过期 31 天丢弃、计费网络可推迟/中断、特定网络错误重试至最多 30 次。队列移除不等于远端成功存储。
    本地代理 startLocalProxy() 仅允许 LOCAL_PORT_WHITELIST={22}(8022 兼容映射到 22),再桥到 127.0.0.1;白名单约束本地目标端口,不是对 remote_ws_uri 的域名校验。
    直播 startStream() 固定宽路摄像头、输出 carState/deviceState;仅在持久 CarParams.notCar 为真时声明入站 testJoystick。

    WebRTC 本机入口与数据桥

    webrtcd.main() 默认监听 127.0.0.1:5001,HTTP handler 提供 /stream、/schema、/notify,但在 WebrtcdHandler._dispatch_request() 中没有独立鉴权检查。不能据此推断外部网络可直接访问默认监听地址;本机其他进程、端口转发或修改 --host 后的暴露面需要单独控制。

    handle_get_stream() 只要求 JSON content type。会话时限很具体:生成 Answer 限时 30 秒,建立连接限时 15 秒,普通会话最长 300 秒;包含 testJoystick 的 body 会话走不限时等待断开的分支。无会话 5 秒后 schedule_teardown() 清除 IsLiveStreaming。

    数据桥的关键不对称在于:远端 Athena 的 startStream() 固定了服务名单,但直接调用本机 /stream 的请求体可自选名单;StreamSession.message_handler() 对入站普通消息仅按本会话 bridge_services_in 名单过滤,代码中没有按主题风险等级的统一许可层。所以必须区分「默认 Athena 调用约束」和「本机服务自身约束」。

    /schema 仅检验字段属于 log.Event 且非废弃字段,/notify 向当前会话广播提供的 JSON;CerealOutgoingMessageProxy.update() 把订阅主题转 JSON 发数据通道。handle_post_notify() 的假通道探针确认:通知按当前会话列表逐个广播,某通道发送异常被忽略,即使没有会话也返回 HTTP 200 OK——该返回值只代表本机 handler 完成遍历,不是远端客户端已收到通知的回执。

    自制硬件与插件化要求

    1. 将远程 RPC 与直播纳入独立、非实时服务域。插件注册清单需声明具体 RPC、日志文件和 cereal 主题读写权限;不能把设备身份 JWT 直接作为所有本机方法的全权令牌。
    2. 本机 /stream、/notify 和桥接写主题应由受信任调用方认证并用固定 allowlist 约束;尤其是 sendcan、carControl 等控制路径不能成为通用数据通道的可写目标。
    3. 上传能力应限制可访问目录的真实路径、目标 URL/域名与重定向策略,并把「服务端接受」「传输完成」「HTTP 拒绝」「重试耗尽」表示为不同状态。
    4. 验证矩阵至少包括:断网/重连时队列隔离,恶意 RPC 参数与超大消息,本机端口被非预期进程访问,symlink/路径边界,计费网络切换,会话竞争与清理,权限拒绝及行车状态切换。

    以上均为建议的重构验收,不是当前源码已经提供的保证;现有测试并未证明服务端授权、HTTP 本机访问控制、敏感主题限制、外发 URL 范围或实车会话时序。

    常见问题

    设备 JWT 等于本机方法的授权吗?

    不等于。JWT 只用于建立 Athena 长连接;rpc.handle() 以方法名查全局 dispatcher,本地方法映射没有逐方法角色/权限检查。因此不能把设备身份 JWT 当作所有本机方法的全权令牌。

    getMessage() 有按主题分级的权限吗?

    没有。它只要求服务名存在于 SERVICE_LIST,没有按主题敏感度进一步分级。这是广范围读能力,不能直接授予通用第三方插件。

    WebRTC 本机服务是默认对外开放的吗?

    不是。它默认监听 127.0.0.1:5001,且 _dispatch_request() 中没有独立鉴权检查。但本机其他进程、端口转发或修改 --host 后的暴露面需单独控制,不能据此断言对外可访问。

    为什么说 /stream 的名单约束与 Athena 的不是一回事?

    因为远端 Athena 的 startStream() 固定了服务名单,而直接调用本机 /stream 的请求体可自选名单;message_handler() 仅按本会话 bridge_services_in 过滤,没有按主题风险等级的统一许可层。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/REMOTE_RPC_WEBRTC_BOUNDARIES_DEEP_DIVE.md(取用了 Athena 连接与调度、RPC 能力清单、WebRTC 本机入口与数据桥、插件化要求)。
    • 项目 02 —— openpilot-research/DEVICE_IDENTITY_REGISTRATION_DEEP_DIVE.md(仅用于确认设备 JWT 的来源与注册前提)。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11