跳转至

流式传输

通过 WebSocket 流式传输浏览器视口,用于实时预览或“结对浏览”——人类可以在一旁观看并与 AI 智能体互动。

流式传输

每个会话都会自动在操作系统分配的端口上启动一个 WebSocket 流式服务器。该服务器流式传输视口帧,并接受输入事件(鼠标、键盘、触摸)。

要绑定到特定端口,请设置 AGENT_BROWSER_STREAM_PORT:

AGENT_BROWSER_STREAM_PORT=9223 agent-browser open example.com

你还可以在运行时管理流式传输:

agent-browser stream status            # Show streaming state and bound port
agent-browser stream enable --port 9223  # Re-enable on a specific port
agent-browser stream disable           # Stop streaming for the session

stream status 返回启用状态、活动端口、浏览器连接状态,以及截屏是否处于活动状态。stream disable 会拆除服务器并删除会话的 .stream 元数据文件。

当你需要保存为 WebM 或 MP4 产物而不是实时 WebSocket 流时,请使用 Video Recording。录制需要在 PATH 上有 ffmpeg。

运行时状态响应

agent-browser stream status --json 返回如下数据:

{
  "enabled": true,
  "port": 9223,
  "connected": true,
  "screencasting": true
}

connected 报告守护进程当前是否已附加浏览器。screencasting 报告是否正在为流式服务器积极生成帧。

与截屏命令的关系

stream enable 会创建 WebSocket 服务器,并在会话期间保持其可用。WebSocket 客户端随后会自动触发实时的帧投递。

更底层的 screencast_start 和 screencast_stop 命令仍然直接控制显式 CDP 截屏。当你需要截屏但又不需要 WebSocket 运行时服务器时,可以使用它们。

WebSocket 协议

连接到 ws://localhost:9223 以接收帧并发送输入。

帧编码由每个守护进程通过 AGENT_BROWSER_STREAM_QUALITY(0 到 100)、AGENT_BROWSER_STREAM_MAX_WIDTH 和 AGENT_BROWSER_STREAM_MAX_HEIGHT 设置。实时流请求 jpeg 格式。显式调用 screencast_start 会重新配置同一下层的截屏,因此客户端可能会在流中途看到格式变化。宽度和高度限制帧大小,并默认使用会话视口大小。在 1280x720 的繁忙页面上,质量 80 每帧约 54 KB,质量 20 约 25 KB,质量 20 在 640x360 下约 9 KB。

浏览器客户端必须从 localhost、127.0.0.1、::1 或 file:// 加载。任何其他来源在升级时都会收到 403,并需要通过代理。

帧消息

服务器发送包含 base64 编码图像的帧消息:

{
  "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 节流客户端会将其回显;它在浏览器重启后仍会持续递增,因此长期存活的客户端永远不会看到 id 回退。

metadata.timestamp 是以 Unix 纪元毫秒为单位的捕获时间。将其与绘制帧时的时钟进行比较,可以得出屏幕上内容的年龄,这是在受限链路上值得关注的数据。

URL 消息

在 Chrome 中,服务器会在活动标签页的主帧中针对整页文档导航、History API 导航和片段导航发送 URL 更新:

{
  "type": "url",
  "url": "https://example.com/dashboard#activity",
  "timestamp": 1785038682238
}

子帧或后台标签页内的导航不会发出 URL 消息,也不会替换活动标签页缓存的 URL。

状态消息

连接与截屏状态:

{
  "type": "status",
  "connected": true,
  "screencasting": true,
  "viewportWidth": 1280,
  "viewportHeight": 720
}

帧投递

帧以最新优先的方式投递:服务器只保留最新的帧,并在发送时读取它,因此当较早的帧仍在写入时所产生的每一帧都会被跳过而不是排队。应用程序永远不会形成积压。

在默认的推送节流下,底层传输仍可能如此:已由 socket 接受的帧会按顺序传送,因此完全停止读取的客户端会把写入器阻塞前内核缓冲的所有内容都排空。ack 节流消除了这个窗口(见下文)。状态、控制台和标签页消息通过一个独立的有序通道流动,因此它们不会像帧那样被较新的消息替换。它们并非无条件持久:客户端如果落后太多,可能会在该通道中滞后并丢失消息,因此请将控制台输出视为实时源,而不是审计日志。

输入处理独立于帧投递。每个连接在自己的任务中读取输入,因此即使正在向慢客户端写入大帧,点击和按键也会立即分派到浏览器。事件被发送到浏览器时无需等待其回复,因此在一连串鼠标移动之后点击仍然保持响应,并且顺序得到保留。

客户端配置

客户端可以随时发送 config 消息来限制自己的帧率:

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

maxFps 限制服务器向该客户端发送的每秒帧数(1 到 120)。设置为 0 可移除限制(默认情况)。每个客户端控制自己的速率;其他已连接的客户端不受影响。

Ack 节流

推送节流会在速率允许时立即将每一帧交给 socket,因此停滞的客户端会把传输层缓冲的内容排空。若要改为一次只获取一帧,可以选择 ack 节流:

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

服务器随后至多保持一帧在途,并等待客户端确认:

{
  "type": "ack",
  "seq": 41
}

每条 frame 消息都带有一个单调递增的 seq。请回显你已完成渲染的帧 id。当存在未确认的 ack 时,较新的帧会在服务器中相互替换,并且永远不会到达 socket,因此停滞十秒后恢复的客户端收到的是当前页面,而不是十秒的历史。这与 Chrome 使用 Page.screencastFrameAck 对其上游截屏进行节流的方式一致。

在 ack 节流模式下,同一时刻只有一个帧在途,因此速率是每个传输周期一个帧,再加上一次确认往返。链路的带宽和延迟都会限制它,而带宽延迟积超过单个帧的链路将得不到充分利用。这个余量就是新鲜性保证的代价。

Ack 节流只限制单跳。路径中存在代理时,请转发渲染器的 ack;在接收时生成的 ack 会让帧堆积在远端。

Ack 是累积的:确认更新的 id 会覆盖所有更早的 id,因此跳过多个 id 的客户端仍能解除交付阻塞。已选择开启却又停止确认的客户端只会停止接收帧,而 status、tabs、url 和 console 仍然持续流动。发送 {"type":"config","pacing":"push"} 即可恢复默认行为。

pacing 与 maxFps 相互独立且可组合:pacing 限制在途数据量,maxFps 限制速率。受限链路下的预览通常两者都需要。

config 消息无法覆盖连接建立时的首帧,因为服务器会在客户端连上后立即发送最新帧。若希望 pacing 从第一帧就开始生效,请在 URL 上声明:

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

稍后发送的 config 消息仍然优先。URL 上无法识别或无法解析的值会被忽略,而不会导致连接失败。

输入注入

发送输入事件即可远程控制浏览器。鼠标、键盘和触摸输入会重置守护进程的空闲计时器,因此受控的仪表盘或流式会话不会被默认超时关闭。

鼠标事件

// Click
{
  "type": "input_mouse",
  "eventType": "mousePressed",
  "x": 100,
  "y": 200,
  "button": "left",
  "clickCount": 1
}

// Release
{
  "type": "input_mouse",
  "eventType": "mouseReleased",
  "x": 100,
  "y": 200,
  "button": "left"
}

// Move
{
  "type": "input_mouse",
  "eventType": "mouseMoved",
  "x": 150,
  "y": 250
}

// Scroll
{
  "type": "input_mouse",
  "eventType": "mouseWheel",
  "x": 100,
  "y": 200,
  "deltaX": 0,
  "deltaY": 100
}

键盘事件

// Key down
{
  "type": "input_keyboard",
  "eventType": "keyDown",
  "key": "Enter",
  "code": "Enter"
}

// Key up
{
  "type": "input_keyboard",
  "eventType": "keyUp",
  "key": "Enter",
  "code": "Enter"
}

// Type character
{
  "type": "input_keyboard",
  "eventType": "char",
  "text": "a"
}

// With modifiers (1=Alt, 2=Ctrl, 4=Meta, 8=Shift)
{
  "type": "input_keyboard",
  "eventType": "keyDown",
  "key": "c",
  "code": "KeyC",
  "modifiers": 2
}

触摸事件

// Touch start
{
  "type": "input_touch",
  "eventType": "touchStart",
  "touchPoints": [{ "x": 100, "y": 200 }]
}

// Touch move
{
  "type": "input_touch",
  "eventType": "touchMove",
  "touchPoints": [{ "x": 150, "y": 250 }]
}

// Touch end
{
  "type": "input_touch",
  "eventType": "touchEnd",
  "touchPoints": []
}

// Multi-touch (pinch zoom)
{
  "type": "input_touch",
  "eventType": "touchStart",
  "touchPoints": [
    { "x": 100, "y": 200, "id": 0 },
    { "x": 200, "y": 200, "id": 1 }
  ]
}

编程接口

对于高级使用场景,可直接通过 TypeScript API 控制流式传输:

import { BrowserManager } from 'agent-browser';

const browser = new BrowserManager();
await browser.launch({ headless: true });
await browser.navigate('https://example.com');

// Start screencast with callback
await browser.startScreencast((frame) => {
  console.log('Frame:', frame.metadata.deviceWidth, 'x', frame.metadata.deviceHeight);
  // frame.data is base64-encoded image
}, {
  format: 'jpeg',  // or 'png'
  quality: 80,     // 0-100, jpeg only
  maxWidth: 1280,
  maxHeight: 720,
  everyNthFrame: 1
});

// Inject mouse event
await browser.injectMouseEvent({
  type: 'mousePressed',
  x: 100,
  y: 200,
  button: 'left',
  clickCount: 1
});

// Inject keyboard event
await browser.injectKeyboardEvent({
  type: 'keyDown',
  key: 'Enter',
  code: 'Enter'
});

// Inject touch event
await browser.injectTouchEvent({
  type: 'touchStart',
  touchPoints: [{ x: 100, y: 200 }]
});

// Check if screencasting
console.log('Active:', browser.isScreencasting());

// Stop screencast
await browser.stopScreencast();

使用场景

  • 配对浏览 - 人类实时观看并协助 AI 代理
  • 远程预览 - 在独立 UI 中查看浏览器输出
  • 录制 - 捕获帧以用于视频生成
  • 移动端测试 - 注入触摸事件以进行移动模拟
  • 无障碍测试 - 在自动化测试期间进行手动交互

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 agen/agent-browser