Connexion WebRTC à Qwen-Omni-Realtime
WebRTC transporte l’audio et la vidéo sur les pistes média, et les événements du modèle et le texte via les DataChannels. Les applications Web peuvent utiliser les API natives du navigateur ; les autres plateformes peuvent utiliser des bibliothèques WebRTC standard. Model Studio ne fournit pas de SDK WebRTC dédié.
Ce document décrit la procédure d’intégration WebRTC de ce modèle et fournit un exemple complet côté navigateur.
Périmètre d’application
Ce document concerne Qwen-Omni-Realtime. La documentation d’intégration existante couvre qwen3.8-omni-flash-realtime, qwen3.5-omni-plus-realtime et qwen3.5-omni-flash-realtime. Le modèle de l’exemple est qwen3.8-omni-flash-realtime.
- Utilisez l’API Key et l’ID de l’espace de travail correspondant à la région et à l’espace de travail sélectionnés.
- Domaine de Pékin :
{WorkspaceId}.cn-beijing.maas.aliyuncs.com; domaine de Singapour :{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com. - Les API Keys à longue durée de validité sont stockées côté serveur et ne doivent pas être incluses dans les applications mobiles ou de navigateur. Pour leur génération et leurs autorisations, consultez Générer une API Key temporaire.
Le navigateur utilise un contexte sécurisé pour capturer les médias ; demandez les autorisations correspondantes pour utiliser le microphone ou la caméra. Lors de la réception audio, gérez les restrictions de lecture automatique du navigateur.
Authentification et échange SDP
Le navigateur envoie une Offer SDP à l’AppServer métier. AppServer transmet la requête suivante avec l’API Key et renvoie l’Answer SDP sans modification au navigateur ; les API Keys à longue durée de validité ne parviennent pas au frontend.
- Méthode :
POST - Adresse de Pékin :
https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/webrtc/realtime?model=qwen3.8-omni-flash-realtime - Adresse de Singapour :
https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/webrtc/realtime?model=qwen3.8-omni-flash-realtime - En-têtes de requête :
Authorization: Bearer <API_KEY>,Content-Type: application/sdp - Corps de la requête : chaîne Offer SDP.
- Réponse réussie : HTTP 200, avec l’Answer SDP dans le corps ; un échec renvoie un code 4xx.
WebRTC effectue l’authentification pendant l’échange SDP et ne nécessite pas de jeton AOQ préalable. Dans les exemples ci-dessous, /api/realtime/sdp désigne une interface personnalisée de l’AppServer métier, qui doit implémenter le transfert décrit ci-dessus ; ce n’est pas une URL fournie par Model Studio.
Déroulement
- Préparez la clé API et les paramètres du modèle cible sur AppServer.
- Créez l’objet de connexion client et enregistrez les rappels.
- Configurez les directions des médias et les DataChannels sans attacher les pistes média montantes.
- Échangez les SDP via AppServer et attendez que la connexion et le canal d’événements soient prêts.
- Configurez le modèle selon son protocole. Attachez les pistes média montantes nécessaires après confirmation.
- Arrêtez la capture, fermez la connexion et libérez les ressources à la fin de l’interaction.
Remarque
-
Les pistes média WebRTC transportent l’audio et la vidéo. Votre application obtient les pistes locales, associe les lecteurs distants et échange les messages DataChannel selon les définitions d’événements du modèle cible.
-
L’authentification WebRTC s’effectue lors de l’échange SDP ; aucun jeton AOQ n’est nécessaire. Relayez l’authentification et les SDP via AppServer pour ne pas exposer la clé API à long terme dans le code frontend.
-
Une connexion réussie indique que le canal de transmission est établi, mais ne signifie pas que le modèle a terminé son initialisation. Avec
qwen3.8-omni-flash-realtimeutilisé sur cette page, n’activez les médias en amont nécessaires qu’après avoir envoyésession.updateet reçusession.updated.
Exemple complet
Voici un exemple complet côté navigateur. Implémentez d’abord l’interface /api/realtime/sdp de l’AppServer métier comme décrit dans Authentification et échange SDP, puis ouvrez cette page HTML depuis la même origine que l’AppServer, avec le même protocole, hôte et port. Par exemple, la page et /api/realtime/sdp sont toutes deux servies par http://localhost:3000. Le serveur stocke l’API Key et transfère les données SDP ; n’incluez pas d’API Keys à longue durée de validité dans la page. Cliquez sur “Start” pour établir la connexion, puis sur “End” pour libérer les ressources à la fin de l’interaction. Cet exemple établit une connexion à la fois ; rechargez la page pour démarrer une autre session.
<!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. Préparer l’authentification et les paramètres de connexion
AppServer configure l’échange SDP selon le modèle cible, la région et l’espace de travail. Les paramètres suivants concernent les connexions Realtime standard. Pour les paramètres propres à une application, suivez la bonne pratique correspondante.
| Setting | Value |
|---|---|
| Méthode | POST |
| URL | https://{endpoint}/api/v1/webrtc/realtime?model={model_name} |
Content-Type | application/sdp |
Authorization | Bearer <API_KEY> |
| Corps de la requête | Offer SDP |
| Réponse réussie | Answer SDP |
Pour les régions, les adresses et l’authentification, consultez Authentification et échange SDP. L’interface de l’AppServer métier est implémentée par l’entreprise et ne fait pas partie de l’API SDK de Model Studio.
2. Initialiser l’objet de connexion et enregistrer les callbacks
Côté web, créez une RTCPeerConnection ; sur les autres plateformes, initialisez la bibliothèque correspondante. Avant d’établir la connexion, enregistrez les gestionnaires suivants :
-
Changements d’état de la connexion : déterminez si la connexion a réussi, a échoué ou a été interrompue.
-
Création, ouverture, messages et fermeture de DataChannel : transporte les événements du modèle.
-
Piste de médias distante : reçoit et lit l’audio, ou traite les autres médias pris en charge par l’application cible.
L’exemple complet crée l’objet de connexion, les variables d’état et les callbacks dans le même script. Chaque appel crée une seule connexion ; une fois l’appel terminé, réinitialisez ces objets pour effectuer un autre appel.
L’objet de connexion et les variables d’état sont détaillés dans Exemple complet.
Enregistrez les écouteurs de messages avant l’initialisation du modèle pour éviter de manquer les événements de session envoyés spontanément par le serveur. N’écoutez pas uniquement les canaux créés par le client : le serveur transmet les événements sur un canal nommé txt ; vous devez donc traiter le callback datachannel.
3. Configurer les directions des médias et les DataChannels
Sélectionner la direction des médias
Les directions sont relatives au client. Les émetteurs-récepteurs WebRTC négocient l’envoi et la réception : sendrecv est bidirectionnel, sendonly envoie uniquement et recvonly reçoit uniquement. Les capacités disponibles dépendent aussi de la réponse SDP du serveur et de la prise en charge du modèle.
| Direction | Configuration |
|---|---|
| Envoyer l’audio | Négociez l’envoi audio et préparez le microphone ou une piste audio externe. Ajoutez la piste une fois le modèle prêt. |
| Recevoir l’audio | Négociez la réception audio et configurez la lecture dans le callback de réception d’une piste distante. |
| Envoyer la vidéo | Lorsque le modèle cible prend en charge les entrées visuelles, négociez l’envoi vidéo et préparez la caméra ou une piste vidéo externe. |
Les conversations vocales nécessitent généralement un audio bidirectionnel ; une entrée visuelle nécessite également une vidéo montante. L’Offer SDP doit contenir une section média m=audio. Ne la supprimez pas simplement parce que l’application n’a pas besoin de lecture audio.
Directions des médias pour ce modèle
| Modèle ou application | Envoyer de l’audio | Recevoir de l’audio | Envoyer de la vidéo | Recevoir de la vidéo |
|---|---|---|---|---|
| Qwen-Omni-Realtime | activez lorsqu’une entrée vocale est nécessaire | activez lorsqu’une réponse vocale est nécessaire | activez lorsqu’une entrée visuelle est nécessaire | ne peut pas être activé |
Ce tableau décrit les directions des médias nécessaires à l’application ; il ne garantit pas que toutes les combinaisons puissent être négociées indépendamment. La configuration dépend du modèle cible et des capacités de négociation du serveur.
Préparer les médias sans les envoyer
Négociez les directions requises avant de créer l’Offer. Si le modèle nécessite une confirmation d’initialisation, réservez les capacités d’envoi sans attacher les pistes capturées. Lorsque le modèle est prêt, utilisez replaceTrack sur l’émetteur correspondant pour démarrer la transmission. Enregistrez à l’avance les gestionnaires de réception.
Configurez séparément l’envoi audio, la réception audio et l’envoi vidéo selon les besoins ; ne regroupez pas l’activation du microphone, la lecture audio et l’initialisation du modèle dans une seule opération.
Exemple de configuration des médias
L’exemple suivant présente une conversation vocale bidirectionnelle avec un envoi vidéo facultatif. Les trois options dans media contrôlent respectivement l’envoi audio, la réception audio et l’envoi vidéo. addTransceiver négocie la direction à l’avance ; les pistes capturées sont d’abord stockées localement jusqu’à ce que le modèle soit prêt, puis associées à l’émetteur.
Consultez le code dans l’exemple complet.
Définissez la résolution vidéo et la fréquence d’images selon le modèle cible. Si l’aperçu local et le flux montant nécessitent des fréquences différentes, consultez l’implémentation Canvas dans la bonne pratique.
Créer les canaux d’événements
Créez un DataChannel avant de créer une Offer pour que la SDP inclue la négociation du canal de données. Consultez Envoi et réception d’événements sur le canal Data pour savoir comment le client et le serveur créent les canaux.
Les DataChannels transportent l’initialisation du modèle, le texte et les événements de contrôle, et renvoient les résultats, états et erreurs. Les pistes média transportent l’audio et la vidéo.
L’exemple complet crée un canal d’événements client et enregistre le callback du canal serveur avant de créer une Offer.
4. Établir la connexion
Effectuez la connexion dans l’ordre suivant :
- Appelez
createOfferetsetLocalDescription. - Collectez les informations sur les candidats selon les méthodes ICE prises en charge par le serveur ; dans cet exemple, la SDP locale est envoyée une fois la collecte ICE terminée.
- Envoyez la SDP locale à l’AppServer métier, qui utilise l’API Key pour appeler l’interface d’échange SDP de Model Studio.
- Vérifiez l’état HTTP et, en cas de succès, appelez
setRemoteDescriptionavec l’Answer SDP renvoyée. - Attendez que l’état de la connexion indique un succès et confirmez que le DataChannel utilisé pour envoyer les événements du modèle est ouvert.
La réussite de l’échange SDP ou la fin de setRemoteDescription indique uniquement que l’étape de négociation correspondante est terminée ; elle ne prouve pas que la connexion des médias a réussi. Les événements du modèle ne peuvent pas non plus être envoyés si DataChannel n’est pas à l’état open.
Code de connexion et callbacks d’état
Écoutez connectionstatechange pour déterminer l’état de la connexion et icegatheringstatechange pour attendre la SDP locale complète. L’initialisation du modèle nécessite également l’état open de DataChannel et l’événement de disponibilité du modèle ; elle ne doit pas dépendre uniquement de la fin de l’échange SDP.
L’exemple complet effectue l’échange SDP avec startSession() et traite le résultat de la connexion dans le callback d’état de la connexion.
L’AppServer métier doit transférer l’Offer selon l’adresse et les en-têtes de requête de la section 1 ; l’API d’exemple doit renvoyer du texte SDP brut. Ne transmettez pas directement à setRemoteDescription du contenu incluant des en-têtes de réponse HTTP, des journaux ou des enveloppes JSON.
5. Échanger les événements du modèle et les médias
Initialiser le modèle
Lorsque la connexion et le canal d’événements sont disponibles, initialisez le modèle cible selon son protocole. Conservez les événements d’initialisation serveur et combinez-les avec l’état du canal pour avancer ; ne supposez pas un ordre fixe d’arrivée des rappels.
Par exemple, lorsque vous configurez un modèle avec session.update, construisez l’événement selon son protocole et attendez la confirmation de configuration avant d’envoyer les entrées. Les paramètres de voix, de modalités de sortie, de VAD et de fréquence d’échantillonnage doivent tous provenir de la documentation du modèle actuel.
session.created indique la création de la session ; il ne signifie pas systématiquement que la configuration est appliquée. Pour les modèles exigeant une confirmation de la configuration de session, attendez session.updated.
Les définitions des événements du modèle figurent dans Événements client et Événements serveur. Gérez la transmission des médias et les modes disponibles selon les instructions de cette page.
Avec WebRTC, Qwen-Omni-Realtime prend uniquement en charge le mode VAD, sans prise en charge du mode Manual. L’audio est transmis sur la piste audio, sans envoyer input_audio_buffer.append ; la vidéo est transmise sur la piste vidéo, sans envoyer input_image_buffer.append.
Exemples d’événements du modèle
RemarqueL’audio et la vidéo sont transmis sur les pistes de médias sans utiliser les événements WebSocket d’envoi des médias.
L’exemple suivant présente le déroulement session.created → session.update → session.updated pour qwen3.8-omni-flash-realtime. Configurez les champs selon la référence des événements de ce modèle. Après avoir reçu session.updated, qui confirme la fin de la configuration, associez la piste de médias en amont.
Consultez le code dans l’exemple complet.
Pour le modèle d’exemple qwen3.8-omni-flash-realtime, lorsque la sortie audio est activée, response.audio.done permet de vérifier l’état de génération audio du modèle ; cet événement ne signifie pas que la lecture audio du client est terminée.
Démarrer la transmission et la réception des médias
Lorsque le modèle est prêt, attachez les pistes audio et vidéo nécessaires à l’envoi. Traitez les sorties du modèle dans le rappel de piste distante et continuez à analyser les événements DataChannel.
-
L’audio transite par les pistes média RTP ; aucun événement
input_audio_buffer.appendn’est nécessaire. -
Les images transitent par les pistes vidéo. WebRTC ne prend pas en charge
input_image_buffer.append. -
Les DataChannels transportent le texte et les événements d’état, de contrôle et d’erreur.
Dans l’exemple complet, startSendingMedia() associe la piste en amont et ontrack traite les médias reçus.
6. Terminer l’interaction et libérer les ressources
Lorsque la connexion n’est plus nécessaire, arrêtez les pistes de capture locales de votre application, fermez les DataChannels et le RTCPeerConnection, libérez les lecteurs et effacez l’état de l’application.
Effacez l’état de disponibilité du modèle après un échec ou une déconnexion inattendue. Initialisez à nouveau le modèle sur une nouvelle connexion au lieu de réutiliser l’état de session précédent.
Dans l’exemple complet, endSession() libère la connexion et les ressources de médias.
Appelez endSession() lorsque l’utilisateur termine l’appel ou quitte la page. Si l’application ajoute les fonctionnalités Canvas ou d’enregistrement de la bonne pratique, annulez également les boucles d’animation et arrêtez les pistes média Canvas ainsi que le MediaRecorder.
Envoyer et recevoir des événements sur le canal Data
Vérifiez que le canal est open avant d’appeler send avec un événement sérialisé. Analysez les messages entrants dans son rappel message, y compris sur les canaux créés par le serveur.
Les noms, champs, paramètres et moments d’envoi des événements suivent les définitions des événements client du modèle cible ; l’analyse, les changements d’état, les résultats et la gestion des erreurs après réception suivent les définitions des événements serveur du modèle. Les événements d’initialisation sont envoyés selon les exigences du modèle, puis les événements textuels et de contrôle selon les besoins métier.
Exemple DataChannel
Utilisez la même fonction de liaison pour les canaux créés par le client et le serveur. Elle gère le transport et l’analyse JSON ; handleModelEvent de la section 5 traite la sémantique du modèle. Utilisez sendModelEvent pour l’initialisation, le texte ou le contrôle uniquement lorsque le protocole du modèle le permet.
Dans l’exemple complet, sendModelEvent() et bindDataChannel() envoient et reçoivent les événements.
Dans l’exemple complet, le bouton de démarrage appelle startSession() et le bouton de fin appelle endSession(). Ce sont des fonctions définies dans cet exemple, et non des API WebRTC intégrées.
Définitions des événements Qwen-Omni-Realtime
| Modèle ou application | Événements client (envoi) | Événements serveur (réception) |
|---|---|---|
| Qwen-Omni-Realtime | Événements client | Événements serveur |
Respectez également les règles suivantes :
-
Utilisez le champ
typedes événements pour identifier les événements du modèle et traitez les identifiants associés selon la référence des événements de ce modèle. -
Les opérations requises par le modèle, telles que la confirmation de l’initialisation, la soumission des entrées, l’annulation des réponses et la fin des tâches, sont toutes traitées sur le canal Data selon le protocole correspondant ; elles définissent les interactions du modèle et sont distinctes de la déconnexion du transport.
-
Les données audio et vidéo sont transmises sur les pistes de médias. Lorsque vous consultez la documentation des événements du modèle, l’encapsulation des médias et les modes d’interaction disponibles restent définis par les méthodes de connexion décrites ici ; ils n’appliquent pas directement la méthode WebSocket d’envoi des médias.