agent-browser 实时视口流(Live Streaming)协议与接入指南:WebSocket 推帧、远程输入与带宽控制
2026/9/19 8:09:31 网站建设 项目流程

agent-browser 实时视口流(Live Streaming)协议与接入指南:WebSocket 推帧、远程输入与带宽控制

【免费下载链接】agent-browserBrowser automation CLI for AI agents项目地址: https://gitcode.com/gh_mirrors/agen/agent-browser

agent-browser 的 Live Streaming 能力让浏览器视口以 JPEG 帧的形式通过 WebSocket 实时推送,并把鼠标、键盘、触摸事件回传驱动页面,这正是远程预览(remote preview)与嵌入式 Dashboard 所连接的后端:浏览器运行在 daemon 所在之处(沙箱、容器或 CI 机器),而客户端负责渲染帧并回传点击。读完本文,你将掌握如何启用流服务、如何连接并解析服务端消息、如何发送输入与控制消息,以及如何用 push/ack 两种节奏与 per-client 帧率上限在受限链路上稳定消费实时视口。

本文以 skill-data/core/references/streaming.md 为核心,并结合 cli/src/native/stream/ 下的 Rust 源码(mod.rswebsocket.rscdp_loop.rs)与相关测试,给出协议层面的事实依据。

流式传输解决什么问题

agent-browser 本身是面向 AI Agent 的浏览器自动化 CLI:浏览器与 daemon 运行在同一台机器上,Agent 通过命令操作页面。Live Streaming 把这一模型扩展到"浏览器与客户端分离"的场景——浏览器运行在 daemon 所在处(一个沙箱、一个容器或一台 CI 机器),客户端通过 WebSocket 连接ws://127.0.0.1:<port>,接收视口帧并在本地渲染,同时把点击和按键发送回去。远程预览与嵌入式 Dashboard 连接的就是这个流服务。

流的核心设计目标有两个:最新的帧永远优先,以及输入不被帧的写入阻塞。前者由"每客户端只保留最新一帧、发送时才读取"保证;后者由"输入在独立任务中派发、不等浏览器回复"保证(详见下文"消息与帧率"两节)。这与 skill-data/core/SKILL.md 中"浏览器跨命令保持运行"的会话模型一致:流服务由 daemon 承载,随会话生命周期启停。

启用流服务

流服务默认即可用——文档原话是 "Streaming is always available",daemon 启动时就会绑定一个由操作系统分配的 localhost 端口。通过以下命令管理:

agent-browser stream status --json # 报告启用状态、端口、客户端数 agent-browser stream enable # 创建流服务(--port 可指定端口) agent-browser stream disable # 拆除流服务
  • stream enable --port 9223可以固定端口;不带--port时由操作系统分配。
  • 重复enable会返回 "Streaming is already enabled for this session" 错误(见 cli/src/native/actions.rs),端口参数须在 0-65535 范围内。
  • stream disable会关闭服务器并清理会话的.stream元数据文件(cli/src/native/actions.rs)。
  • stream status --json返回结构化状态,源码中的字段包括enabledportconnected(浏览器连接是否存活)、screencasting(cli/src/native/actions.rs)。务必从这里读取端口,因为 OS 分配的默认端口对每个 daemon 都不同。

AGENT_BROWSER_STREAM_PORT环境变量可以在整个 daemon 生命周期内固定端口,而无需每次传--port。daemon 启动时读取该变量,缺省为0(表示由 OS 分配),见 cli/src/native/daemon.rs。启动时若指定端口被占用,daemon 会自动回退到 OS 分配端口;而运行期stream enable命令则不会回退,端口占用会直接报错(见 cli/src/native/stream/mod.rs 中allow_port_fallback参数的语义)。

帧编码参数

帧编码参数是 daemon 级(daemon-wide)的,只在启动时读取一次:

变量默认值说明
AGENT_BROWSER_STREAM_QUALITY80JPEG 质量,0 到 100,越界会被钳制(clamp)
AGENT_BROWSER_STREAM_MAX_WIDTH视口宽度限制帧的宽度上限,不会改变页面本身尺寸
AGENT_BROWSER_STREAM_MAX_HEIGHT视口高度同上

这些变量由ScreencastConfig::from_env()读取、parse()解析(cli/src/native/stream/mod.rs),解析规则值得注意:

  • quality先解析为i32,再用clamp(0, 100)钳制——CDP 对越界 quality 的行为是"接受但忽略",会让配置看起来没生效,所以这里选择主动钳制;
  • max_width/max_height必须能解析为u32且大于 0、不超过i32::MAX,否则被丢弃、保留默认(视口尺寸)。测试覆盖了"0""-100""wide""4294967295"等非法输入(cli/src/native/stream/mod.rs);
  • 维度默认为None是有意为之:注释明确说明max_width/max_height只是"覆盖值",None时使用会话视口,若默认成固定尺寸反而会把更大的视口全部缩小。

编码之所以固定 JPEG 且不提供格式开关,是因为frame消息本身没有 format 字段——daemon 级切换格式会让已连接的客户端"盲解"。不过文档也指出:显式的screencast_start会重新配置同一个底层 screencast,客户端中途仍可能看到格式变化,以字节嗅探(sniff the bytes)为准,不要假设格式(cli/src/native/stream/mod.rs)。

带宽量级参考(文档在 1280x720 的繁忙页面上实测):quality 80 约 54 KB/帧,quality 20 约 25 KB/帧,quality 20 且 640x360 时约 9 KB/帧。这直接决定了受限链路上的参数选择。

底层推流由 CDP 的Page.startScreencast驱动,参数为format: "jpeg"qualitymaxWidth/maxHeight(缺省用视口)、everyNthFrame: 1,见 cli/src/native/stream/cdp_loop.rs。从该源码还可以看到:只有engine == "chrome"的会话才支持 screencast(supports_screencast = is_chrome),非 Chrome 引擎(如 LightPanda)不会启动帧推流,status消息中的screencasting会是false

连接与来源校验

连接方式极简:WebSocket 客户端直接连ws://127.0.0.1:<port>没有订阅消息——客户端一旦接入,帧就开始自动投递(连接建立时服务器会先发送status、已知的tabs,并把最新一帧作为种子帧推给新客户端,见 cli/src/native/stream/websocket.rs)。

服务端对浏览器客户端有来源(Origin)白名单限制:

  • 只允许来自localhost127.0.0.1::1file://的页面连接;
  • 其他任何 Origin 会在 WebSocket 升级阶段收到403(错误信息 "Origin not allowed"),需要前置代理才能访问。

实现位于握手回调中对Origin头的校验(cli/src/native/stream/websocket.rs),白名单判定逻辑is_allowed_origin在 cli/src/native/stream/mod.rs。同时,HTTP 层的敏感端点(chat、models 等)也会按同一白名单反射 CORS 头,避免 API key 被任意网页读取(cli/src/native/stream/http.rs)。e2e 测试中有一个直接用Origin: https://evil.example发起的跨源命令请求被拒绝的用例(cli/src/native/e2e_tests.rs)。

服务端消息(Server → Client)

所有消息都是带type字段的 JSON 文本。

frame:视口图像及其元数据

帧是流的主体,按"最新优先"(latest-first)投递(机制见下一节)。示例:

{ "type": "frame", "seq": 41, "data": "<base64-encoded-jpeg>", "metadata": { "deviceWidth": 1280, "deviceHeight": 720, "pageScaleFactor": 1, "offsetTop": 0, "scrollOffsetX": 0, "scrollOffsetY": 0, "timestamp": 1785038682238 } }

字段含义:

字段说明
seq单调递增的帧 id;ack 节奏下被回显,且跨浏览器重启保持递增
database64 编码的 JPEG 图像
metadata.deviceWidth/deviceHeight视口尺寸
metadata.pageScaleFactor页面缩放因子
metadata.offsetTop/scrollOffsetX/scrollOffsetY视口相对页面内容的位置
metadata.timestamp捕获时刻的 epoch 毫秒,Date.now() - timestamp即该帧的"年龄"

seq的单调性由进程级原子计数器FRAME_SEQ保证(cli/src/native/stream/mod.rs),并有用例test_frame_ids_survive_a_stream_server_restart验证服务器重启后 id 仍递增(cli/src/native/stream/mod.rs)。timestamp由 CDP 的Network.TimeSinceEpoch(浮点秒)乘以 1000 转换而来,直接按整数读会得到 0(cli/src/native/stream/cdp_loop.rs 及对应测试)。

status/tabs/url/console

  • status:连接状态、screencasting 标志、视口尺寸、引擎、录制标志。连接时发送一次,之后每次状态变化再发。
  • tabs:当前标签页列表,连接时(若已知)和变化时发送。
  • url:仅在 Chrome 上发送,覆盖活动标签页主框架(main frame)的整页导航、History API 导航和 fragment 导航;子框架(child-frame)与后台标签页的导航会被忽略。实现上由Page.frameNavigatedPage.navigatedWithinDocument事件驱动,并先通过Page.getFrameTree确认主框架 id(cli/src/native/stream/cdp_loop.rs)。
  • console:控制台事件。

通道语义差异(重要)statustabsurlconsole有序通道——按序投递,且不会像帧那样被新消息顶替。但它们并非无条件可靠:客户端落后太多会从该通道掉队,丢失从未见过的消息。因此文档明确建议:把 console 输出当作实时流(live feed),而不是审计日志(audit log)。从源码看,有序消息走 tokiobroadcast通道(容量 64),帧走watch通道(只保留最新值)——这就是两种通道语义差异的来源(cli/src/native/stream/mod.rs)。

客户端消息(Client → Server)

客户端可发送以下消息:

{"type": "input_mouse", "eventType": "mousePressed", "x": 40, "y": 40, "button": "left", "clickCount": 1} {"type": "input_keyboard", "eventType": "keyDown", "key": "a", "text": "a"} {"type": "input_touch", "eventType": "touchStart", "touchPoints": []} {"type": "config", "maxFps": 10} {"type": "config", "pacing": "ack"} {"type": "ack", "seq": 41}

输入派发的底层行为

  • 输入在独立的任务中派发到浏览器,与帧投递互不阻塞——一次点击不会排队等在一帧之后。
  • 事件不等浏览器回复就发出(send_command_no_wait),所以一次点击不会卡在一串鼠标移动之后。
  • 顺序保持:press 永远不会越过 move。所有派发命令共享同一 socket 互斥锁、按调用顺序发送(cli/src/native/stream/websocket.rs)。测试test_input_dispatch_does_not_wait_for_a_cdp_reply用一个永不回复的模拟 CDP 服务器验证了"不等待回复"的行为(cli/src/native/stream/websocket.rs)。
  • 鼠标、键盘、触摸输入还会重置 daemon 的空闲计时器idle_activity.mark()),因此被持续远程驱动的预览不会被空闲超时关掉(cli/src/native/stream/websocket.rs)。注意:只有真正派发到 CDP 的输入才算活动。

input_mouse支持的事件类型包括mouseMoved/mousePressed/mouseReleased等(默认mouseMoved),input_keyboard支持keyDown/keyUp/char(默认keyDown),input_touch支持touchStart/touchMove/touchEnd等(默认touchStart)。键盘消息中省略的可选字符串字段(keycodetext)会被整体省略而不是发null——CDP 会因 null 字符串拒绝整个命令,导致按键静默丢失(cli/src/native/stream/websocket.rs)。

config:每客户端帧率上限

config消息设置每客户端(per-client)的帧率上限:

  • maxFps:1 到 120,0表示不限(默认值);
  • 立即生效,包括放宽上限的情况(测试test_loosening_cap_midstream_takes_effect_immediately验证了放宽后不再等待旧的节流期限,见 cli/src/native/stream/mod.rs);
  • 各客户端的上限互相独立,不影响其他连接;
  • 大于 120 的值钳制到 120(常量MAX_CONFIGURABLE_FPS = 120,见 cli/src/native/stream/websocket.rs);负数或非数字值被忽略,保持现有上限;两种情况都不会拒绝连接。

把设置放在 URL 上

两个设置也可以放在 URL 查询参数里,这是**唯一能覆盖连接首帧(opening frame)**的方式:

ws://127.0.0.1:<port>/?pacing=ack&maxFps=10

原因很直接:种子帧在config消息到达之前就已写入。连接建立后发送的config消息仍然优先(会覆盖 URL 上的设置)。URL 解析在 cli/src/native/stream/websocket.rs 的config_from_upgrade中实现,并有测试验证 URL 设置的 ack 节奏能覆盖首帧(cli/src/native/stream/mod.rs)。

帧率与陈旧帧处理

这是整个协议最有价值的部分:应用层永远不堆积陈旧帧

最新帧优先(latest-frame-wins)

服务器对每个客户端只保留最新一帧,并在发送时读取watch通道的borrow_and_update)。在一帧尚未写完时产生的新帧会被跳过而不是排队,因此应用永远不会构建积压(backlog)。测试test_new_client_receives_latest_frame_only验证新客户端只会收到最新帧而非历史帧(cli/src/native/stream/mod.rs)。

push 节奏(默认)

默认的 push 节奏在此处截止:底层传输(TCP/WebSocket)仍然有序——已经被 socket 接受的帧会按序送达,所以一个停顿的客户端会先排干内核缓冲区的数据,然后写线程才会阻塞。也就是说,push 模式无法避免"停顿后恢复要消化历史帧"的问题。

ack 节奏(推荐用于受限链路)

发送{"type":"config","pacing":"ack"}后,服务器同时最多只有一帧在途,等收到{"type":"ack","seq":N}才发下一帧:

  • 每一帧都带单调seq;客户端应回显自己渲染完成的那一帧的 seq;
  • 在 ack 未返回期间产生的新帧互相顶替,永远不会到达 socket——所以一个停顿 10 秒的客户端恢复后拿到的是当前页面,而不是 10 秒的历史(测试test_ack_pacing_holds_one_frame_and_skips_to_newest验证"ack 释放且只释放最新帧",见 cli/src/native/stream/mod.rs)。

ack 节奏的吞吐边界:一帧在途时,速率约等于"一次传输 + 一次确认往返",同时受带宽和延迟约束;若链路的带宽-延迟积超过单帧大小,链路就会利用不足。另有几点工程细节:

  • ack 只约束一跳(one hop):路径中有代理时,应转发渲染端生成的 ack;若代理在收到帧时就本地生成 ack,帧会堆积在对端。
  • ack 是累计的:确认更新的 id 覆盖所有更旧的帧。客户端乱序渲染或跳过中间 id 都不会卡死写线程(send_if_modified保证水位只前进不后退,见 cli/src/native/stream/websocket.rs)。
  • 客户端启用 ack 节奏后停止 ack,只是停止收帧statustabsurlconsole仍会继续流动。
  • 提前 ack 未来的 id 也不会卡死写线程(测试test_ack_ahead_of_the_stream_does_not_wedge_the_writer,cli/src/native/stream/mod.rs);切回 push 会立刻释放正在等 ack 的那帧(test_leaving_ack_pacing_releases_the_held_frame)。

两个设置如何组合

pacing限制在途量(in flight),maxFps限制速率(rate)。受限链路上的预览通常两者都要:

{"type": "config", "pacing": "ack", "maxFps": 10}

或直接在 URL 上声明:ws://127.0.0.1:<port>/?pacing=ack&maxFps=10

局限性(Limitations)

  • 仅限本机(localhost only)。把流暴露到机器之外是接入方(embedder)的职责——隧道、代理或端口转发;且 Origin 白名单只适用于浏览器客户端。
  • 帧是图像,不是视频编码。带宽随视口尺寸和页面活跃度增长,受限链路请用maxFps/quality 控制速率。
  • push 节奏下,服务器无法区分渲染慢与渲染快的客户端(只有传输背压这一条线索)。当这个区分很重要时,改用 ack 节奏。

相关资源

  • 命令全量参考:skill-data/core/references/commands.md;快速上手:skill-data/core/SKILL.md。
  • 流服务核心实现:cli/src/native/stream/mod.rs(服务器与配置)、cli/src/native/stream/websocket.rs(连接与消息处理)、cli/src/native/stream/cdp_loop.rs(CDP 事件到流消息的转换)。
  • 流命令(enable/disable/status)的入口与状态字段:cli/src/native/actions.rs。
  • 环境变量完整清单(含AGENT_BROWSER_STREAM_PORT/QUALITY/MAX_WIDTH/MAX_HEIGHT):cli/src/output.rs。
  • 基于该流构建的嵌入式 Dashboard 位于 packages/dashboard/,其启动与--allowed-origins配置说明见 skill-data/core/SKILL.md。

【免费下载链接】agent-browserBrowser automation CLI for AI agents项目地址: https://gitcode.com/gh_mirrors/agen/agent-browser

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询