WebRTC
WebRTC
브라우저 음성 애플리케이션을 GPT-Live와 Realtime API에 연결하는 WebRTC 가이드예요. 마이크 입력과 생성 음성은 협상된 미디어 트랙으로 오고, 데이터 채널로 전사·세션 업데이트·위임 작업의 JSON 이벤트가 흘러요.
출처: 문서
본문
브라우저를 GPT-Live에 연결하기
브라우저 음성 애플리케이션에는 WebRTC를 써요. 마이크 입력과 생성 음성이 협상된 미디어 트랙으로 이동하고, 데이터 채널이 전사·세션 업데이트·위임 작업의 JSON 이벤트를 나릅니다. 브라우저가 SDP(Session Description Protocol) offer를 만들고, 애플리케이션 서버가 프로젝트 API 키로 POST /v1/live/sessions에 answer와 교환해요. 키와 세션 구성은 신뢰할 수 있는 서버에 보관하세요.
시작 전에
필요한 것:
- GPT-Live 접근 권한이 있는 프로젝트 API 키
- 선택한 SDK 예시를 돌릴 서버 런타임. Node.js 예시는 Node.js 22.6 이상.
- 마이크 권한이 있고 HTTPS 또는 localhost에서 동작하는 브라우저
예시는 gpt-5.6-terra와 호스팅 웹 검색을 쓰는 Responses 위임을 사용해요. 백엔드 지침과 애플리케이션 도구는 Delegation and tools를, 음성·백엔드 사용량은 Cost optimization을 참고하세요.
연결 순서 이해하기
- 사용자 동작에서 마이크 접근을 요청하고 그 트랙을 피어 연결에 추가해요.
- SDP offer를 만들기 전에 데이터 채널을 만들고 이벤트 리스너를 등록해요.
- 로컬 설명을 설정하고 ICE 후보 수집을 기다린 뒤 offer를 서버로 보내요.
- 서버가
session과transport: { type: "webrtc", sdp: ... }를 담은 JSON을 OpenAI에 POST해요. - 반환된 SDP answer를 원격 설명으로 적용해요. 애플리케이션 명령을 보내기 전에 데이터 채널의
session.started를 기다려요.
HTTP 요청이 세션을 시작해요. 데이터 채널에서 session.start를 보내지 마세요. 예시의 oai-events 문자열이 데이터 채널 라벨이에요.
POST /v1/live/sessions로 WebRTC 세션을 만들면 초기화 동안 15초의 음성 지속시간이 청구돼요. 이 양은 세션이 실행되기 시작하면 지속시간 요금으로 크레딧이 되고, 실행 중인 세션에 15초가 추가되는 게 아니에요.
애플리케이션 서버 만들기
서버 예시를 새 디렉터리에 저장하고 그 환경에 OPENAI_API_KEY를 설정하세요. Node.js는 server.mjs로 저장하고 npm install openai express로 설치해요. Python은 openai만 설치해요. 이 예시는 127.0.0.1에 바인딩하고 http://localhost:3000의 세션 요청만 받으며, 실행하는 디렉터리에서 index.html을 서빙해요.
Node.js 예시(Express):
import express from "express";
import OpenAI from "openai";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
const app = express();
const client = new OpenAI({ maxRetries: 0 });
const port = 3000;
const origin = `http://localhost:${port}`;
const indexPath = resolve("index.html");
app.use(express.json({ limit: "64kb" }));
app.get("/", async (_request, response) => {
response.type("html").send(await readFile(indexPath, "utf8"));
});
// 로컬 전용 데모입니다. 세션 생성을 다른 사용자에게 열기 전에
// 애플리케이션의 인증·권한을 추가하세요.
app.post("/api/session", async (request, response) => {
if (request.headers.origin !== origin) {
response.status(403).json({ error: "Unexpected request origin" });
return;
}
if (typeof request.body?.sdp !== "string" || !request.body.sdp.trim()) {
response.status(400).json({ error: "An SDP offer is required" });
return;
}
if (!process.env.OPENAI_API_KEY) {
response.status(503).json({ error: "Set OPENAI_API_KEY on the server" });
return;
}
try {
const result = await client.live.create({
session: {
model: "gpt-live-1",
instructions:
"Be concise. Delegate requests needing current information to the backend, which can search the web.",
delegation: {
type: "responses",
responses: {
model: "gpt-5.6-terra",
instructions:
"Use web search when current facts are needed. Return concise, grounded results for a spoken conversation.",
tools: [{ type: "web_search" }],
tool_choice: "auto",
},
},
},
transport: {
type: "webrtc",
sdp: request.body.sdp,
},
});
// SDK의 타입 있는 세션 ID와 SDP answer를 그대로 보존합니다.
response.status(201).json(result);
} catch (error) {
if (!(error instanceof OpenAI.APIError)) throw error;
console.error("Live session creation failed", error.status);
response
.status(error.status ?? 502)
.json({ error: "Live session creation failed" });
}
});
app.listen(port, "127.0.0.1", () => console.log(`Open ${origin}`));
Python http.server 버전도 같은 끝점(/api/session)과 index.html 서빙을 3000 포트에서 해요. 서버를 다른 사용자에게 열기 전에 /api/session을 애플리케이션 인증·권한·요청 제한·HTTPS로 보호하세요. 이 로컬 예시의 origin 검사는 사용자를 인증하지 않아요.
브라우저 클라이언트 만들기
서버를 실행하는 디렉터리에서 index.html을 만들고 모듈 스크립트 안에 브라우저 코드를 넣어요. 코드는 시작·종료 컨트롤을 추가하고, 마이크와 출력 오디오를 연결하고, 세션 이벤트를 처리해요. 핵심 흐름:
RTCPeerConnection을 만들고track이벤트로 모델 오디오를<audio>에 연결navigator.mediaDevices.getUserMedia({ audio: true })로 마이크 트랙 추가- SDP offer 전에 데이터 채널
oai-events만들기(리슨 등록 포함) connection.createOffer()→setLocalDescription→ ICE gathering 완료 대기/api/session에 SDP를 POST하고,result.transport.sdp를setRemoteDescription({ type: "answer", sdp })에 적용session.started에서 "Connected" 표시,session.closed에서 최종 usage 처리 후 정리
종료 버튼은 session.close를 보내고 session.closed가 올 때까지 계속 받은 뒤(15초 타임아웃) 피어 연결과 마이크 트랙을 닫아요. 데이터 채널이 열릴 때까지 session.closed 리스너를 이미 등록해 두는 패턴이 중요해요.
서버를 실행(node server.mjs 또는 python server.py)하고 http://localhost:3000을 연 뒤 Start conversation을 선택하세요. 상태가 Connected로 바뀌면 최신 정보가 필요한 질문을 해 호스팅 검색을 동작시켜 보세요. 브라우저가 자동 재생을 막으면 오디오 컨트롤을 이용하세요.
세션 응답 읽기
성공한 요청은 HTTP 201과 함께 세션 ID·SDP answer를 담은 JSON을 반환해요.
{
"session": { "id": "live_123" },
"transport": { "type": "webrtc", "sdp": "<SDP answer>" }
}
result.session.id를 읽고 result.transport.sdp를 setRemoteDescription에 넘겨요. 세션 ID는 불투명하게 취급하고 접두사를 포함해 그대로 보존해요.
미디어와 이벤트 처리
마이크 오디오를 보내고 생성 음성을 받으려면 미디어 트랙을 써요. WebRTC가 SDP를 통해 오디오 형식을 협상하므로 세션 구성에서 audio.format을 생략해요. 데이터 채널에 session.input_audio.append를 보내거나 session.output_audio.delta를 기대하지 마세요. 데이터 채널은 전사 델타, 세션 명령, 중첩 response.event 메시지에 써요. 전사 처리와 수명 주기 이벤트는 Managing sessions, 서버 쪽 이벤트 연결이 필요하면 Server-side controls를 참고하세요. 대화를 끝내려면 session.close를 보내고, 피어 연결과 마이크 트랙을 닫기 전에 session.closed까지 계속 받아요. 최종 사용량 처리는 Usage and graceful close를 참고하세요.
Realtime API: WebRTC로 연결
WebRTC는 실시간 애플리케이션을 만들기 위한 강력한 표준 인터페이스 세트예요. Realtime API는 WebRTC 피어 연결로 실시간 모델에 연결하는 것을 지원해요. 브라우저 기반 음성-음성 애플리케이션은 Voice agents부터 시작하는 걸 권장해요 — Realtime 세션을 관리하는 Agents SDK의 고수준 헬퍼와 API를 다뤄요. WebRTC 인터페이스는 강력하고 유연하지만 Agents SDK보다 저수준이에요. 웹 브라우저나 모바일 기기 같은 클라이언트에서 Realtime 모델에 연결할 때는 더 일관된 성능을 위해 WebSockets보다 WebRTC를 권장해요. WebRTC 위에서 UI를 만드는 방법은 MDN 문서를 참고하세요.
개요
Realtime API는 브라우저에서 연결하는 두 가지 메커니즘을 지원해요: 통합 인터페이스(unified interface)와 임시 API 키(ephemeral API key, OpenAI REST API로 생성). 통합 인터페이스는 설정이 더 간단하고 연결이 더 빨라요. 이 방식은 세션 초기화의 중요 경로에 애플리케이션 서버를 두는 구조예요.
통합 인터페이스로 연결
통합 인터페이스로 WebRTC 연결을 초기화하는 과정(웹 브라우저 클라이언트 기준):
- 브라우저가 WebRTC 피어 연결의 SDP 데이터로 개발자가 제어하는 서버에 요청해요.
- 서버가 그 SDP를 세션 구성과 함께 multipart 폼으로 합쳐 표준 API 키로 인증하며 OpenAI Realtime API에 보내요.
세션을 만들려면 작은 서버 쪽 애플리케이션을 만들어 /v1/realtime/calls에 요청해요. 간단한 Node.js express 서버 예시:
import express from "express";
const app = express();
// 브라우저에서 POST되는 raw SDP 페이로드를 파싱합니다.
app.use(express.text({ type: ["application/sdp", "text/plain"] }));
const sessionConfig = JSON.stringify({
type: "realtime",
model: "gpt-realtime-2.1",
audio: { output: { voice: "marin" } },
});
// Realtime API 세션을 만드는 엔드포인트.
app.post("/session", async (req, res) => {
const fd = new FormData();
fd.set("sdp", req.body);
fd.set("session", sessionConfig);
try {
const r = await fetch("https://api.openai.com/v1/realtime/calls", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.OPENAI_API_KEY,
"OpenAI-Safety-Identifier": "hashed-user-id",
},
body: fd,
});
// OpenAI REST API에서 받은 SDP를 그대로 보냅니다.
const sdp = await r.text();
res.send(sdp);
} catch (error) {
console.error("Token generation error:", error);
res.status(500).json({ error: "Failed to generate token" });
}
});
app.listen(3000);
애플리케이션이 각 최종 사용자에게 안전 식별자를 할당한다면 해시된 내부 사용자 ID 같은 안정적이고 개인정보를 보존하는 값을 OpenAI-Safety-Identifier 헤더로 이 서버 쪽 요청에 포함하세요. 헤더는 브라우저가 아니라 신뢰하는 백엔드가 설정해야 해요.
브라우저에선 표준 WebRTC API로 연결해요. 클라이언트가 SDP 데이터를 서버에 직접 POST하고, RTCPeerConnection을 만들고, 마이크 트랙을 추가하고, oai-events 데이터 채널을 만들고, offer를 만들어 서버에 보낸 뒤 반환된 SDP answer를 setRemoteDescription에 적용해요.
임시 토큰으로 연결
임시 API 키로 WebRTC 연결을 초기화하는 과정:
- 브라우저가 개발자가 제어하는 서버에 임시 API 키 생성을 요청해요.
- 개발자 서버가 표준 API 키로 OpenAI REST API에 임시 키를 요청하고 새 키를 브라우저에 돌려줘요.
- 브라우저가 임시 키로 Realtime API에 WebRTC 피어 연결로 직접 인증해요.
임시 토큰 서버 끝점은 HTTP 요청을 주고받을 수 있는 어떤 플랫폼에서도 만들 수 있어요. 표준 OpenAI API 키는 서버에서만 쓰고 브라우저에는 절대 쓰지 마세요. 임시 토큰을 쓸 때는 클라이언트 시크릿을 만드는 서버 쪽 요청에 OpenAI-Safety-Identifier를 설정해요. Realtime API가 그 식별자를 결과 임시 토큰에 바인딩하므로, 브라우저는 나중에 그 토큰으로 연결할 때 안전 식별자를 보낼 필요가 없어요.
브라우저에선 먼저 서버 끝점에서 토큰을 받고, 그 임시 토큰으로 SDP 데이터를 Realtime API에 POST해요.
// OpenAI Realtime API용 세션 토큰 가져오기
const tokenResponse = await fetch("/token");
const data = await tokenResponse.json();
const EPHEMERAL_KEY = data.value;
// 피어 연결 만들기
const pc = new RTCPeerConnection();
audioElement.current = document.createElement("audio");
audioElement.current.autoplay = true;
pc.ontrack = (e) => (audioElement.current.srcObject = e.streams[0]);
const ms = await navigator.mediaDevices.getUserMedia({ audio: true });
pc.addTrack(ms.getTracks()[0]);
const dc = pc.createDataChannel("oai-events");
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const sdpResponse = await fetch("https://api.openai.com/v1/realtime/calls", {
method: "POST",
body: offer.sdp,
headers: {
Authorization: "Bearer " + EPHEMERAL_KEY,
"Content-Type": "application/sdp",
},
});
const answer = { type: "answer", sdp: await sdpResponse.text() };
await pc.setRemoteDescription(answer);
WARP로 연결 지연 줄이기
WebRTC Abridged Roundtrip Protocol(WARP)은 Realtime API 음성 세션 시작 시간을 줄여요. 개별 최적화를 켜거나 모두 조합해 전체 WARP 핸드셰이크를 쓸 수 있어요. 네이티브 클라이언트 설정, 브라우저 지원, origin-trial 지침, 통합 연결 흐름은 WebRTC with WARP를 참고하세요.
이벤트 보내고 받기
Realtime API 세션은 개발자가 내는 client-sent events와 Realtime API가 만드는 server-sent events를 조합해 관리돼요. WebRTC로 Realtime 모델에 연결할 때는 WebSockets처럼 모델의 오디오 이벤트를 세밀하게 직접 처리할 필요가 없어요. 위처럼 구성된 WebRTC 피어 연결 객체가 그 작업을 대신해요. 다른 클라이언트·서버 이벤트는 피어 연결의 데이터 채널로 주고받아요.
const dc = pc.createDataChannel("oai-events");
// 서버 이벤트 듣기
dc.addEventListener("message", (e) => {
const event = JSON.parse(e.data);
console.log(event);
});
// 클라이언트 이벤트 보내기
const event = {
type: "conversation.item.create",
item: {
type: "message",
role: "user",
content: [{ type: "input_text", text: "hello there!" }],
},
};
dc.send(JSON.stringify(event));
Realtime 대화 관리에 대한 자세한 내용은 Realtime conversations 가이드를 참고하세요. 가벼운 예시 앱으로는 Realtime Console을 확인해 보세요.
더 알아보기 (Learn more)
서버 쪽 제어는 Server-side controls, WebSocket 연결은 WebSockets 가이드, 전화 연결은 SIP 가이드를 참고하세요.