Qwen-Omni-Realtime WebRTC アクセス
WebRTC はメディアトラックで音声と映像を、DataChannel でモデルイベントとテキストを転送します。Web アプリケーションはブラウザー標準 API を使用でき、その他のプラットフォームは標準 WebRTC に対応するライブラリを使用できます。Model Studio は専用 WebRTC SDK を提供していません。
このドキュメントでは、このモデルの WebRTC アクセス手順と、ブラウザー側の完全なサンプルを示します。
適用範囲
このドキュメントは Qwen-Omni-Realtime を対象とします。既存のアクセスドキュメントは qwen3.8-omni-flash-realtime、qwen3.5-omni-plus-realtime、qwen3.5-omni-flash-realtime を扱います。サンプルモデルは qwen3.8-omni-flash-realtime です。
- 選択したリージョンとワークスペースに対応する API キーとワークスペース IDを使用してください。
- 北京のドメイン:
{WorkspaceId}.cn-beijing.maas.aliyuncs.com。シンガポールのドメイン:{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com。 - 長期 API キーはサーバー側に保存し、ブラウザーやモバイルアプリケーションに含めないでください。一時 API キーの生成方法と権限は、一時 API キーの生成をご覧ください。
ブラウザーでメディアをキャプチャする場合は、安全なコンテキストを使用します。マイクやカメラを使用する場合は、対応する権限を要求します。音声受信時は、ブラウザーの自動再生制限に対応してください。
認証と SDP の交換
ブラウザーは業務 AppServer に Offer SDP を送信します。AppServer は API キーを使用して次のリクエストを転送し、Answer SDP をそのままブラウザーに返します。長期 API キーはフロントエンドに渡りません。
- メソッド:
POST - 北京のアドレス:
https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/webrtc/realtime?model=qwen3.8-omni-flash-realtime - シンガポールのアドレス:
https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/webrtc/realtime?model=qwen3.8-omni-flash-realtime - リクエストヘッダー:
Authorization: Bearer <API_KEY>、Content-Type: application/sdp - リクエストボディ:Offer SDP の文字列。
- 成功時のレスポンス:HTTP 200。ボディに Answer SDP が含まれます。失敗時は 4xx を返します。
WebRTC は SDP の交換時に認証を行うため、事前に AOQ Token を取得する必要はありません。以下のサンプルの /api/realtime/sdp は、業務 AppServer が構築するカスタムインターフェイスです。前述の転送処理を実装する必要があり、Model Studio が提供する URL ではありません。
処理フロー
- AppServer に API キーと対象モデルのパラメーターを用意します。
- クライアントの接続オブジェクトを作成し、コールバックを登録します。
- 上りメディアトラックを接続せずに、メディア方向と DataChannel を設定します。
- AppServer 経由で SDP を交換し、接続とイベントチャネルの準備完了を待ちます。
- モデルのプロトコルに従って設定します。確認後、必要な上りメディアトラックを接続します。
- 対話終了時にキャプチャを停止し、接続を閉じてリソースを解放します。
注記
-
WebRTC のメディアトラックは音声と映像を転送します。アプリケーションはローカルトラックの取得、リモートプレーヤーの関連付け、対象モデルのイベント定義に従った DataChannel メッセージの送受信を行います。
-
WebRTC の認証は SDP 交換時に行うため、AOQ トークンの取得は不要です。長期 API キーをフロントエンドに置かないよう、AppServer 経由で認証と SDP を中継します。
-
接続成功は転送チャネルの確立を示し、モデルの初期化完了を意味しません。このページの
qwen3.8-omni-flash-realtimeを使用する場合は、session.updateを送信し、session.updatedを受信してから、必要な上りメディアを開始します。
完全なサンプル
以下はブラウザー側の完全なサンプルです。まず、認証と SDP の交換の説明に従って業務 AppServer の /api/realtime/sdp インターフェイスを実装します。次に、業務 AppServer と同じオリジンでこの HTML ページを開き、プロトコル、ホスト、ポートを一致させます。たとえば、ページと /api/realtime/sdp の両方を http://localhost:3000 から配信します。API キーの保存と SDP の転送はサーバーが行い、長期 API キーをページに含めないでください。「Start」で接続し、対話の終了時に「End」でリソースを解放します。このサンプルは一度に 1 つの接続を確立します。別のセッションを開始するには、ページを再読み込みしてください。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebRTC access example</title>
</head>
<body>
<button id="start-session" type="button">Start</button>
<button id="end-session" type="button">End</button>
<script>
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();
const media = {
sendAudio: true,
receiveAudio: true,
sendVideo: 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 : false;
// 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 call 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 empty, so no captured data is sent to the model.
}
// See Send and receive events over DataChannels for bindDataChannel.
pc.ondatachannel = ({ channel }) => bindDataChannel(channel);
const clientChannel = pc.createDataChannel("oai-events");
bindDataChannel(clientChannel);
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();
}
}
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"],
"voice": "Tina",
"turn_detection": { "type": "server_vad" },
},
});
updateSent = true;
}
async function handleModelEvent(event, channel) {
if (stopped) return;
if (event.type === "session.created") {
sessionCreated = true;
eventChannel = channel; // Send configuration through the channel that receives session events.
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 {
// Display text, transcripts, or response status as defined by the selected model. Receive audio over media tracks.
console.log("Model event:", event);
}
}
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(). Tracks without streams are also received correctly.
const remoteAudioStream = new MediaStream();
pc.ontrack = ({ track }) => {
if (stopped) return;
if (track.kind === "audio" && media.receiveAudio) {
remoteAudioStream.addTrack(track);
remoteAudio.srcObject = remoteAudioStream;
remoteAudio.play().catch(() => {
console.info("Autoplay is blocked. Use the audio controls to play.");
});
}
};
function endSession() {
if (stopped) return;
stopped = true;
modelReady = false;
sessionCreated = false;
updateSent = false;
mediaStarted = false;
requestController.abort(); // Cancel pending SDP requests and ICE gathering.
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();
}
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();
}
document.getElementById("start-session").addEventListener("click", startSession);
document.getElementById("end-session").addEventListener("click", endSession);
</script>
</body>
</html>
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 |
リージョン、アドレス、認証の詳細は、認証と SDP の交換をご覧ください。業務 AppServer のインターフェイスは業務側で実装するもので、Model Studio の SDK API には含まれません。
2. 接続オブジェクトの初期化とコールバックの登録
Web 側では RTCPeerConnection を作成します。その他のプラットフォームでは、対応するライブラリを初期化します。接続前に、次のハンドラーを登録してください。
-
接続状態の変化:接続の成功、失敗、切断を判定します。
-
DataChannel の確立、オープン、メッセージ、クローズ:モデルイベントを扱います。
-
リモートメディアトラック:音声の受信と再生、または対象アプリケーションがサポートするその他のメディアの処理を行います。
完全なサンプルでは、同じスクリプト内で接続オブジェクト、状態変数、コールバックを作成します。1 回の呼び出しにつき 1 つの接続を作成し、終了後に再度呼び出す場合は、これらのオブジェクトを再初期化してください。
接続オブジェクトと状態変数の詳細は、完全なサンプルをご覧ください。
サーバーが自発的に送信するセッションイベントを取りこぼさないよう、モデル初期化前にメッセージリスナーを登録してください。クライアントが作成したチャネルだけを監視しないでください。サーバーは txt という名前のチャネルでイベントを送信するため、datachannel コールバックを処理する必要があります。
3. メディア方向と DataChannel の設定
メディア方向の選択
方向はクライアントから見たものです。WebRTC トランシーバーで送受信をネゴシエートします。sendrecv は双方向、sendonly は送信のみ、recvonly は受信のみです。利用可能な機能はサーバーの SDP 応答とモデルの対応状況にも依存します。
| 方向 | 設定 |
|---|---|
| 音声を送信 | 音声送信機能をネゴシエートし、マイクまたは外部音声トラックを準備します。モデルの準備が整ってからトラックを接続します。 |
| 音声を受信 | 音声受信機能をネゴシエートし、リモートトラックのコールバックで音声再生をバインド |
| 動画を送信 | 対象モデルが視覚入力に対応する場合、動画送信機能をネゴシエートし、カメラまたは外部動画トラックを準備 |
音声会話には通常、双方向音声が必要です。視覚入力には上り映像も必要です。Offer SDP には m=audio メディアセクションを含める必要があります。アプリケーションで音声再生が不要という理由だけで削除しないでください。
このモデルのメディア方向
| モデルまたはアプリケーション | 音声送信 | 音声受信 | 映像送信 | 映像受信 |
|---|---|---|---|---|
| Qwen-Omni-Realtime | 音声入力が必要な場合に有効化 | 音声応答が必要な場合に有効化 | 視覚入力が必要な場合に有効化 | 有効にできません |
この表はアプリケーションに必要なメディア方向を示すもので、すべての組み合わせを個別にネゴシエートできることを保証するものではありません。実際の設定は対象モデルとサーバーのネゴシエーション機能に依存します。
送信せずにメディアを準備
Offer 作成前に必要な方向をネゴシエートします。初期化確認が必要なモデルでは、取得したトラックを接続せずに送信機能を確保します。モデルの準備完了後、対応する sender の replaceTrack で送信を開始します。メディア受信ハンドラーは事前に登録してください。
音声送信、音声受信、動画送信は、必要に応じて個別に設定します。マイクの有効化、音声再生、モデル初期化を 1 つの操作にまとめないでください。
メディア設定の例
次の例は、任意の動画アップロードを含む双方向音声を示します。media の 3 つのスイッチが、音声送信、音声受信、動画送信をそれぞれ制御します。addTransceiver で方向を事前にネゴシエートし、キャプチャしたトラックはモデルの準備が整うまでローカルに保存してから送信側に接続します。
コードは、完全なサンプルをご覧ください。
動画の解像度とフレームレートは対象モデルに従って設定します。ローカルプレビューと上り送信のフレームレートを変える必要がある場合は、ベストプラクティスの Canvas 実装をご覧ください。
イベントチャネルの作成
Offer の作成前に DataChannel を作成して、SDP にデータチャネルのネゴシエーションを含めます。クライアントとサーバーがチャネルの作成を処理する方法は、データチャネルでのイベント送受信をご覧ください。
DataChannel はモデル初期化、テキスト、制御イベントを転送し、モデルの結果、状態、エラーを返します。音声と映像はメディアトラックで転送します。
完全なサンプルでは、Offer の作成前にクライアントイベントチャネルを作成し、サーバー側のチャネルコールバックを登録します。
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 の open とモデルの準備完了イベントも必要です。SDP 交換の完了だけで判断しないでください。
完全なサンプルでは、startSession() で SDP 交換を完了し、接続状態コールバックで接続結果を処理します。
業務 AppServer は、セクション 1 のアドレスとリクエストヘッダーに従って Offer を転送します。サンプル API はプレーンな SDP テキストを返す想定です。HTTP レスポンスヘッダー、ログ、JSON ラッパーを含む内容を、そのまま setRemoteDescription に渡さないでください。
5. モデルイベントとメディアの交換
モデルの初期化
接続とイベントチャネルが利用可能になったら、対象モデルのプロトコルに従って初期化します。サーバーの初期化イベントを保持し、チャネル状態と組み合わせて処理を進めます。コールバックの到着順序が固定されているとは想定しないでください。
たとえば、session.update でモデルを設定する場合は、そのプロトコルに従ってイベントを構築し、設定の確認を待ってから入力を送信します。音声、出力モダリティ、VAD、サンプリングレートなどのパラメーターは、現在のモデルのドキュメントに従ってください。
session.created はセッションの作成を示し、設定の適用完了を一律に示すものではありません。セッション設定の確認が必要なモデルでは、session.updated を待ってください。
モデルのイベント定義は、クライアントイベントとサーバーイベントをご覧ください。メディアの転送と利用可能なモードは、このページの説明に従ってください。
Qwen-Omni-Realtime を WebRTC で使用する場合、VAD モードのみをサポートし、Manual モードはサポートしません。音声は音声トラックで転送し、input_audio_buffer.append は送信しません。動画は動画トラックで転送し、input_image_buffer.append は送信しません。
モデルイベントの例
注記音声と動画はメディアトラックで転送し、WebSocket のメディア送信イベントは使用しません。
次の例は、qwen3.8-omni-flash-realtime の session.created → session.update → session.updated フローを示します。設定フィールドは、このモデルのイベントリファレンスに従います。設定完了を確認する session.updated を受信したら、上りメディアトラックを接続します。
コードは、完全なサンプルをご覧ください。
サンプルモデル qwen3.8-omni-flash-realtime では、音声出力を有効にすると response.audio.done でモデルの音声生成状態を確認できます。このイベントはクライアントの音声再生完了を示しません。
メディア送受信の開始
モデルの準備完了後、上り送信に必要な音声と映像のトラックを接続します。リモートトラックのコールバックでモデル出力を処理し、DataChannel イベントの解析も継続します。
-
音声は RTP メディアトラックで転送するため、
input_audio_buffer.appendは不要です。 -
画像は映像トラックで転送します。WebRTC は
input_image_buffer.appendに対応していません。 -
DataChannel はテキスト、状態、制御、エラーイベントを転送します。
完全なサンプルでは、startSendingMedia() が上りトラックを接続し、ontrack が受信メディアを処理します。
6. 対話の終了とリソースの解放
接続が不要になったら、アプリケーションが所有するローカルキャプチャトラックを停止し、DataChannel と RTCPeerConnection を閉じ、プレーヤーを解放してアプリケーション状態をクリアします。
接続失敗や予期しない切断後はモデルの準備完了状態をクリアします。新しい接続では以前のセッション状態を再利用せず、モデルを再初期化してください。
完全なサンプルでは、endSession() が接続とメディアリソースを解放します。
ユーザーが通話を終了するかページを離れる際に endSession() を呼び出します。ベストプラクティスの Canvas や録画機能を追加した場合は、アニメーションループも取り消し、Canvas のメディアトラックと MediaRecorder を停止してください。
データチャネルでのイベント送受信
シリアライズしたイベントを send で送信する前に、チャネルが open であることを確認します。サーバーが作成したチャネルのイベントも含め、受信メッセージを message コールバックで解析します。
送信するイベントの名前、フィールド、パラメーター、タイミングは、対象モデルのクライアントイベント定義に従います。受信後の解析、状態変化、結果、エラー処理は、モデルのサーバーイベント定義に従います。初期化イベントはモデルの要件に応じて送信し、その後のテキストイベントや制御イベントはアプリケーションの必要に応じて送信してください。
DataChannel の例
クライアントが作成したチャネルとサーバーが作成したチャネルに同じバインド関数を使用します。この関数は転送と JSON 解析を担当し、セクション 5 の handleModelEvent がモデルの意味に応じた処理を行います。sendModelEvent は、モデルのプロトコルで許可される場合にのみ初期化、テキスト、制御イベントに使用します。
完全なサンプルでは、sendModelEvent() と bindDataChannel() でイベントを送受信します。
完全なサンプルでは、開始ボタンが startSession() を、終了ボタンが endSession() を呼び出します。これらはサンプルで定義した関数で、WebRTC の組み込み API ではありません。
Qwen-Omni-Realtime のイベント定義
| モデルまたはアプリケーション | クライアントイベント(送信) | サーバーイベント(受信) |
|---|---|---|
| Qwen-Omni-Realtime | クライアントイベント | サーバーイベント |
次のルールにも従ってください。
-
イベントの
typeフィールドでモデルイベントを識別し、関連する識別子はこのモデルのイベントリファレンスに従って処理します。 -
モデルが必要とする初期化の確認、入力のコミット、レスポンスのキャンセル、タスクの完了などの操作は、対応するプロトコルに従って Data チャネル内で処理します。これらはモデルの対話動作であり、トランスポート接続の切断とは別の操作です。
-
音声と動画のデータはメディアトラックで転送します。モデルのイベントドキュメントを参照する場合も、メディアのカプセル化と利用可能な対話モードは、ここで説明するアクセス方法に従います。WebSocket のメディア転送方式をそのまま適用しないでください。