실시간 전사 결과는 두 번 도착합니다. 먼저는 계속 바뀌는 초안으로, 그다음에는 더 이상 바뀌지 않는 확정 줄로 도착합니다. 초안은 교체되는 끝부분으로, 확정 줄은 지나간 기록으로 그리면, 실시간 자막 화면의 나머지 부분은 연결 작업에 가깝습니다.
실시간 세션은 VocalStack API에서 가장 작은 단위입니다. 서버에서 HTTP 호출을 한 번 하면 서명된 streamUrl을 받습니다. 클라이언트는 이 URL로 WebSocket을 열고, 캡처되는 원시 오디오를 그때그때 보내며, 전사 결과는 발화자보다 조금 뒤처진 시점에 JSON 텍스트 프레임으로 받습니다. 오디오가 끝나면 eof를 보내고, 서버는 남은 내용을 비우고 세션이 측정한 값을 알려준 뒤 연결을 닫습니다.
아무것도 저장되지 않습니다. 전사 결과는 전송 중에만 존재하며, 이는 자막 화면, 회의 오버레이, 앱 안의 음성 기능에 필요한 방식입니다. 단어를 나중에도 보관하고 공유하고 내보내고 싶다면 실시간 전사와 방송을 사용하는 편이 맞습니다. 마지막 절에서 그 방법을 안내합니다.
대부분의 스트리밍 음성 API가 이 구조를 따릅니다. HTTP로 자격 증명을 주고받고, 오디오는 들어가고 JSON은 나오는 장기 소켓을 쓰며, 임시 텍스트와 확정 텍스트를 구분하는 방법이 있습니다. API마다 이름은 다르지만 실제로 움직이는 부분은 거의 같습니다. 여기서는 순서대로 정리합니다:
POST /v1/realtime/sessions를 호출합니다. 응답은 { sessionId, streamUrl, expiresAt }입니다.streamUrl로 WebSocket을 엽니다.session.ready가 올 때까지 session.loading 하트비트를 기다립니다.transcript.draft(끝부분 교체)와 transcript.committed(확정 줄 추가)를 읽습니다.{"type":"eof"}를 보내고 session.done을 읽습니다.1단계에서만 키가 필요하므로, 그 부분만 서버에서 실행됩니다.
Developers, API keys에서 Write 권한을 가진 키를 만듭니다. 키는 서버와 스크립트를 위한 것입니다. API는 웹 페이지에서 보내졌거나 URL에 담긴 키를 거부하므로, 세션은 여러분이 만든 라우트 뒤에서 생성하고, 페이지는 streamUrl만 보게 합니다.
URL에는 자체 토큰이 포함됩니다. 이는 일반적으로 알아둘 가치가 있는 브라우저의 제약 때문입니다. 브라우저의 WebSocket 생성자는 요청 헤더를 설정할 수 없으므로 키를 넣을 Authorization 헤더가 없습니다. 그래서 브라우저를 대상으로 하는 음성 API는 모두 짧은 수명의 토큰을 URL에 넣거나(또는 서브프로토콜 이름에 숨겨) 소켓을 인증합니다. 수명이 짧다는 점이 핵심입니다. URL은 로그와 기록에 남기 쉬우므로, 그 안에 담긴 값은 반드시 만료되어야 합니다. expiresAt 이전에 연결하세요.
SDK는 패키지 레지스트리가 아니라 Developers 영역에서 내려받습니다. 빠른 시작 가이드의 설치 섹션에 이 환경에 맞는 명령이 나와 있습니다. TypeScript SDK를 쓰면 서버 라우트는 다음과 같습니다:
// On your server, behind a route of yours (POST /api/transcribe, say):
import { VocalStack } from '@vocalstack/sdk';
const vs = new VocalStack({ baseUrl: '{{api}}', apiKey: process.env.VOCALSTACK_API_KEY });
export async function createStream(): Promise<{ sessionId: string; streamUrl: string }> {
const { sessionId, streamUrl } = await vs.realtime.createSession({ spokenLanguage: 'en' });
return { sessionId, streamUrl };
}
SDK가 없어도 fetch 한 번이면 됩니다. 아래 예시는 스페인어를 요청하고 문장 조립을 끄는데, 뒤에 나오는 와이어 샘플과 맞춘 것입니다:
const res = await fetch('{{api}}/v1/realtime/sessions', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.VOCALSTACK_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ spokenLanguage: 'en', translate: ['es'], sentences: { enabled: false } }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`); // the error envelope: { code, message, … }
const { streamUrl } = (await res.json()) as { sessionId: string; streamUrl: string; expiresAt: string };
모든 옵션에는 기본값이 있습니다. 처음부터 알아둘 만한 옵션은 spokenLanguage(생략하면 언어를 자동으로 감지합니다), translate(번역 대상 언어 목록), speakerLabels, latencyMode입니다. 나머지는 아래에서 필요할 때 설명합니다.
streamUrl을 오디오를 캡처하는 쪽에 넘기세요. 이제부터는 어떤 키도 필요하지 않습니다.
streamUrl에 헤더 없이 그대로 연결합니다. 세션이 시작되는 동안 서버는 session.loading 하트비트를 보냅니다. 그다음 session.ready가 옵니다. 그 전에는 오디오를 보내지 마세요. 준비되기 전에 보낸 오디오는 나중을 위해 보관되지 않습니다. 서버는 이를 오류로 보고 error 프레임을 보낸 뒤 소켓을 닫습니다.
소켓이 열린 뒤에는 모든 실패가 해당 프레임의 형태를 따릅니다: {"type":"error","code":"…","message":"…","retryable":true|false}. code는 정해진 목록에서만 나오며(invalid_audio, invalid_options, invalid_token, capacity_exceeded, session_expired, slow_consumer, engine_unavailable, internal), message는 로그용이고, retryable은 새 세션을 시도해 볼 가치가 있는지 알려줍니다. 첫 프레임부터 처리하세요:
const ws = new WebSocket(streamUrl); // the token is in the URL: no header
ws.binaryType = 'arraybuffer';
ws.addEventListener('message', (m: MessageEvent) => {
if (typeof m.data !== 'string') return;
const f = JSON.parse(m.data);
switch (f.type) {
case 'session.loading':
console.error('starting…');
break;
case 'session.ready':
startMicrophone(); // send audio only after session.ready
break;
case 'error':
console.error(`${f.code}: ${f.message}`);
if (f.retryable) scheduleReconnect(); // a new session, with backoff; see "Stop"
break;
}
});
직접 구현하는 클라이언트는 두 가지를 더 처리해야 합니다(SDK는 이미 처리합니다). 첫째, 거부된 업그레이드 요청에는 API의 일반적인 HTTP 오류 형식이 담깁니다. 401 invalid_token, 410 session_expired, 또는 지켜야 할 Retry-After 헤더가 붙은 503 unavailable입니다.
둘째, 포화 상태는 오류가 아니라 침묵처럼 보입니다. 30초 동안 프레임이 하나도 도착하지 않으면 capacity_exceeded로 간주하고 다시 연결하세요. 새 세션은 보통 통과합니다. 로딩 프레임은 계속 오는데 session.ready가 끝내 오지 않으면 재시도 가능한 engine_unavailable로 간주합니다. SDK에서는 RealtimeSession.connect가 session.ready 시점에 결과를 반환하고, readyDeadlineMs가 지나면 올바른 코드와 함께 실패합니다.
session.ready가 도착하면 서버는 사람이 말하는 속도에 맞춰 오디오가 들어오기를 기대합니다.
오디오는 원시 PCM의 바이너리 프레임으로 보냅니다. 32비트 부동소수점, 리틀 엔디언, 16 kHz, 모노(pcm_f32le, 세션의 기본 audio 형식)입니다. 캡처되는 대로 약 100~250 ms 단위의 청크로 보내세요. 청크 사이에 전사 결과를 기다릴 필요는 없습니다.
바이트 계산은 한 번 해볼 만합니다. 16,000 샘플이 초당 4바이트이므로 초당 64,000바이트입니다. 따라서 100 ms는 6,400바이트, 250 ms는 16,000바이트입니다. 음성 모델은 보통 16 kHz 모노로 학습되어 있어서 많은 실시간 API가 이 형식을 요구합니다. 반면 마이크와 브라우저는 44.1 kHz나 48 kHz로, 종종 스테레오로 캡처합니다. 바이트를 보내기 전에 리샘플링하고 채널을 하나로 합치는 작업이 필요하며, 여기서는 ffmpeg가 맡고, 브라우저에서는 SDK의 마이크 헬퍼가 맡습니다. 48 kHz 오디오를 보내면서 16 kHz라고 선언해도 실패하지는 않습니다. 대신 슬로모션처럼 늘어진 음성을 받아 적게 되어 결과의 품질이 떨어집니다.
Node에서는 ffmpeg가 가장 간단한 캡처 장치입니다:
import { spawn } from 'node:child_process';
function startMicrophone(): void {
// Raw PCM: f32le, 16 kHz, mono, as binary frames. macOS: -f avfoundation -i :0, Linux: -f pulse -i default.
const mic = spawn('ffmpeg', ['-loglevel', 'error', '-f', 'avfoundation', '-i', ':0', '-f', 'f32le', '-ac', '1', '-ar', '16000', '-']);
mic.stdout.on('data', (chunk: Buffer) => {
// Backpressure: bytes the socket has accepted but not yet put on the wire.
// Two seconds of audio (128,000 bytes) queued means the link, not the server, is behind.
if (ws.bufferedAmount > 128_000) console.error(`link behind by ${ws.bufferedAmount} bytes`);
ws.send(new Uint8Array(chunk));
});
mic.on('close', () => ws.send(JSON.stringify({ type: 'eof' })));
process.once('SIGINT', () => mic.kill());
}
ws.send는 블로킹되지 않고 큐에 쌓아 둡니다. bufferedAmount는 그 큐의 크기이며, 정상적인 링크에서는 0 근처에 머뭅니다. 라이브 오디오는 멈출 수 없으므로, 큐가 늘어날 때 유용한 대응은 프레임을 버리는 것이 아니라 그 상황을 알아채는 것입니다. 서버도 역압(백프레셔)을 적용합니다. 실시간보다 몇 초 앞선 오디오까지만 받고(limits.audioBufferS), 따라잡을 때까지 읽기를 멈춥니다. 반대 방향에서는 확정 결과가 유실 없이 큐에 쌓이고 초안은 하나로 합쳐져서, 느린 리더는 더 적고 더 최신인 초안을 보게 됩니다. 확정 결과조차 따라잡지 못하는 클라이언트는 slow_consumer와 retryable: false가 담긴 error 프레임을 받고 연결이 끊깁니다. 이 오류가 보인다면 메시지 핸들러가 너무 많은 일을 하고 있다는 뜻입니다.
브라우저에서는 @vocalstack/sdk/mic의 streamMicrophone이 마이크 권한을 요청하고, 16 kHz로 리샘플링하며, 브라우저가 탭을 제한할 때 서버가 공백을 표시할 수 있도록 clock 프레임과 함께 청크를 보냅니다. 페이지는 라우트에서 세션을 받으며 키는 전혀 보지 않습니다:
import { RealtimeSession } from '@vocalstack/sdk';
import { streamMicrophone } from '@vocalstack/sdk/mic';
export async function transcribeMicrophone(
stream: { sessionId: string; streamUrl: string },
render: (committed: string, tail: string) => void,
): Promise<void> {
const session = await RealtimeSession.connect(stream);
const mic = await streamMicrophone(session); // asks for the microphone, sends 16 kHz PCM
document.querySelector('#stop')?.addEventListener('click', () => {
void mic.stop();
session.eof();
});
let committed = '';
for await (const ev of session) {
if (ev.type === 'transcript.draft') render(committed, ev.segments.map((s) => s.text).join(' '));
if (ev.type === 'transcript.committed') {
committed += `${ev.segment.text} `;
render(committed, '');
}
}
}
이 render(committed, tail) 시그니처가 UI 패턴 전체입니다. 그 이유는 다음 절에서 설명합니다.
전사본은 두 가지 프레임 종류로 만들어지며, 둘을 혼동하는 것이 가장 흔한 실수입니다.
먼저 왜 둘이 있는지 설명하겠습니다. 음성 모델은 단어가 무엇이었는지를 뒤따르는 단어들로 부분적으로 판단합니다. "I scream"과 "ice cream"은 문장이 이어지기 전까지 같은 소리로 들립니다. 그래서 실시간 인식기는 처음에 추측을 내보내고, 더 많은 오디오가 들어오면 그것을 수정하며, 충분히 들은 뒤에야 확정합니다. 모든 스트리밍 음성 API가 어떤 방식으로든 이를 노출합니다. 추측을 "중간(interim)" 또는 "부분(partial)" 결과라고 부르고 확정된 결과에 is_final을 표시하는 API도 있습니다. 용어는 제각각이어도 원칙은 같습니다. 여기서는 추측을 초안, 확정된 텍스트를 확정이라고 부릅니다.
transcript.draft는 라이브 끝부분입니다. 마지막 확정 이후에 들린 단어들이며 아직 바뀌는 중입니다. 각 초안은 이전 초안을 통째로 대체합니다. segments로 이 끝부분을 다시 그리고, 절대 덧붙이지 마세요. 설계상 초안에는 seq가 없으므로, 나중에 이것이 어떤 확정 결과가 되는지 맞춰보려고 하지 마세요.
transcript.committed는 확정입니다. seq(0, 1, 2, 빈틈 없이 증가), start, end, text, speaker, 그리고 단어별 시간 정보가 담긴 words를 가진 segment 하나입니다. 이를 추가하고, 확정이 덮는 초안은 버리세요. 다음 초안은 이 확정이 끝난 지점에서 시작합니다.
committed: [seq 0] [seq 1]
draft: "how are" ← frame 1
draft: "how are you do" ← frame 2 REPLACES frame 1
committed: [seq 0] [seq 1] [seq 2 "How are you doing?"]
← the commit consumes the tail; drop the draft
초안을 덧붙이는 UI는 중복되고 깜빡이는 텍스트를 보여 줍니다. 자막 화면은 발화자에게는 없던 끊김을 보이게 됩니다. 모든 확정 세그먼트를 그린 뒤 그 뒤에 최신 초안을 그리고, 확정이 도착하면 초안을 지우세요. 확정에는 어떤 초안에도 나타나지 않았던 단어가 들어 있을 수 있습니다.
앞의 fetch 예시(translate: ["es"], 문장 조립 끔)를 기준으로 한 몇 초 분량의 음성이 전송된 모습은 다음과 같습니다:
{"type":"session.loading"}
{"type":"session.ready"}
{"type":"transcript.draft","segments":[{"start":0.0,"text":"Good","speaker":0,"tentative":true}]}
{"type":"transcript.draft","segments":[{"start":0.0,"text":"Good morning every","speaker":0,"tentative":true}]}
{"type":"transcript.committed","segment":{"seq":0,"start":0.0,"end":1.4,"text":"Good morning, everyone.","speaker":0,"words":[{"start":0.0,"end":0.3,"text":"Good"},{"start":0.3,"end":0.8,"text":"morning,"},{"start":0.8,"end":1.4,"text":"everyone."}]}}
{"type":"transcript.translated","seq":0,"translations":{"es":"Buenos días a todos."}}
{"type":"transcript.draft","segments":[{"start":1.6,"text":"Thanks for","speaker":0,"tentative":true}]}
이 프레임 주변에 두 가지가 더 있습니다. transcript.amended는 확정 세그먼트 안의 단어가 수정되면 그 세그먼트를 제자리에서 고칩니다(같은 seq, 새 text 또는 speaker). 이미 그려 둔 줄에 이를 적용하세요. transcript.words는 확정 직전 끝부분의 단어 시간 정보를 스트리밍합니다. 이것은 기본으로 켜져 있는 events.words가 필요하며, 확정에는 이벤트 옵션과 관계없이 항상 words가 포함됩니다. 마지막으로, 알 수 없는 프레임 종류는 무시하세요. 새 종류는 예고 없이 추가됩니다.
다음은 Python SDK의 읽기 루프로, 터미널을 자막 화면으로 씁니다. 여기서도 문장 조립을 끄므로 번역은 transcript.translated로 도착하고, error 프레임은 루프에서 VocalStackError로 발생합니다:
import asyncio
import os
from vocalstack import RealtimeSession, VocalStack, VocalStackError
MIC = ["ffmpeg", "-loglevel", "error", "-f", "avfoundation", "-i", ":0",
"-f", "f32le", "-ac", "1", "-ar", "16000", "-"]
async def main() -> None:
async with VocalStack("{{api}}", api_key=os.environ["VOCALSTACK_API_KEY"]) as vs:
created = await vs.realtime.create_session(
spoken_language="en", translate=["es"], sentences={"enabled": False}
)
session = await RealtimeSession.connect(created.session_id, created.stream_url)
mic = await asyncio.create_subprocess_exec(*MIC, stdout=asyncio.subprocess.PIPE)
async def feed() -> None:
assert mic.stdout is not None
while chunk := await mic.stdout.read(8000): # 125 ms of audio
await session.send_audio(chunk)
await session.eof() # the server flushes, then sends session.done
feeder = asyncio.create_task(feed())
try:
async with session:
async for event in session:
if event.type == "transcript.draft":
# The live tail: replace it, never append it.
tail = " ".join(s.text for s in event.segments)
print(f"\r\x1b[K\x1b[2m{tail}\x1b[0m", end="", flush=True)
elif event.type == "transcript.committed":
# Final: append it (seq 0, 1, 2...), and the tail it covers is gone.
print(f"\r\x1b[K{event.segment.text}", flush=True)
elif event.type == "transcript.translated":
print(f" es: {event.translations.get('es')}")
await feeder
print(f"done ({session.done_reason})", session.stats)
except VocalStackError as e:
# An `error` frame mid-stream, or a refused connect.
print(f"\nerror {e.code}: {e.message} (retryable: {e.retryable})")
finally:
if mic.returncode is None:
mic.kill()
asyncio.run(main())
위와 같은 세그먼트별 번역이 가장 단순한 형태입니다. 읽기에 더 좋은 형태는 별도의 절에서 다룹니다.
translate: ["es", "fr"]는 세션이 진행되는 동안 번역을 요청합니다. 번역이 어떤 방식으로 도착하는지는 문장 조립(sentences: { enabled: true })에 따라 달라지며, 이 기능은 기본으로 켜져 있습니다.
문장 조립이 꺼져 있으면, 각 확정 뒤에 잠시 후 transcript.translated 프레임이 옵니다. 이 프레임은 seq를 밝히고 언어별 키의 translations를 담고 있으므로, 이를 해당 세그먼트에 붙이세요. 지금까지의 예시가 이 방식을 썼습니다. 단순하며 확정 로그처럼 읽힙니다. 세그먼트 하나에 번역 하나이고, 문장 경계는 화자가 숨을 고른 지점에 생깁니다.
문장 조립이 켜져 있으면 서버가 확정을 문장 단위로 모아 번역하며, 훨씬 자연스럽게 읽힙니다. 각 transcript.sentence 프레임은 sentenceId로 구분되는 한 문장을 통째로 교체합니다. status는 open에서 closed로 바뀌며(버리라는 뜻의 dropped도 있습니다), translations는 열려 있는 동안에는 임시이고 닫히면 확정됩니다. sentenceId 기준으로 갱신하고, 절대 덧붙이지 마세요. TypeScript SDK에는 바로 이 목적의 applySentence가 들어 있습니다:
import { applySentence, type SentenceSegment } from '@vocalstack/sdk';
let sentences: SentenceSegment[] = [];
for await (const ev of session) {
if (ev.type !== 'transcript.sentence') continue;
sentences = applySentence(sentences, ev.sentence); // upsert by sentenceId
if (ev.sentence.status === 'closed') console.log(`${ev.sentence.text}\n es: ${ev.sentence.translations?.es}`);
}
번역은 전사보다 한두 초 늦게 따라옵니다. 번역도 음성으로 듣고 싶다면 voice: ["es"]를 추가하세요(문장 조립이 필요합니다). 닫힌 각 문장의 스페인어 번역은 transcript.voice 프레임으로 도착하며, 이 프레임에는 24 kHz 모노 base64 Ogg/Opus 클립이 담깁니다. 긴 문장은 조각(parts의 part)으로 나뉘어 도착하므로, 순서대로 이어서 재생하면 됩니다. 임시 텍스트는 절대 음성으로 읽지 않습니다. 음성 번역에 대한 자세한 내용은 실시간 번역 가이드에 있으며, 지연 시간이 어디서 생기는지에서 지연이 발생하는 과정을 설명합니다.
번역 대상 언어는 생성 시점에 고정되지 않습니다.
session.update 텍스트 프레임은 그 시점부터 spokenLanguage, translate, speakerLabels, latencyMode, voice 또는 voiceByLang을 바꿉니다. 생략한 필드는 그대로 유지됩니다.
{"type":"session.update","translate":["es","fr"],"latencyMode":"fast"}
SDK에서는 session.update(…)로 노출됩니다. translate는 목록 전체를 대체하며, []를 보내면 번역이 꺼집니다. 이미 전달된 세그먼트는 다시 전사되거나 다시 번역되지 않으며, 화자 레이블을 켜면 그 시점부터만 화자가 표시됩니다. 이미 말한 내용은 그대로 남고, 설정은 그다음에 오는 것에만 영향을 줍니다.
오디오가 끝나면 {"type":"eof"}를 보내세요(SDK에서는 session.eof()). 서버는 아직 가지고 있는 오디오를 전사하고, 마지막 확정을 보낸 뒤 session.done을 보내고 닫습니다. 대신 cancel을 보내면 즉시 멈추고 나머지는 버립니다.
{"type":"session.done","reason":"eof","stats":{"audioS":30.0,"processingS":30.6,"realtimeFactor":0.98}}
reason은 eof, max_duration(세션이 요금제의 최대 길이에 도달해 같은 마무리 절차를 거친 경우), 또는 cancelled입니다. stats는 서버가 측정합니다. audioS는 보낸 오디오 길이이고, processingS는 세션이 실행된 실제 경과 시간이며, realtimeFactor는 둘의 비율입니다. 실시간 세션은 보통 1 바로 아래에 옵니다. 실제 경과 시간에는 eof 뒤의 마지막 마무리 처리가 포함되기 때문입니다. 이 값이 그보다 눈에 띄게 낮으면 세션이 오디오 길이보다 상당히 오래 걸렸다는 뜻이며, 가장 먼저 볼 것은 오디오가 얼마나 빨리 도착했는지입니다. 파일의 경우 같은 값은 전사본이 재생 속도보다 몇 배 빠르게 돌아왔는지를 알려줍니다.
세션은 GET /v1/realtime/capabilities에 있는 limits.maxDurationS를 넘을 수 없으며, 오디오는 요금제의 실시간 요율에 따라 초 단위로 크레딧을 사용합니다. 다른 전사와 같은 5분 최소 과금이 적용되어, 오디오를 받은 세션은 각각 최소 5분이 청구됩니다(요금제 참조). 같은 소켓으로 파일 전체를 보낼 수도 있습니다. 세션을 input: { kind: "file" }로 만들고, 파일의 원본 바이트(mp3, wav, flac, ogg, m4a)와 eof를 보내면, 전사본은 재생 시간보다 여러 배 빠르게 돌아오며 그 사이에 transcript.progress 프레임이 옵니다.
재개(resume) 기능은 없습니다. 연결이 끊기면 세션도 끝납니다. 다시 연결하려면 새 세션을 만들어야 하며, 이 세션에도 최소 과금이 따로 청구되고, 라이브 소스에서 다시 스트리밍을 시작해야 하며, seq는 0부터 다시 시작합니다. 재연결 자체는 모든 소켓 API에서 쓰는 표준 패턴이며, 한 번쯤 제대로 익혀둘 가치가 있습니다:
retryable: true인 error 프레임, 대기 시간이 적힌 503, 또는 프레임 없이 30초가 지난 경우(capacity_exceeded를 뜻함)에만 재시도하세요. invalid_token과 session_expired는 같은 streamUrl로 다시 시도하라는 뜻이 아니라, 서버에서 새 streamUrl을 받아야 한다는 뜻입니다.세션을 만드는 라우트는 가볍게 호출할 수 있도록 유지하고, 연결 종료를 새로운 시작으로 취급하세요. 재개 기능이 없는 것은 아무것도 저장하지 않는 것의 대가입니다. 전사본이 소켓보다 오래 살아남아야 한다면 직접 보관하세요.
같은 스트림을 녹음으로 저장하고, 나중에 수정하고, 내보내고, 여러 언어를 쓰는 청중이 읽게 하고 싶다면 대신 실시간 전사로 게시하세요. 라이브 시작 가이드는 같은 PCM 소켓을 전사와 방송 링크로 연결하는 과정을 보여 주고, 청취 가이드는 구독자 쪽을 설명합니다. 청취자는 각자 언어를 고르고, 늦게 들어온 사람은 lastSeq 커서로 따라잡습니다. 한 공간에서 이것이 어떤 모습인지는 링크 하나, 모든 언어에서 다룹니다.
보관하고 싶은 파일이라면 저장 경로는 API로 파일 전사하기입니다. 모든 필드를 담은 전체 프레임 참조는 API 레퍼런스의 마이크 스트리밍 가이드에 있습니다.