WebRTC の認証準備、メディア方向の設定、SDP とモデルイベントの交換、リソース解放の手順を説明します。
WebRTC はメディアトラックで音声と映像を、DataChannel でモデルイベントとテキストを転送します。Web アプリケーションはブラウザー標準 API を使用でき、その他のプラットフォームは標準 WebRTC に対応するライブラリを使用できます。Model Studio は専用 WebRTC SDK を提供していません。
前提条件
-
接続の概要の準備を完了します。
-
Realtime API の概要で対象モデルの WebRTC 対応を確認します。対応カテゴリはリアルタイムマルチモーダル、音声会話、音声翻訳、マルチモーダル対話スイートです。音声合成や音声認識には、対象モデルが対応する AOQ または WebSocket を使用してください。
-
SDP 交換を中継するアプリケーションサーバー (AppServer) に、リージョンとワークスペースに対応する API キーを用意します。認証フィールドはトークン認証を参照してください。
注記ブラウザーではセキュアコンテキストを使用し、必要に応じてマイクやカメラの権限を要求します。音声受信時はブラウザーの自動再生制限に対応してください。
処理フロー
- AppServer に API キーと対象モデルのパラメーターを用意します。
- クライアントの接続オブジェクトを作成し、コールバックを登録します。
- 上りメディアトラックを接続せずに、メディア方向と DataChannel を設定します。
- AppServer 経由で SDP を交換し、接続とイベントチャネルの準備完了を待ちます。
- モデルのプロトコルに従って設定します。確認後、必要な上りメディアトラックを接続します。
- 対話終了時にキャプチャを停止し、接続を閉じてリソースを解放します。
注記
-
WebRTC のメディアトラックは音声と映像を転送します。アプリケーションはローカルトラックの取得、リモートプレーヤーの関連付け、対象モデルのイベント定義に従った DataChannel メッセージの送受信を行います。
-
WebRTC の認証は SDP 交換時に行うため、AOQ トークンの取得は不要です。長期 API キーをフロントエンドに置かないよう、AppServer 経由で認証と SDP を中継します。
-
接続成功はトランスポートの確立を意味しますが、メディアの送信には対象モデルの初期化成功を待つ必要があります。たとえば、Realtime モデルでは
session.updatedを待つ必要があります。クライアントはモデルの初期化成功後にのみ、必要な上りメディアを有効にできます。
1. 認証と接続パラメーターの準備
AppServer で対象モデル、リージョン、ワークスペースに応じた SDP 交換を設定します。以下は標準 Realtime 接続のパラメーターです。アプリケーション固有のパラメーターは、対応するベストプラクティスに従ってください。
| 設定項目 | 値 |
|---|---|
| メソッド | POST |
| URL | https://{endpoint}/api/v1/webrtc/realtime?model={model_name} |
Content-Type | application/sdp |
Authorization | Bearer <API_KEY> |
| リクエスト本文 | Offer SDP |
| 成功時の応答 | Answer SDP |
リージョン、エンドポイント、認証についてはトークン認証を参照してください。AppServer のインターフェイスはアプリケーション側で実装します。Model Studio SDK の API ではありません。
2. 接続オブジェクトの初期化とコールバックの登録
ブラウザーで RTCPeerConnection を作成するか、他のプラットフォームで対応するライブラリを初期化します。接続前に次のハンドラーを登録します。
-
成功、失敗、切断を含む接続状態の変化。
-
DataChannel の作成、オープン、メッセージ、クローズ。
-
音声や映像を受信、再生するためのリモートメディアトラック。
const pc = new RTCPeerConnection({ iceServers: [ ] });
const channels = new Set();
const senders = new Map();
const remoteAudio = document.createElement("audio");
remoteAudio.controls = true; // Allow manual playback if autoplay is blocked.
remoteAudio.autoplay = true;
document.body.appendChild(remoteAudio);
let localStream = null;
let eventChannel = null;
let stopped = false;
let started = false;
let sessionCreated = false;
let updateSent = false;
let modelReady = false;
let mediaStarted = false;
const requestController = new AbortController();
サーバーが送信するセッションイベントを取りこぼさないよう、モデル初期化前にメッセージリスナーを登録します。クライアントが作成したチャネルだけを監視しないでください。サーバーは txt というチャネルでイベントを送るため、datachannel コールバックも処理します。
3. メディア方向と DataChannel の設定
メディア方向の選択
方向はクライアントから見たものです。WebRTC トランシーバーで送受信をネゴシエートします。sendrecv は双方向、sendonly は送信のみ、recvonly は受信のみです。利用可能な機能はサーバーの SDP 応答とモデルの対応状況にも依存します。
| 方向 | 設定 |
|---|---|
| 音声送信 | 音声送信をネゴシエートし、マイクまたは外部音声トラックを準備して、モデルの準備完了後に接続 |
| 音声受信 | 音声受信をネゴシエートし、リモートトラックのコールバックで再生を関連付け |
| 映像送信 | モデルが視覚入力に対応する場合のみ映像送信をネゴシエートし、カメラまたは外部映像トラックを準備 |
| 映像受信 | モデルまたはアプリケーションが映像出力を明示的にサポートする場合のみ、受信のネゴシエートと描画を実装 |
音声会話には通常、双方向音声が必要です。視覚入力には上り映像も必要です。Offer SDP には m=audio メディアセクションを含める必要があります。アプリケーションで音声再生が不要という理由だけで削除しないでください。
モデルに応じた方向の選択
| モデルまたはアプリケーション | 音声送信 | 音声受信 | 映像送信 | 映像受信 |
|---|---|---|---|---|
| Qwen-Omni-Realtime | 音声入力時 | 音声応答時 | 視覚入力時 | 非対応 |
| Qwen-Audio-Realtime | 音声会話時 | 音声応答時 | 非対応 | 非対応 |
| Qwen-LiveTranslate-Realtime | 音声入力時 | 音声翻訳出力時 | バージョンとシナリオに依存 | 非対応 |
| multimodal-dialog | アプリケーションの入力に応じる | アプリケーションの出力に応じる | アプリケーションの機能に応じる | アプリケーションが明示的にサポートする場合のみ |
この表はアプリケーションに必要なメディア方向を示すもので、すべての組み合わせを個別にネゴシエートできることを保証するものではありません。実際の設定は対象モデルとサーバーのネゴシエーション機能に依存します。
送信せずにメディアを準備
Offer 作成前に必要な方向をネゴシエートします。初期化確認が必要なモデルでは、取得したトラックを接続せずに送信機能を確保します。モデルの準備完了後、対応する sender の replaceTrack で送信を開始します。メディア受信ハンドラーは事前に登録してください。
音声送信、音声受信、映像送信、映像受信は独立して設定します。マイクの起動、音声の再生、モデルの初期化は別々の操作です。
メディア設定の例
この例は双方向音声と任意の上り映像を使用します。4 つの media スイッチはアプリケーションの要件を表します。映像受信は対象アプリケーションが明示的に対応する場合のみ有効にします。addTransceiver で事前に方向をネゴシエートし、キャプチャしたトラックはモデルの準備完了後に sender に接続するまでローカルに保持します。
const media = {
sendAudio: true,
receiveAudio: true,
sendVideo: false,
receiveVideo: false,
};
function mediaDirection(send, receive) {
if (send && receive) return "sendrecv";
if (send) return "sendonly";
if (receive) return "recvonly";
return "inactive";
}
async function configureMedia() {
for (const kind of ["audio", "video"]) {
const send = kind === "audio" ? media.sendAudio : media.sendVideo;
const receive = kind === "audio" ? media.receiveAudio : media.receiveVideo;
// Keep m=audio; the target service must support the selected directions.
if (kind === "audio" || send || receive) {
const transceiver = pc.addTransceiver(kind, {
direction: mediaDirection(send, receive),
});
if (send) senders.set(kind, transceiver.sender);
}
}
if (!media.sendAudio && !media.sendVideo) return;
const stream = await navigator.mediaDevices.getUserMedia({
audio: media.sendAudio,
video: media.sendVideo ? {
width: { ideal: 640 }, height: { ideal: 480 },
frameRate: { ideal: 2, max: 2 },
} : false,
});
// The user may end the session while the permission prompt is open.
if (stopped) {
stream.getTracks().forEach(track => track.stop());
throw new Error("Session ended");
}
localStream = stream;
// sender.track is still null; captured data is not sent to the model.
}
映像の解像度とフレームレートは対象モデルに従って設定します。ローカルプレビューと上り映像のフレームレートを分ける場合は、ベストプラクティスの Canvas 実装を参照してください。
イベントチャネルの作成
SDP にデータチャネルのネゴシエーションが含まれるよう、Offer の前に DataChannel を作成します。クライアント側とサーバー側で作成するチャネルについては、後述の WebRTC ベストプラクティスを参照してください。
DataChannel はモデル初期化、テキスト、制御イベントを転送し、モデルの結果、状態、エラーを返します。音声と映像はメディアトラックで転送します。
// See Send and receive events over DataChannels for bindDataChannel.
pc.ondatachannel = ({ channel }) => bindDataChannel(channel);
const clientChannel = pc.createDataChannel("oai-events");
bindDataChannel(clientChannel);
4. 接続の確立
createOfferとsetLocalDescriptionを呼び出します。- サーバーが対応する ICE モードで候補を収集します。この例では ICE 収集完了を待ってからローカル SDP を送信します。
- ローカル SDP を AppServer に送信します。AppServer は API キーを使って Model Studio の SDP 交換エンドポイントを呼び出します。
- HTTP ステータスを確認し、成功時は返された Answer SDP を
setRemoteDescriptionに渡します。 - 接続成功を待ち、モデルイベント送信用の DataChannel が開いていることを確認します。
SDP 交換の成功や setRemoteDescription の完了は、そのネゴシエーション段階の完了にすぎず、メディア接続の準備完了を意味しません。DataChannel が open になるまでモデルイベントは送信できません。
接続コードと状態コールバック
connectionstatechange で接続状態を判定し、icegatheringstatechange で完全なローカル SDP を待ちます。モデルの初期化には DataChannel が開いていることとモデルイベントも必要で、SDP 交換だけでは不十分です。
pc.onconnectionstatechange = () => {
console.log("WebRTC state:", pc.connectionState);
if (pc.connectionState === "connected") {
tryInitializeModel();
} else if (["failed", "disconnected", "closed"].includes(pc.connectionState)) {
// This example ends the session; implement recovery as needed.
endSession();
}
};
function waitForIceComplete() {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => finish(new Error("ICE gathering timed out")), 15000);
function finish(error) {
clearTimeout(timer);
pc.removeEventListener("icegatheringstatechange", check);
requestController.signal.removeEventListener("abort", cancel);
error ? reject(error) : resolve();
}
function check() {
if (pc.iceGatheringState === "complete") finish();
}
function cancel() { finish(new Error("Session ended")); }
pc.addEventListener("icegatheringstatechange", check);
requestController.signal.addEventListener("abort", cancel, { once: true });
if (stopped) cancel();
else check();
});
}
function normalizeAnswerSdp(sdp) {
return String(sdp).trim().replace(/\r?\n/g, "\r\n") + "\r\n";
}
async function startSession() {
if (started || stopped) return;
started = true;
try {
await configureMedia();
if (stopped) return;
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
await waitForIceComplete();
// Implement this same-origin application endpoint; it is not a Model Studio API.
// AppServer forwards SDP with the API key and returns the raw Answer SDP.
const response = await fetch("/api/realtime/sdp", {
method: "POST",
headers: { "Content-Type": "application/sdp" },
body: pc.localDescription.sdp,
signal: requestController.signal,
});
if (!response.ok) {
throw new Error(`SDP exchange failed:${response.status} ${await response.text()}`);
}
const answerSdp = normalizeAnswerSdp(await response.text());
if (stopped) return;
await pc.setRemoteDescription({ type: "answer", sdp: answerSdp });
// Continue through connectionstatechange, DataChannel, and model events.
} catch (error) {
if (!stopped) console.error("Connection failed:", error);
endSession();
}
}
AppServer はセクション 1 のエンドポイントとヘッダーで Offer を転送する必要があります。例のインターフェイスはプレーンテキストの SDP を返します。setRemoteDescription に HTTP ヘッダー、ログ、JSON ラッパーを渡さないでください。
5. モデルイベントとメディアの交換
モデルの初期化
接続とイベントチャネルが利用可能になったら、対象モデルのプロトコルに従って初期化します。サーバーの初期化イベントを保持し、チャネル状態と組み合わせて処理を進めます。コールバックの到着順序が固定されているとは想定しないでください。
たとえば session.update で設定するモデルでは、プロトコルに準拠するイベントを送り、確認を受けてから入力を送信します。音声、出力モダリティ、VAD、サンプルレートは現在のモデルドキュメントを参照してください。マルチモーダル対話スイートは独自のイベントプロトコルを使用します。
session.created はセッション作成を意味し、設定の反映完了を必ずしも意味しません。設定確認が必要なモデルでは session.updated を待ってください。他のモデルやアプリケーションには、それぞれ固有の準備完了条件があります。
WebSocket 接続の概要のモデルナビゲーションからイベント定義を参照できます。再利用するのはイベント定義のみで、WebRTC のメディア転送と制約は引き続きこのページに従います。
モデルイベントの例:Omni
この例は Omni の session.created → session.update → session.updated フローを示します。他のモデルでは、以下のリンク先のイベント定義に従い、初期化イベントと応答処理を変更してください。session.updated で設定を確認してから上りメディアトラックを接続します。
function tryInitializeModel() {
if (stopped || updateSent || !sessionCreated ||
pc.connectionState !== "connected" ||
eventChannel?.readyState !== "open") return;
sendModelEvent({
event_id: `event_${crypto.randomUUID()}`,
type: "session.update",
session: {
modalities: media.receiveAudio ? ["text", "audio"] : ["text"],
input_audio_format: "pcm",
output_audio_format: "pcm",
turn_detection: { type: "server_vad", threshold: 0.5,
silence_duration_ms: 800 },
},
});
updateSent = true;
}
async function handleModelEvent(event, channel) {
if (stopped) return;
if (event.type === "session.created") {
sessionCreated = true;
eventChannel = channel; // Send configuration on the channel that delivered the session event.
tryInitializeModel();
} else if (event.type === "session.updated" && updateSent) {
modelReady = true;
await startSendingMedia();
} else if (event.type === "error") {
console.error("Model error:", event.error);
endSession();
} else {
// Handle text, transcripts, and response state; receive audio on media tracks.
console.log("Model event:", event);
}
}
メディア送受信の開始
モデルの準備完了後、上り送信に必要な音声と映像のトラックを接続します。リモートトラックのコールバックでモデル出力を処理し、DataChannel イベントの解析も継続します。
-
音声は RTP メディアトラックで転送するため、
input_audio_buffer.appendは不要です。 -
画像は映像トラックで転送します。WebRTC は
input_image_buffer.appendに対応していません。 -
DataChannel はテキスト、状態、制御、エラーイベントを転送します。
async function startSendingMedia() {
if (stopped || !modelReady || mediaStarted) return;
mediaStarted = true;
for (const track of localStream?.getTracks() ?? []) {
if (stopped) return;
const sender = senders.get(track.kind);
if (sender) await sender.replaceTrack(track);
}
}
// Register before startSession(); also handle tracks with no streams.
const remoteAudioStream = new MediaStream();
const remoteVideo = media.receiveVideo ? document.createElement("video") : null;
if (remoteVideo) {
remoteVideo.autoplay = true;
remoteVideo.playsInline = true;
remoteVideo.controls = true;
document.body.appendChild(remoteVideo);
}
pc.ontrack = ({ track }) => {
if (stopped) return;
if (track.kind === "audio" && media.receiveAudio) {
remoteAudioStream.addTrack(track);
remoteAudio.srcObject = remoteAudioStream;
remoteAudio.play().catch(() => {
console.info("Autoplay blocked. Use the audio controls to play.");
});
}
if (track.kind === "video" && remoteVideo) {
remoteVideo.srcObject = new MediaStream([track]);
remoteVideo.play().catch(() => console.info("Use the video controls to play."));
}
};
6. 対話の終了とリソースの解放
接続が不要になったら、アプリケーションが所有するローカルキャプチャトラックを停止し、DataChannel と RTCPeerConnection を閉じ、プレーヤーを解放してアプリケーション状態をクリアします。
接続失敗や予期しない切断後はモデルの準備完了状態をクリアします。新しい接続では以前のセッション状態を再利用せず、モデルを再初期化してください。
function endSession() {
if (stopped) return;
stopped = true;
modelReady = false;
sessionCreated = false;
updateSent = false;
mediaStarted = false;
requestController.abort(); // Cancel the pending SDP request and ICE wait.
localStream?.getTracks().forEach(track => track.stop());
localStream = null;
channels.forEach(channel => channel.close());
channels.clear();
eventChannel = null;
pc.close();
senders.clear();
remoteAudioStream.getTracks().forEach(track => track.stop());
remoteAudio.pause();
remoteAudio.srcObject = null;
remoteAudio.remove();
if (remoteVideo) {
remoteVideo.srcObject?.getTracks().forEach(track => track.stop());
remoteVideo.pause();
remoteVideo.srcObject = null;
remoteVideo.remove();
}
}
ユーザーが通話を終了するかページを離れる際に endSession() を呼び出します。ベストプラクティスの Canvas や録画機能を追加した場合は、アニメーションループも取り消し、Canvas のメディアトラックと MediaRecorder を停止してください。
DataChannel でのイベント送受信
シリアライズしたイベントを send で送る前に、チャネルが open であることを確認します。サーバーが作成したチャネルも含め、受信メッセージは message コールバックで解析します。
送信するイベント名、フィールド、パラメーター、タイミングは対象モデルのクライアントイベント定義に従ってください。応答の解析、状態変化、結果、エラーはサーバーイベント定義に従います。モデルに必要な初期化イベントを送り、その後はアプリケーションに必要なテキストや制御イベントを送信します。
DataChannel の例
クライアントが作成したチャネルとサーバーが作成したチャネルに同じバインド関数を使用します。この関数は転送と JSON 解析を担当し、セクション 5 の handleModelEvent がモデルの意味に応じた処理を行います。sendModelEvent は、モデルのプロトコルで許可される場合にのみ初期化、テキスト、制御イベントに使用します。
function sendModelEvent(event, channel = eventChannel) {
if (stopped || pc.connectionState !== "connected" ||
channel?.readyState !== "open") {
throw new Error("Data channel is not ready");
}
channel.send(JSON.stringify(event));
}
function bindDataChannel(channel) {
channels.add(channel);
channel.onopen = () => {
if (stopped) return;
console.log("DataChannel opened:", channel.label);
tryInitializeModel();
};
channel.onmessage = ({ data }) => {
if (stopped) return;
let event;
try { event = JSON.parse(data); }
catch (error) {
console.warn("Cannot parse model event:", error);
return;
}
if (!event || typeof event !== "object") return;
handleModelEvent(event, channel).catch(error => {
if (!stopped) console.error("Model event handling failed:", error);
endSession();
});
};
channel.onerror = error => console.error("DataChannel error:", error);
channel.onclose = () => {
channels.delete(channel);
if (channel === eventChannel) endSession();
};
if (channel.readyState === "open") channel.onopen();
}
すべてのコード断片を同じスクリプトに配置します。宣言とコールバックの登録後、開始ボタンのハンドラーから startSession()、終了ボタンのハンドラーから endSession() を呼び出してください。これらは例で定義した関数であり、WebRTC の組み込み API ではありません。
モデルイベントの定義
| モデルまたはアプリケーション | クライアントイベント (送信) | サーバーイベント (受信) |
|---|---|---|
| Qwen-Omni-Realtime | クライアントイベント | サーバーイベント |
| Qwen-Audio-Realtime | クライアントイベント | サーバーイベント |
| Qwen-LiveTranslate-Realtime | クライアントイベント | サーバーイベント |
| multimodal-dialog | 対話プロトコルの Input Message | 対話プロトコルの Output Message |
マルチモーダル対話スイートでは 1 つの対話プロトコルに両方向を定義しています。実際に使用するモデルバージョンのイベント定義を選択してください。すべてのモデルに共通する固定のイベントスキーマはありません。
次のルールにも従ってください。
-
モデルに応じてイベント種別と相関 ID を識別します。Realtime は通常
type、マルチモーダル対話スイートはクライアントイベントにheader.action、サーバーイベントにheader.event、関連付けにtask_idを使用します。 -
初期化確認、入力送信、応答キャンセル、タスク完了は、プロトコルに従って DataChannel で処理します。これらはトランスポート接続を閉じる操作とは異なります。
-
メディアにはメディアトラックを使用します。モデルイベントのドキュメントを参照する場合も、メディア転送と利用可能な対話モードはこの WebRTC ページに従い、WebSocket のメディアアップロード方式をコピーしないでください。
ベストプラクティス
- WebRTC による Omni リアルタイム通話:アプリケーション UI、録画、Canvas によるフレームレート削減。モデル初期化とメディアの接続はこのページの順序に従ってください。