For the complete documentation index, see llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.
メインナビゲーション

WebRTC

WebRTC を使用してブラウザの音声アプリケーションを接続します。

アプリケーションで使用する API を選択してください。認証、セッション作成、イベントの仕様は API ごとに異なります。

ブラウザから GPT-Live への接続

ブラウザの音声アプリケーションには WebRTC を使用します。マイク入力と生成された音声は、ネゴシエーションで確立したメディアトラックを通じて送受信されます。文字起こし、セッションの更新、委任した作業に関する JSON イベントは、データチャネルで送受信されます。

ブラウザが Session Description Protocol(SDP)のオファーを作成します。アプリケーションサーバーはプロジェクトの API キーを使用し、POST /v1/live/sessions でこのオファーと引き換えにアンサーを取得します。キーとセッション構成は、信頼できるサーバー上に保持してください。

事前準備

必要なものは次のとおりです。

  • GPT-Live にアクセスできるプロジェクトの API キー
  • 選択した SDK のサンプルに対応するサーバーランタイム。Node.js のサンプルには Node.js 22.6 以降が必要です。
  • HTTPS または localhost 上で動作し、マイクの使用が許可されたブラウザ

このサンプルでは、gpt-5.6-terra を使用した Responses への委任と、ホスト型のウェブ検索を利用します。バックエンドへの指示とアプリケーションのツールについては、委任とツールを参照してください。音声とバックエンドの使用量については、コストの最適化を参照してください。

接続の流れ

  1. ユーザーの操作をきっかけにマイクへのアクセスを要求し、マイクのトラックをピア接続に追加します。
  2. SDP オファーを作成する前に、データチャネルを作成してイベントリスナーを登録します。
  3. ローカル記述を設定し、ICE 候補の収集が完了するのを待ってから、オファーをサーバーに送信します。
  4. サーバーから、sessiontransport: { type: "webrtc", sdp: ... } を含む JSON を OpenAI に POST します。
  5. 返された SDP アンサーをリモート記述として適用します。データチャネルで session.started を受信してから、アプリケーションのコマンドを送信します。

セッションは HTTP リクエストによって開始されます。 データチャネルで session.start を送信しないでください。 サンプル内の oai-events という文字列は、データチャネルのラベルです。

POST /v1/live/sessions で WebRTC セッションを作成すると、初期化時に音声の利用時間 15 秒分が課金されます。この金額は、セッションの実行開始後に発生する利用時間の料金に充当されます。実行中のセッションに 15 秒分が追加で課金されるわけではありません。料金の計算については、WebRTC の初期化料金を参照してください。

アプリケーションサーバーの作成

サーバーのサンプルを新しいディレクトリに保存し、その環境で OPENAI_API_KEY を設定します。Node.js の場合は server.mjs を使用し、npm install openai expressopenaiexpress をインストールします。Python の場合は openai をインストールします。このサンプルは 127.0.0.1 にバインドし、http://localhost:3000 からのセッションリクエストを受け付け、実行ディレクトリ内の index.html を配信します。

以下からサーバーの言語を選択してください。どのサンプルも、ポート 3000 で index.html と同じ /api/session エンドポイントを提供します。Live に対応したバージョンの SDK を使用してください。同時に実行するサンプルは 1 つだけにしてください。

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

他のユーザーがサーバーにアクセスできるようにする前に、アプリケーションの認証、認可、リクエスト制限、HTTPS で /api/session を保護してください。このローカル環境向けサンプルのオリジンチェックでは、ユーザー認証は行われません。

ブラウザクライアントの作成

サーバーを実行するディレクトリに 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());
  events?.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 を開いて、 会話を開始を選択します。ステータスが 接続済みに変わったら、最新情報を必要とする質問をして、ホスト型検索を試してください。ブラウザが自動再生をブロックする場合は、音声コントロールを使用してください。

セッションレスポンスの読み取り

リクエストが成功すると、HTTP 201 と、セッション ID および SDP アンサーを含む JSON が返されます。

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

result.session.id を読み取り、result.transport.sdpsetRemoteDescription に渡します。セッション ID は内部構造を解釈せず、プレフィックスも含めて変更せずに保持してください。

メディアとイベントの処理

メディアトラックを通じてマイク音声を送信し、生成された音声を受信します。WebRTC は SDP を通じて音声形式をネゴシエーションするため、セッション構成では audio.format を省略してください。データチャネルで session.input_audio.append を送信しないでください。また、データチャネルで session.output_audio.delta が届くことを前提にしないでください。

文字起こしの差分、セッションコマンド、ネストされた response.event メッセージにはデータチャネルを使用します。文字起こしの処理とライフサイクルイベントについては、セッションの管理を参照してください。サーバー側で独自のイベント接続が必要な場合は、サーバー側の制御を参照してください。

会話を終了するには、session.close を送信し、session.closed を受信するまで受信処理を続けてから、ピア接続とマイクのトラックを閉じます。このサンプルでは、コマンドを送信する前に最終イベントのリスナーを登録します。その前に接続が失敗またはタイムアウトした場合、最終的な使用量は未確認のままです。最終的な使用量の処理については、使用量と正常な終了処理を参照してください。