For the complete documentation index, see llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.
기본 탐색 메뉴
Theme

WebRTC

WebRTC로 음성 애플리케이션을 연결하세요.

사용할 API를 선택하면 연결 절차와 세션 이벤트를 확인할 수 있습니다.

브라우저를 GPT-Live에 연결하기

브라우저, 모바일, 서버 애플리케이션에서 GPT-Live와 음성을 주고받을 때는 WebRTC를 권장합니다. 이 가이드에서는 브라우저를 예로 들어 연결 방법을 설명합니다. 마이크 입력과 모델이 생성한 음성은 연결 협상 과정에서 설정한 미디어 트랙으로 전송됩니다. 전사, 세션 업데이트, 위임된 작업에 관한 JSON 이벤트는 데이터 채널로 전송됩니다.

브라우저가 SDP(Session Description Protocol) 오퍼를 생성합니다. 애플리케이션 서버는 프로젝트 API 키로 POST /v1/live/sessions를 호출해 오퍼를 보내고 SDP 응답을 받습니다. API 키와 세션 설정은 신뢰할 수 있는 서버에 보관하세요.

시작하기 전에

다음 항목을 준비하세요.

  • GPT-Live에 액세스할 수 있는 프로젝트 API 키
  • 선택한 SDK 예제를 실행할 서버 런타임. Node.js 예제에는 Node.js 22.6 이상이 필요합니다.
  • HTTPS 또는 로컬호스트에서 페이지를 열고 해당 페이지에 마이크 접근 권한을 부여할 수 있는 브라우저

이 예제는 gpt-5.6-terra와 호스팅형 웹 검색을 사용해 Responses에 작업을 위임합니다. 백엔드 지침과 애플리케이션 도구는 위임 및 도구를 참고하세요. 음성과 백엔드 사용량은 비용 최적화를 참고하세요.

연결 절차 이해하기

  1. 사용자 동작에 응답해 마이크 접근 권한을 요청하고 마이크 트랙을 피어 연결에 추가하세요.
  2. SDP 오퍼를 생성하기 전에 데이터 채널을 만들고 이벤트 리스너를 등록하세요.
  3. 로컬 설명을 설정하고 ICE 후보 수집이 끝날 때까지 기다린 다음 오퍼를 서버로 보내세요.
  4. 서버에서 session과 transport: { type: "webrtc", sdp: ... }가 포함된 JSON을 HTTP POST로 OpenAI에 보내세요.
  5. 반환된 SDP 응답을 원격 설명으로 설정하세요. 데이터 채널에서 session.started를 받은 후 애플리케이션 명령어를 보내세요.

HTTP 요청으로 세션이 시작됩니다. 데이터 채널로 session.start를 보내지 마세요. 예제의 oai-events 문자열은 데이터 채널의 레이블입니다.

POST /v1/live/sessions로 WebRTC 세션을 만들면 초기화 시 음성 사용 시간 15초에 해당하는 요금이 청구됩니다. 세션이 실행되면 이 금액은 사용 시간에 따른 요금에서 차감됩니다. 실행 시간에 15초가 추가되는 것은 아닙니다. 비용 산정 방식은 WebRTC 초기화 요금을 참고하세요.

애플리케이션 서버 만들기

새 디렉터리에 서버 예제를 저장하고 서버 환경에 OPENAI_API_KEY를 설정하세요. Node.js를 사용하는 경우 파일 이름을 server.mjs로 지정하세요. openai와 express를 설치하려면 npm install openai express를 실행하세요. Python을 사용하는 경우 openai를 설치하세요. 이 예제 서버는 127.0.0.1에 바인딩되어 http://localhost:3000에서 보낸 세션 요청을 수락하고, 실행 디렉터리에 있는 index.html을 제공합니다.

Go를 사용하는 경우 예제를 main.go로 저장하고 go mod init live-demo로 모듈을 초기화한 다음 github.com/openai/openai-go/v3@v3.85.1을 go get으로 설치하세요. Go 1.26 이상에서 go run .을 실행하세요. Java를 사용하는 경우 Java 17 이상에서 Maven 종속성 com.openai:openai-java:4.95.0을 추가하고 LiveConnectionWebrtcExample 클래스를 실행하세요. Ruby 3.3 이상에서 openai 0.110.0과 webrick을 설치하려면 gem install openai -v 0.110.0과 gem install webrick을 실행하세요. 예제를 server.rb로 저장하고 ruby server.rb를 실행하세요.

아래에서 서버 언어를 선택하세요. 어느 베리언트를 선택해도 포트 3000에서 index.html을 제공하며, 세션 엔드포인트는 /api/session으로 동일합니다. Live를 지원하는 SDK 버전을 사용하고 한 번에 하나의 베리언트만 실행하세요.

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"));
});

// Local-only demo. Add your application's authentication and authorization
// before exposing session creation to other users.
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,
      },
    });
    // Preserve the SDK's typed session ID and 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}`));

다른 사용자가 서버에 접근할 수 있도록 공개하기 전에 /api/session에 애플리케이션의 인증, 인가, 요청 제한, HTTPS를 적용하세요. 이 로컬 예제의 출처 검사는 사용자를 인증하지 않습니다.

브라우저 클라이언트 만들기

서버 실행 디렉터리에 index.html을 만드세요.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>GPT-Live connection</title>
  </head>
  <body>
    <script type="module">
      // Paste the browser code below here.
    </script>
  </body>
</html>

다음 코드를 모듈 스크립트 안에 붙여 넣으세요. 이 코드는 대화를 시작하고 종료하는 버튼을 추가하고, 마이크와 오디오 출력을 연결하며, 세션 이벤트를 처리합니다. /api/session은 애플리케이션 서버의 라우트입니다.

const start = document.createElement("button");
start.textContent = "Start conversation";
const stop = document.createElement("button");
stop.textContent = "End conversation";
stop.disabled = true;
const status = document.createElement("p");
const audio = new Audio();
audio.autoplay = true;
audio.controls = true;
document.body.append(start, stop, status, audio);

let peer;

let events;

let microphone;

let closeTimeout;
let ready = false;
let finalized = false;

function cleanup() {
  clearTimeout(closeTimeout);
  microphone?.getTracks().forEach((track) => track.stop());
  const closingEvents = events;
  events = undefined;
  closingEvents?.close();
  peer?.close();
  audio.srcObject = null;
  ready = false;
  start.disabled = false;
  stop.disabled = true;
}

start.addEventListener("click", async () => {
  start.disabled = true;
  finalized = false;
  status.textContent = "Connecting…";
  try {
    const connection = new RTCPeerConnection();
    peer = connection;
    connection.addEventListener("track", (event) => {
      audio.srcObject = new MediaStream([event.track]);
      audio.play().catch(() => {
        status.textContent =
          "Select play on the audio controls to hear the assistant.";
      });
    });
    microphone = await navigator.mediaDevices.getUserMedia({ audio: true });
    for (const track of microphone.getAudioTracks()) {
      connection.addTrack(track, microphone);
    }

    // Create the event channel before creating the SDP offer.
    events = connection.createDataChannel("oai-events");
    events.addEventListener("message", ({ data }) => {
      const event = JSON.parse(data);
      if (event.type === "session.started") {
        ready = true;
        stop.disabled = false;
        status.textContent = "Connected: " + event.session.id;
      } else if (event.type === "session.closed") {
        finalized = true;
        console.log("Final session usage", event.usage);
        status.textContent = "Conversation ended.";
        cleanup();
      } else {
        // Save transcript and nested Responses events as needed by your app.
        console.log(event);
      }
    });
    events.addEventListener("close", (event) => {
      if (event.target !== events) return;
      if (!finalized) {
        status.textContent = "Disconnected without final session usage.";
        cleanup();
      }
    });

    const offer = await connection.createOffer();
    await connection.setLocalDescription(offer);
    if (connection.iceGatheringState !== "complete") {
      await new Promise((resolve, reject) => {
        const timeout = setTimeout(() => {
          connection.removeEventListener("icegatheringstatechange", onState);
          reject(new Error("Timed out while gathering ICE candidates"));
        }, 10_000);
        function onState() {
          if (connection.iceGatheringState !== "complete") return;
          clearTimeout(timeout);
          connection.removeEventListener("icegatheringstatechange", onState);
          resolve(undefined);
        }
        connection.addEventListener("icegatheringstatechange", onState);
        onState();
      });
    }

    const sdp = connection.localDescription?.sdp;
    if (!sdp) throw new Error("Missing local SDP offer");
    const response = await fetch("/api/session", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ sdp }),
    });
    if (!response.ok) throw new Error(await response.text());

    const result = await response.json();
    console.log("Created session", result.session.id);
    await connection.setRemoteDescription({
      type: "answer",
      sdp: result.transport.sdp,
    });
    // The HTTP request started this session. Do not send session.start here.
  } catch (error) {
    status.textContent =
      error instanceof Error ? error.message : String(error);
    cleanup();
  }
});

stop.addEventListener("click", () => {
  if (!ready || !events || events.readyState !== "open") return;
  stop.disabled = true;
  status.textContent = "Finishing the conversation…";
  // The session.closed handler is already registered. Keep media and events
  // alive while pending work drains; only clean up after the final event.
  events.send(JSON.stringify({ type: "session.close" }));
  closeTimeout = setTimeout(() => {
    status.textContent = "Incomplete finalization: no session.closed event.";
    cleanup();
  }, 15_000);
});

선택한 서버를 실행하고(node server.mjs 또는 python server.py) http://localhost:3000을 연 다음 Start conversation을 선택하세요. 상태가 Connected로 바뀌면 최신 정보가 필요한 질문을 해서 호스팅형 웹 검색을 테스트하세요. 브라우저가 자동 재생을 차단하면 오디오 재생 컨트롤을 사용하세요.

세션 응답 읽기

요청이 성공하면 HTTP 201과 함께 세션 ID와 SDP 응답이 담긴 JSON이 반환됩니다.

{
  "session": { "id": "live_123" },
  "transport": { "type": "webrtc", "sdp": "<SDP answer>" }
}

result.session.id를 읽고 result.transport.sdp를 setRemoteDescription에 전달하세요. 세션 ID의 내부 구조를 해석하지 말고 접두사를 포함한 전체 값을 그대로 유지하세요.

연결을 시작할 때 지연 시간을 줄이는 방법은 WARP를 사용한 WebRTC 연결을 참고하세요.

미디어와 이벤트 처리하기

미디어 트랙으로 마이크 오디오를 보내고 모델이 생성한 음성을 받으세요. WebRTC는 SDP를 통해 오디오 형식을 협상하므로 세션 설정에서 audio.format을 생략하세요. 데이터 채널로 session.input_audio.append를 보내지 마세요. session.output_audio.delta도 이 채널로 수신되지 않습니다.

전사 내용의 증분 업데이트, 세션 명령어, 중첩된 response.event 메시지는 데이터 채널로 주고받으세요. 전사 처리와 수명 주기 이벤트는 세션 관리를 참고하세요. 서버에 별도의 이벤트 연결이 필요하면 서버 측 제어를 참고하세요.

대화를 종료하려면 session.close를 보내고 이벤트를 계속 수신하세요. session.closed를 받은 후 피어 연결을 닫고 마이크 트랙을 종료하세요. 예제에서는 명령어를 보내기 전에 마지막 이벤트를 수신할 리스너를 등록합니다. 마지막 이벤트를 받기 전에 연결이 끊기거나 시간 초과가 발생하면 최종 사용량을 확인할 수 없습니다. 최종 사용량 처리 방법은 사용량 확인 및 정상 종료를 참고하세요.