Skip to main content

Qwen-Omni-Realtime Conexión mediante WebRTC

Qwen-Omni-Realtime Conexión mediante WebRTC

WebRTC transporta audio y video en pistas multimedia, y eventos de modelo y texto a través de DataChannels. Las aplicaciones web pueden usar las API nativas del navegador; otras plataformas pueden utilizar bibliotecas WebRTC estándar. Model Studio no proporciona un SDK dedicado para WebRTC. Este documento proporciona el procedimiento de integración de WebRTC para este modelo y un ejemplo completo del lado del navegador.

Ámbito de aplicación

Este documento está dirigido a Qwen-Omni-Realtime. La documentación de integración existente abarca qwen3.8-omni-flash-realtime, qwen3.5-omni-plus-realtime y qwen3.5-omni-flash-realtime. El modelo del ejemplo es qwen3.8-omni-flash-realtime.
  • Use la API Key y el ID del espacio de trabajo correspondientes a la región y al espacio de trabajo seleccionados.
  • Dominio de Pekín: {WorkspaceId}.cn-beijing.maas.aliyuncs.com; dominio de Singapur: {WorkspaceId}.ap-southeast-1.maas.aliyuncs.com.
  • Las API Keys de larga duración se almacenan en el servidor y no deben incluirse en aplicaciones móviles ni de navegador. Para obtener instrucciones sobre la generación de API Keys temporales y sus permisos, consulte Generar una API Key temporal.
El navegador usa un contexto seguro para capturar medios; solicite los permisos correspondientes al usar el micrófono o la cámara. Al recibir audio, maneje las restricciones de reproducción automática del navegador.

Autenticación e intercambio de SDP

El navegador envía una Offer SDP al AppServer de negocio. AppServer reenvía la siguiente solicitud con la API Key y devuelve la Answer SDP sin cambios al navegador; las API Keys de larga duración no llegan al frontend.
  • Método: POST
  • Dirección de Pekín: https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1/webrtc/realtime?model=qwen3.8-omni-flash-realtime
  • Dirección de Singapur: https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/webrtc/realtime?model=qwen3.8-omni-flash-realtime
  • Encabezados de la solicitud: Authorization: Bearer <API_KEY>, Content-Type: application/sdp
  • Cuerpo de la solicitud: cadena Offer SDP.
  • Respuesta exitosa: HTTP 200, con la Answer SDP en el cuerpo; si se produce un error, se devuelve un código 4xx.
WebRTC realiza la autenticación durante el intercambio de SDP y no requiere un token de AOQ previo. En los ejemplos siguientes, /api/realtime/sdp hace referencia a una interfaz personalizada del AppServer de negocio, que debe implementar el proceso de reenvío descrito arriba; no es una URL proporcionada por Model Studio.

Flujo de trabajo

Secuencia de conexión WebRTC
  1. Prepare la clave de API y los parámetros del modelo objetivo en AppServer.
  2. Crear el objeto de conexión del cliente y registrar devoluciones de llamada.
  3. Configure las direcciones de medios y los DataChannels sin adjuntar pistas de medios ascendentes.
  4. Intercambie SDP a través de AppServer y espere a que la conexión y el canal de eventos estén listos.
  5. Configure el modelo según su protocolo. Adjunte las pistas de medios ascendentes necesarias después de la confirmación.
  6. Detenga la captura, cierre la conexión y libere los recursos cuando la interacción haya terminado.
  • Las pistas multimedia de WebRTC transportan audio y video. Su aplicación obtiene pistas multimedia locales, adjunta reproductores remotos e intercambia mensajes de DataChannel según las definiciones de eventos del modelo objetivo.
  • La autenticación de WebRTC se realiza durante el intercambio de SDP; no se requiere ningún token AOQ. Utilice AppServer como proxy para la autenticación y el SDP a fin de mantener la clave API de largo plazo fuera del código frontend.
  • Una conexión exitosa indica que se estableció el canal de transmisión, pero no significa que el modelo haya terminado la inicialización. Al usar qwen3.8-omni-flash-realtime en esta página, inicie los medios ascendentes necesarios solo después de enviar session.update y recibir session.updated.

Ejemplo completo

El siguiente es un ejemplo completo del lado del navegador. Primero implemente la interfaz /api/realtime/sdp del AppServer de negocio como se describe en Autenticación e intercambio de SDP; después abra esta página HTML desde el mismo origen que el AppServer, con el mismo protocolo, host y puerto. Por ejemplo, tanto la página como /api/realtime/sdp se sirven desde http://localhost:3000. El servidor almacena la API Key y reenvía el SDP; no incluya API Keys de larga duración en la página. Haga clic en “Start” para establecer la conexión y en “End” para liberar los recursos al terminar la interacción. Este ejemplo establece una conexión a la vez; vuelva a cargar la página para iniciar otra sesión.
<!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. Preparar autenticación y parámetros de conexión

AppServer configura el intercambio de SDP para el modelo, la región y el espacio de trabajo objetivo. Los siguientes parámetros se aplican a conexiones Realtime estándar. Para parámetros específicos de la aplicación, siga la mejor práctica correspondiente.
SettingValue
MétodoPOST
URLhttps://{endpoint}/api/v1/webrtc/realtime?model={model_name}
Content-Typeapplication/sdp
AuthorizationBearer <API_KEY>
Cuerpo de la solicitudOffer SDP
Respuesta exitosaAnswer SDP
Los detalles completos de región, dirección y autenticación se encuentran en Autenticación e intercambio de SDP. La interfaz del AppServer de negocio la implementa la propia empresa y no forma parte de la API del SDK de Model Studio.

2. Inicializar el objeto de conexión y registrar las funciones de callback

En el entorno web, cree una RTCPeerConnection; en otras plataformas, inicialice la biblioteca correspondiente. Antes de establecer la conexión, registre los siguientes controladores:
  • Cambios de estado de la conexión: determine si la conexión se estableció correctamente, falló o se desconectó.
  • Creación, apertura, mensajes y cierre de DataChannel: transporta los eventos del modelo.
  • Pista de medios remota: recibe y reproduce audio, o maneja otros medios admitidos por la aplicación de destino.
El ejemplo completo crea el objeto de conexión, las variables de estado y las funciones de callback en el mismo script. Cada llamada crea una sola conexión; al terminar, reinicialice estos objetos para realizar otra llamada. El objeto de conexión y las variables de estado se detallan en Ejemplo completo. Registre los escuchadores de mensajes antes de inicializar el modelo para evitar perder los eventos de sesión que el servidor envía por iniciativa propia. No escuche únicamente los canales que crea el cliente: el servidor envía eventos mediante un canal llamado txt, por lo que debe manejar la función de callback datachannel.

3. Configurar direcciones de medios y DataChannels

Seleccionar la dirección de medios

Las direcciones son relativas al cliente. Los transceptores WebRTC negocian el envío y la recepción: sendrecv es bidireccional, sendonly solo envía y recvonly solo recibe. Las capacidades disponibles también dependen de la respuesta SDP del servidor y de la compatibilidad del modelo.
DirecciónConfiguración
Enviar audioNegocie el envío de audio y prepare el micrófono o una pista de audio externa. Agregue la pista cuando el modelo esté listo.
Recibir audioNegocie la recepción de audio y configure la reproducción en el callback de recepción de una pista remota.
Enviar videoCuando el modelo de destino admita entrada visual, negocie el envío de video y prepare la cámara o una pista de video externa.
Las conversaciones de voz generalmente requieren audio bidireccional; la entrada visual requiere además video ascendente. El SDP de oferta debe incluir una sección de medios m=audio. No la elimine simplemente porque la aplicación no necesite reproducción de audio.

Direcciones de medios para este modelo

Modelo o aplicaciónEnviar audioRecibir audioEnviar videoRecibir video
Qwen-Omni-Realtimeactive cuando se necesite entrada de vozactive cuando se necesite una respuesta de vozactive cuando se necesite entrada visualno se puede activar
Esta tabla describe las direcciones de medios requeridas por la aplicación; no garantiza que cada combinación pueda negociarse de forma independiente. La configuración depende del modelo objetivo y de las capacidades de negociación del servidor.

Preparar medios sin enviarlos

Negocie las direcciones necesarias antes de crear la oferta. Para modelos que requieren confirmación de inicialización, reserve capacidades de envío sin adjuntar pistas capturadas. Una vez que el modelo esté listo, utilice replaceTrack en el remitente correspondiente para iniciar la transmisión. Registre previamente los controladores de recepción de medios. Configure el envío de audio, la recepción de audio y el envío de video por separado según sea necesario; no combine la activación del micrófono, la reproducción de audio y la inicialización del modelo en una sola operación.

Ejemplo de configuración de medios

El siguiente ejemplo muestra voz bidireccional con carga de video opcional. Las tres opciones en media controlan el envío de audio, la recepción de audio y el envío de video, respectivamente. addTransceiver negocia la dirección de antemano; las pistas capturadas se almacenan primero de forma local hasta que el modelo esté listo y después se adjuntan al emisor. Consulte el código en el ejemplo completo. Configure la resolución de video y la tasa de cuadros según el modelo objetivo. Si las tasas de cuadros de la vista previa local y de la transmisión ascendente deben diferir, consulte la implementación de Canvas en la mejor práctica.

Crear canales de eventos

Cree un DataChannel antes de crear una Offer para que el SDP incluya la negociación del canal de datos. Consulte Envío y recepción de eventos del canal Data para conocer cómo el cliente y el servidor manejan la creación de canales. Los DataChannels transportan la inicialización del modelo, texto y eventos de control, y devuelven resultados, estado y errores del modelo. Las pistas multimedia transportan audio y video. El ejemplo completo crea un canal de eventos del cliente y registra la función de callback del canal del servidor antes de crear una Offer.

4. Establecer la conexión

Complete la conexión en el siguiente orden:
  1. Llame a createOffer y setLocalDescription.
  2. Recopile información de candidatos según los métodos ICE admitidos por el servidor; en este ejemplo, el SDP local se envía después de que termine la recopilación de ICE.
  3. Envíe el SDP local al AppServer de negocio, que usa la API Key para llamar a la interfaz de intercambio de SDP de Model Studio.
  4. Verifique el estado HTTP y, si la solicitud es exitosa, llame a setRemoteDescription con la Answer SDP devuelta.
  5. Espere a que el estado de la conexión indique éxito y confirme que el DataChannel que se usa para enviar eventos del modelo esté abierto.
Que el intercambio de SDP sea exitoso o que termine setRemoteDescription solo indica que se completó el paso de negociación correspondiente; no demuestra directamente que la conexión de medios se haya establecido. Tampoco se pueden enviar eventos del modelo cuando DataChannel no está en estado open.

Código de conexión y funciones de callback de estado

Escuche connectionstatechange para determinar el estado de la conexión y icegatheringstatechange para esperar el SDP local completo. La inicialización del modelo también requiere el estado open de DataChannel y el evento de disponibilidad del modelo; no debe basarse únicamente en la finalización del intercambio de SDP. El ejemplo completo realiza el intercambio de SDP mediante startSession() y maneja el resultado de la conexión mediante la función de callback del estado de la conexión. El AppServer de negocio debe reenviar la Offer con la dirección y los encabezados de solicitud de la sección 1; se espera que la API de ejemplo devuelva texto SDP sin formato. No pase directamente a setRemoteDescription contenido que incluya encabezados de respuesta HTTP, registros o contenedores JSON.

5. Intercambiar eventos del modelo y medios

Inicializar el modelo

Cuando la conexión y el canal de eventos estén disponibles, inicialice el modelo objetivo según su protocolo. Almacene los eventos de inicialización del servidor y combínelos con el estado del canal para avanzar en el flujo; no asuma un orden fijo de llegada de callbacks. Por ejemplo, al configurar un modelo con session.update, construya el evento según su protocolo y espere la confirmación de configuración antes de enviar la entrada. Los parámetros de voz, modalidades de salida, VAD y frecuencia de muestreo deben provenir de la documentación del modelo actual. session.created indica que se creó la sesión; no debe interpretarse siempre como que la configuración ya se aplicó. Para los modelos que requieren confirmación de la configuración de sesión, espere session.updated. Las definiciones de los eventos del modelo se encuentran en Eventos del cliente y Eventos del servidor. Maneje la transmisión de medios y los modos disponibles según las instrucciones de esta página. Al usar WebRTC, Qwen-Omni-Realtime solo admite el modo VAD; no admite el modo Manual. El audio se transmite mediante la pista de audio sin enviar input_audio_buffer.append; el video se transmite mediante la pista de video sin enviar input_image_buffer.append.

Ejemplos de eventos del modelo

El audio y el video se transmiten mediante las pistas de medios sin usar los eventos de envío de medios de WebSocket.
El siguiente ejemplo muestra el proceso session.created → session.update → session.updated para qwen3.8-omni-flash-realtime. Configure los campos según la referencia de eventos de este modelo. Después de recibir session.updated, que confirma que terminó la configuración, adjunte la pista de medios ascendente. Consulte el código en el ejemplo completo. Para el modelo de ejemplo qwen3.8-omni-flash-realtime, cuando la salida de audio está activada, puede usar response.audio.done para verificar el estado de generación de audio del modelo; este evento no indica que el cliente haya terminado de reproducir el audio.

Iniciar transmisión y recepción de medios

Una vez que el modelo esté listo, adjunte las pistas de audio y video necesarias para la transmisión ascendente. Gestione la salida del modelo en el callback de pista remota y continúe analizando los eventos del DataChannel.
  • El audio viaja a través de pistas de medios RTP; no se requiere el evento input_audio_buffer.append.
  • Las imágenes viajan a través de pistas de video. WebRTC no admite input_image_buffer.append.
  • Los DataChannels transportan eventos de texto, estado, control y error.
En el ejemplo completo, startSendingMedia() adjunta la pista ascendente y ontrack procesa los medios recibidos.

6. Finalizar interacción y liberar recursos

Cuando ya no se necesite la conexión, detenga las pistas de captura locales propiedad de su aplicación, cierre los DataChannels y la RTCPeerConnection, libere los reproductores y borre el estado de la aplicación. Restablezca el estado de disponibilidad del modelo tras una desconexión fallida o inesperada. Inicialice el modelo nuevamente en una nueva conexión en lugar de reutilizar el estado de sesión de la conexión anterior. En el ejemplo completo, endSession() se encarga de liberar la conexión y los recursos de medios. Llame a endSession() cuando el usuario finalice la llamada o abandone la página. Si su aplicación incorpora las funciones de Canvas o grabación de la mejor práctica, cancele también los bucles de animación y detenga las pistas multimedia de Canvas y el MediaRecorder.

Enviar y recibir eventos mediante el canal Data

Verifique que el canal esté open antes de llamar a send con un evento serializado. Analice los mensajes entrantes en su callback message, incluidos los eventos en canales creados por el servidor. Los nombres, campos, parámetros y tiempos de envío de los eventos se ajustan a las definiciones de eventos del cliente del modelo de destino; el método de análisis, los cambios de estado, los resultados y el manejo de errores después de recibir mensajes se ajustan a las definiciones de eventos del servidor del modelo. Los eventos de inicialización se envían según los requisitos del modelo, y los eventos posteriores de texto y control se envían según las necesidades del negocio.

Ejemplo de DataChannel

Utilice la misma función de enlace para canales creados por el cliente y por el servidor. Esta gestiona el transporte y el análisis JSON; handleModelEvent en la sección 5 gestiona la semántica del modelo. Utilice sendModelEvent para la inicialización del modelo, texto o eventos de control únicamente cuando lo permita el protocolo del modelo. En el ejemplo completo, sendModelEvent() y bindDataChannel() envían y reciben eventos. En el ejemplo completo, el botón de inicio llama a startSession() y el botón de finalización llama a endSession(). Estas funciones se definen en este ejemplo y no son API integradas de WebRTC.

Definiciones de eventos de Qwen-Omni-Realtime

Modelo o aplicaciónEventos del cliente (envío)Eventos del servidor (recepción)
Qwen-Omni-RealtimeEventos del clienteEventos del servidor
Siga también estas reglas:
  • Use el campo type de los eventos para identificar los eventos del modelo y maneje los identificadores asociados según la referencia de eventos de este modelo.
  • Las operaciones que requiere el modelo, como la confirmación de inicialización, el envío de la entrada, la cancelación de respuestas y la finalización de tareas, se manejan en el canal Data de acuerdo con el protocolo correspondiente; constituyen la semántica de interacción del modelo y son independientes de la desconexión del transporte.
  • Los datos de audio y video se transmiten mediante las pistas de medios. Al consultar la documentación de eventos del modelo, la encapsulación de medios y los modos de interacción disponibles siguen los métodos de conexión descritos aquí y no adoptan directamente el método de transmisión de medios de WebSocket.

Referencias de eventos

Tutoriales por escenario