使用 Postgres 的视频会议

发布: (2026年2月27日 GMT+8 08:00)
10 分钟阅读

Source: PlanetScale Blog

请提供您想要翻译的具体文本内容,我将为您翻译成简体中文。

昨天在 X 上,SpacetimeDB 推特称他们已经完成了**“全球首个通过数据库进行的视频通话”**,并以自己的方式邀请其他人尝试。

致敬他们——这是个很酷的想法!

简而言之,他们构建了一个前端,实现了:

  1. 从浏览器的媒体 API 捕获音频和视频。
  2. 将其编码为紧凑的帧(PCM‑16LE 音频,JPEG 视频)。
  3. 将帧发送到充当实时消息中介的数据库。
  4. 将帧流式回传给另一位参与者的浏览器进行播放。

实现已开源(SpaceChatDB on GitHub)。
我决定看看使用PostgreSQL——全球最流行的开源数据库——来托管全球第二个通过数据库进行的视频通话会是什么样子。

工作原理

架构

  • 前端 – 使用 SvelteKit 的应用,捕获媒体。
  • 中继 – 小型 Node.js WebSocket 服务器(pg‑relay),验证调用并将帧写入 PostgreSQL。
  • 数据库 – $5 PlanetScale PostgreSQL 实例,存储帧并提供逻辑复制流。

呼叫流程(视频)

  1. 捕获 – 浏览器捕获摄像头帧,将其编码为 JPEG,并以二进制 WebSocket 消息发送到 pg‑relay

  2. 插入pg‑relay 验证调用并运行:

    INSERT INTO video_frames (
        session_id, from_id, to_id, seq, width, height, jpeg
    ) VALUES ($1, $2, $3, $4, $5, $6, $7);
  3. WAL – PostgreSQL 将该行写入 WAL(预写日志)。

  4. 复制pg‑relay 还在同一数据库上运行逻辑复制消费者。当新行出现在复制流中时,它:

    • 检查 to_id 列。
    • 将原始 JPEG 字节通过 WebSocket 转发给接收方。
  5. 播放 – 接收方的浏览器从 JPEG 创建 Blob URL 并进行渲染。

音频 采用相同的模式,使用 audio_frames 表。

逻辑复制?

PostgreSQL 的逻辑复制为我们提供了一个 可靠、有序的变更流

  • 对发布中每个表的 INSERT、UPDATE 和 DELETE 事件都按提交顺序传递。
  • 无需使用 SELECT 语句以足够快的频率轮询表来渲染 15 fps 视频。

由于相同的机制用于推送视频帧,它同样可以推送:

  • 聊天消息。
  • 用户在线状态变化。
  • 通话状态转换(例如,当用户断开连接时的行 DELETE)。

视频表模式

CREATE TABLE video_frames (
    id          BIGSERIAL   PRIMARY KEY,
    session_id  UUID        NOT NULL,
    from_id     TEXT        NOT NULL,
    to_id       TEXT        NOT NULL,
    seq         INT         NOT NULL,
    width       SMALLINT    NOT NULL,
    height      SMALLINT    NOT NULL,
    jpeg        BYTEA       NOT NULL,
    inserted_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

这张表没有什么特别之处——只是一行行在 BYTEA 列中存放 JPEG 的记录。
输出量适中,完全在像 PostgreSQL 这样的数据库能够处理的范围内。

Media → PostgreSQL pipeline

捕获端

  • Video – 浏览器将摄像头帧绘制到离屏 canvas,调用 canvas.toBlob() 获取 JPEG。
  • AudioAudioWorkletNode 收集 PCM 采样,重采样为 16 kHz 单声道,并将其编码为 16 位小端整数。
  • 两个负载被打包进带有小型 JSON 头部(session_idseqrecipient)的二进制 WebSocket 帧,并发送到中继。

播放端

  • Video – 接收到的 JPEG 被转换为 Blob URL,并设置为 <video> 标签的 src
  • Audio – 采样被解码回浮点数,并在带有小抖动缓冲区的 AudioBufferSourceNode 上调度播放。

整个系统以 640 × 360 @ 15 fps、JPEG 质量 0.65 运行。
每帧大约 25–40 KB,相当于每个方向 ≈ 375–600 KB/s 的视频带宽。

累积行

在 15 fps 时,一个通话大约会产生 108 000 行每小时
为避免无限增长,清理任务每 2 秒运行一次,删除超过 5 秒的帧:

DELETE FROM audio_frames
WHERE inserted_at = NOW() - INTERVAL '5 seconds'
GROUP BY from_id
ORDER BY frames_5s DESC;

结果(示例):

from_idframes_5sapprox_fps
06cad97a128947a58e8ff754ec1171d4200c4d774b5c43c9b7637f11bba610367615.2
4f984feaee1042939383b0fffb3f1fc172d28aa92b654551a02c6187880219957615.2

看看——我们只花 5 美元的 PostgreSQL 正在双向流式传输 15 fps 视频!

持久化优势

  • 数据是崩溃安全的、可复制的,并且以后可以查询。
  • 可以存储数小时的视频,以便后续分析或回放。

我甚至可以直接从数据库中提取单帧并在终端渲染(例如,使用 catimg 或 ANSI‑image 查看器)。

我们还能用别的方式吗?

LISTEN/NOTIFY

PostgreSQL 内置的 pub/sub(LISTEN/NOTIFY)看起来很有吸引力:

  • 不需要额外的表——只需通过通知通道直接发送 JPEG 字节。

问题: 有效负载限制为 8 KB
一张 640 × 360 的 JPEG(25–40 KB)需要拆分成 4–5 条通知,导致必须:

  • 实现分块与重组逻辑。
  • 提供顺序保证。
  • 处理丢失的块。

这实际上在 NOTIFY 之上重新创建了类似 TCP 的协议,增加了不必要的复杂度。音频帧通常能装入 8 KB 以下,所以可以采用混合方案,但混用传输机制会破坏我想要的简洁性。

未记录表

另一种选择是使用 未记录表(unlogged tables)来存放入站数据:

  • 未记录表跳过 WAL,消除 fsync 开销。
  • 对写密集型工作负载更快,但牺牲了持久性(崩溃时数据会丢失)。

在本示例中,持久性是必须的特性,因此我保留了常规(已记录)表。

Summary

  • PostgreSQL 的逻辑复制 提供了干净、有序且可靠的变更流,可以取代轮询实现实时媒体传输。
  • 通过将数据库视为消息中间件,我们可以构建一个功能完整的视频通话系统,只需 最小的基础设施(一个小型 Node.js 中继和一个廉价的 PostgreSQL 实例)。
  • 这种方法还能为后续分析提供 持久化、可查询的视频帧,而传统媒体服务器通常不具备此功能。

欢迎探索源代码并将该模式适配到其他实时使用场景!

崩溃恢复

插入更快,因为 PostgreSQL 并未对视频帧提供持久性保证。

我不喜欢这样,因为逻辑复制是从 WAL 中读取的。如果表不写入 WAL,它就不会出现在复制流中。要实现这一点,我们必须回退到轮询:

SELECT * FROM video_frames WHERE seq > $1;

…在循环中。这本来可能运行良好——甚至更好——但从 SELECT * 的轮询循环渲染视频的感觉总是不对劲。

结果如何?
由你来评判。 超出了我的预期。

我们的 $5 PlanetScale PostgreSQL 能够跟上实时视频和音频的插入速率,且浏览器的优化足以将原始 JPEG 帧转换为相当逼真的视频。

音视频同步微调

第一次成功运行后,我唯一的调整是添加一些边界以保持音频同步。视频帧即时渲染(我们只需交换图像),但音频需要提前缓冲和调度以避免间隙。要让它们保持同步,需要限制音频调度缓冲区,使其不会偏离实际时间太远:

const now = audioCtx.currentTime;
const clamped = nextPlayTime > now + 0.15 ? now + 0.02 : nextPlayTime;
const startAt = Math.max(clamped, now + 0.02);

你应该这么做吗?

不! 使用 WebRTC!

但如果你想了解逻辑复制的工作原理,并看看 PostgreSQL 作为通用实时后端能走多远,这是一种有趣的方式。整个中继服务器大约只有 400 行 TypeScript

我的分支:

要是亚历山大·格雷厄姆·贝尔能看到我们现在的样子就好了。

0 浏览
Back to Blog

相关文章

阅读更多 »