Skip to main content

Qwen-Audio-Realtime Conexión mediante WebRTC

Qwen-Audio-Realtime Conexión mediante WebRTC

WebRTC usa pistas de medios para transmitir audio y DataChannel para transmitir eventos del modelo y texto. El cliente web puede usar la API nativa del navegador, mientras que los demás clientes pueden usar bibliotecas compatibles con WebRTC estándar. Model Studio no proporciona un SDK de WebRTC específico. 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-Audio-Realtime. Las instrucciones de integración existentes abarcan qwen-audio-3.1-realtime-plus, qwen-audio-3.1-realtime-flash, qwen-audio-3.0-realtime-plus y qwen-audio-3.0-realtime-flash. El modelo del ejemplo es qwen-audio-3.1-realtime-plus.
  • 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.
Para capturar audio en el navegador, use un contexto seguro; para usar el micrófono, solicite los permisos correspondientes. 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=qwen-audio-3.1-realtime-plus
  • Dirección de Singapur: https://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api/v1/webrtc/realtime?model=qwen-audio-3.1-realtime-plus
  • 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.
  • El audio se transmite mediante las pistas de medios de WebRTC; la aplicación debe obtener la pista de audio local, vincular el reproductor remoto y enviar y recibir mensajes de DataChannel según las definiciones de eventos del modelo de destino.
  • 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 ha establecido el canal de transmisión, pero no significa que el modelo haya completado la inicialización. Al usar qwen-audio-3.1-realtime-plus 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,
};

function mediaDirection(send, receive) {
  if (send && receive) return "sendrecv";
  if (send) return "sendonly";
  if (receive) return "recvonly";
  return "inactive";
}

async function configureMedia() {
  // Keep m=audio. The target service must support the selected directions.
  const transceiver = pc.addTransceiver("audio", {
    direction: mediaDirection(media.sendAudio, media.receiveAudio),
  });
  if (media.sendAudio) senders.set("audio", transceiver.sender);
  if (!media.sendAudio) return;
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: true,
    video: 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 ? ["audio", "text"] : ["text"],
      "voice": "longanqian_v3.1",
    },
  });
  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 audio

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.
Por ejemplo, las conversaciones de voz suelen usar audio bidireccional. La Offer SDP debe incluir la sección de medios de audio m=audio; no elimine esta sección solo porque la aplicación no necesite reproducir audio.

Direcciones de medios para este modelo

Modelo o aplicaciónEnviar audioRecibir audioEnviar videoRecibir video
Qwen-Audio-RealtimeActive para conversaciones de vozActive cuando se necesite una respuesta de vozNo se puede activarNo 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 y la recepción de audio 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. Dos opciones en media controlan el envío y la recepción de audio, respectivamente; addTransceiver negocia la dirección de antemano y la pista capturada se almacena primero de forma local hasta que el modelo esté listo, momento en que se adjunta al emisor. Consulte el código en el ejemplo completo.

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. DataChannel se encarga de enviar los eventos de inicialización del modelo, texto y control, y de recibir los resultados, estados y errores del modelo. La pista de medios se encarga de transmitir el audio. 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 como se describe en esta página.

Ejemplos de eventos del modelo

El audio se transmite mediante la pista de medios y no usa el evento de envío de medios de WebSocket.
El siguiente ejemplo muestra el flujo session.created → session.update → session.updated para qwen-audio-3.1-realtime-plus. Configure los campos según la referencia de eventos del modelo. Adjunte la pista de audio ascendente solo después de recibir session.updated, que confirma que terminó la configuración. Consulte el código en el ejemplo completo. Para el modelo de ejemplo qwen-audio-3.1-realtime-plus, activar la salida de audio permite supervisar el estado de generación de audio del modelo mediante response.audio.done; este evento no indica que el cliente haya terminado de reproducir el audio.

Iniciar transmisión y recepción de medios

Cuando el modelo esté listo, adjunte la pista de audio ascendente necesaria. Maneje la salida del modelo mediante la función de callback de la pista remota y analice continuamente los eventos que devuelve DataChannel.
  • El audio viaja a través de pistas de medios RTP; no se requiere el evento input_audio_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 haga clic en el botón de finalización o cierre la página.

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-Audio-Realtime

Modelo o aplicaciónEventos del cliente (envío)Eventos del servidor (recepción)
Qwen-Audio-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 se transmiten por las pistas de medios. Al usar la referencia de eventos del modelo, siga esta página para la encapsulación de medios y los modos de interacción disponibles. No aplique directamente el método de WebSocket para enviar medios.

Referencias de eventos