Postgres와 비디오 회의
I’m happy to translate the article for you, but I’ll need the full text you’d like translated. Could you please paste the content (excluding the source line you already provided) here? Once I have it, I’ll translate it into Korean while preserving the original formatting, markdown, and any code blocks or URLs.
어제 X에서, SpacetimeDB는 **“세계 최초 데이터베이스를 통한 영상 통화”**를 구현했다고 트윗했고, 그들만의 방식으로 누구든지 시도해 보라고 초대했습니다.
그들에게 감사 – 멋진 아이디어입니다!
간단히 말해, 그들은 프론트엔드를 만들었습니다:
- 브라우저의 미디어 API에서 오디오와 비디오를 캡처합니다.
- 이를 압축된 프레임(PCM‑16LE 오디오, JPEG 비디오)으로 인코딩합니다.
- 프레임을 실시간 메시지 브로커 역할을 하는 데이터베이스에 전송합니다.
- 다른 참가자의 브라우저로 프레임을 스트리밍하여 재생합니다.
구현은 오픈소스로 공개되었습니다(SpaceChatDB on GitHub).
저는 세계에서 가장 인기 있는 오픈소스 데이터베이스인 PostgreSQL을 사용해 데이터베이스를 통한 세계 두 번째 영상 통화를 호스팅해 보면 어떨까 싶었습니다.
작동 방식
아키텍처
- 프론트엔드 – 미디어를 캡처하는 SvelteKit 앱.
- 릴레이 – 호출을 검증하고 프레임을 PostgreSQL에 기록하는 작은 Node.js WebSocket 서버 (
pg‑relay). - 데이터베이스 – 프레임을 저장하고 논리 복제 스트림을 제공하는 $5 PlanetScale PostgreSQL 인스턴스.
호출 흐름 (비디오)
-
캡처 – 브라우저가 카메라 프레임을 캡처하고 JPEG로 인코딩한 뒤
pg‑relay에 바이너리 WebSocket 메시지로 전송합니다. -
삽입 –
pg‑relay가 호출을 검증하고 다음을 실행합니다:INSERT INTO video_frames ( session_id, from_id, to_id, seq, width, height, jpeg ) VALUES ($1, $2, $3, $4, $5, $6, $7); -
WAL – PostgreSQL이 행을 WAL(Write‑Ahead Log)에 기록합니다.
-
복제 –
pg‑relay는 동일한 데이터베이스에서 논리 복제 컨슈머도 실행합니다. 새로운 행이 복제 스트림에 나타나면:to_id열을 확인합니다.- 원시 JPEG 바이트를 WebSocket을 통해 수신자에게 전달합니다.
-
재생 – 수신자의 브라우저가 JPEG에서 Blob URL을 생성하고 이를 렌더링합니다.
오디오는 동일한 패턴을 따르며, audio_frames 테이블을 사용합니다.
논리적 복제?
PostgreSQL의 논리적 복제는 신뢰할 수 있고 순서가 보장된 변경 스트림을 제공합니다:
- 게시물에 포함된 모든 테이블에 대한 INSERT, UPDATE, DELETE 이벤트가 커밋 순서대로 전달됩니다.
- 15 fps 비디오를 만들 정도로 충분히 빠르게
SELECT문으로 테이블을 폴링할 필요가 없습니다.
같은 메커니즘이 비디오 프레임을 푸시하듯이, 다음과 같은 것도 푸시할 수 있습니다:
- 채팅 메시지.
- 사용자 존재 상태 변화.
- 통화 상태 전환(예: 사용자가 연결을 끊을 때 행 DELETE).
Video table schema
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 파이프라인
캡처 측
- Video – 브라우저가 카메라 프레임을 오프스크린 캔버스에 그린 뒤
canvas.toBlob()을 호출해 JPEG를 얻습니다. - Audio –
AudioWorkletNode가 PCM 샘플을 수집하고 16 kHz 모노로 리샘플링한 뒤 16‑bit 리틀‑엔디언 정수 형태로 인코딩합니다. - 두 페이로드는 작은 JSON 헤더(
session_id,seq,recipient)와 함께 바이너리 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_id | frames_5s | approx_fps |
|---|---|---|
| 06cad97a128947a58e8ff754ec1171d4200c4d774b5c43c9b7637f11bba61036 | 76 | 15.2 |
| 4f984feaee1042939383b0fffb3f1fc172d28aa92b654551a02c618788021995 | 76 | 15.2 |
보세요 – 우리 $5짜리 PostgreSQL이 양방향 15 fps 비디오를 스트리밍하고 있습니다!
Persistence benefits
Because each JPEG is durably persisted:
- The data is crash‑safe, replicable, and queryable later.
- Hours of video can be stored for later analysis or replay.
I can even pull a single frame directly from the database and render it in the terminal (e.g., using catimg or an ANSI‑image viewer).
다른 방법으로 할 수 있었을까?
LISTEN/NOTIFY
PostgreSQL의 내장 pub/sub(LISTEN/NOTIFY)가 매력적으로 보였습니다:
- 별도의 테이블이 필요 없으며, JPEG 바이트를 알림 채널을 통해 바로 전송하면 됩니다.
문제: 페이로드 제한이 8 KB라는 점입니다.
640 × 360 JPEG(25–40 KB)는 4–5개의 알림으로 나누어야 하며, 이 경우 다음이 필요합니다:
- 청크화 및 재조립 로직
- 순서 보장
- 누락된 청크 처리
실질적으로 NOTIFY 위에 TCP와 같은 프로토콜을 다시 구현하는 것이 되어, 불필요한 복잡성이 추가됩니다. 오디오 프레임은 보통 8 KB 이하이므로 하이브리드 접근이 가능하지만, 전송 메커니즘을 혼합하면 원래 원했던 단순성을 잃게 됩니다.
Unlogged tables
또 다른 대안은 unlogged 테이블을 사용해 들어오는 데이터를 저장하는 것입니다:
- Unlogged 테이블은 WAL을 건너뛰어 fsync 오버헤드를 없앱니다.
- 쓰기‑집중 워크로드에서는 더 빠르지만, 내구성이 희생됩니다(충돌 시 데이터가 손실됨).
이 데모에서는 내구성이 중요한 기능이므로, 일반(로그가 남는) 테이블을 유지했습니다.
요약
- PostgreSQL의 논리 복제는 실시간 미디어 전송을 위한 폴링을 대체할 수 있는 깨끗하고, 순서가 보장되며, 신뢰할 수 있는 변경 스트림을 제공합니다.
- 데이터베이스를 메시지 브로커로 활용함으로써 최소한의 인프라(작은 Node.js 릴레이와 저렴한 PostgreSQL 인스턴스)만으로 완전한 기능을 갖춘 영상 통화 시스템을 구축할 수 있습니다.
- 이 접근 방식은 또한 전통적인 미디어 서버가 기본적으로 제공하지 않는, 나중에 분석할 수 있는 지속적이고 쿼리 가능한 비디오 프레임을 제공합니다.
소스 코드를 자유롭게 살펴보고 이 패턴을 다른 실시간 사용 사례에 적용해 보세요!
Crash Recovery
삽입이 더 빠른 이유는 PostgreSQL이 비디오 프레임에 대한 내구성 보장을 하지 않기 때문입니다.
이 점이 마음에 들지 않았던 이유는 논리 복제가 WAL에서 읽어오기 때문입니다. 테이블이 WAL에 기록되지 않으면 복제 스트림에 나타나지 않습니다. 이를 작동시키려면 폴링으로 되돌아가야 합니다:
SELECT * FROM video_frames WHERE seq > $1;
…루프 안에서. 이것도 충분히 잘 동작했을 수도 있고, 어쩌면 더 좋았을 수도 있지만 SELECT * 폴링 루프에서 비디오를 렌더링하는 느낌이 별로 였습니다.
How’d it go?
당신이 판단하세요. 기대 이상이었습니다.
우리의 $5 PlanetScale PostgreSQL은 실시간 비디오와 오디오의 삽입 속도를 따라잡을 수 있었고, 브라우저는 원시 JPEG 프레임을 받아 비디오로 변환하는 데 충분히 최적화되어 있었습니다.
Audio‑Video Sync Tweaks
첫 번째 성공적인 실행 이후에 제가 한 유일한 조정은 오디오를 동기화하기 위한 경계값을 추가한 것입니다. 비디오 프레임은 즉시 렌더링됩니다(이미지를 교체하기만 하면 되지만), 오디오는 버퍼링하고 미리 스케줄링해야 끊김을 방지할 수 있습니다. 오디오가 실시간에서 너무 멀리 앞서지 않도록 스케줄링 버퍼를 제한해야 동기화를 유지할 수 있었습니다:
const now = audioCtx.currentTime;
const clamped = nextPlayTime > now + 0.15 ? now + 0.02 : nextPlayTime;
const startAt = Math.max(clamped, now + 0.02);
Should You Do This?
No! WebRTC를 사용하세요!
하지만 논리 복제가 어떻게 작동하는지 이해하고 PostgreSQL을 일반 목적의 실시간 백엔드로 얼마나 밀어붙일 수 있는지 확인하고 싶다면, 이것은 재미있는 방법입니다. 전체 릴레이 서버는 약 400줄의 TypeScript로 구성되어 있습니다.
My fork:
만약 알렉산더 그레이엄 벨이 지금 우리를 본다면.