Skip to main content
Melhores Práticas

Uso do WebRTC com qwen3.5-omni-plus-realtime para chamadas em tempo real

Este tópico descreve como se conectar à Realtime API no navegador usando WebRTC e JavaScript para ativar chamadas de áudio e vídeo em tempo real com o modelo qwen3.5-omni-plus-realtime.

O WebRTC é ideal para cenários de voz com baixa latência baseados em navegador. O áudio é transmitido diretamente via UDP, com cancelamento de eco e redução de ruído integrados. O WebRTC suporta apenas o modo VAD no lado do servidor (server_vad ou semantic_vad). O modo manual não tem suporte.

Pré-requisitos e observações

  1. Configure an API key e defina it as an environment variable.
  2. Use um navegador moderno com suporte a WebRTC (Chrome, Edge, Firefox, Safari, entre outros).
  3. O navegador precisa ter permissão de acesso ao microfone. Para chamadas de vídeo, também é necessária permissão da câmera.
  4. Devido a restrições de CORS, os navegadores não podem enviar solicitações de troca de SDP diretamente ao servidor. No demo, execute o comando curl em um terminal para concluir a conexão. Em produção, essa restrição não se aplica quando a solicitação passa por um proxy em um AppServer.

Implementar chamadas de áudio e vídeo com IA

O diagrama de sequência a seguir ilustra o fluxo completo de chamadas de áudio e vídeo via WebRTC: Diagrama de sequência de chamada de áudio e vídeo via WebRTC
image

Crie RTCPeerConnection

Invoque o construtor nativo RTCPeerConnection do navegador para criar uma instância de conexão. Não é necessário configurar servidores ICE (o servidor gerencia a travessia de NAT).
pc = new RTCPeerConnection({ iceServers: [ ] });
Registre os callbacks principais:
// Connection state listener
pc.onconnectionstatechange = () => {
  if (!pc) return;
  if (pc.connectionState === 'connected') {
    setStatus('Connected, please speak', 'connected');
  } else if (["failed", "closed", "disconnected"].includes(pc.connectionState)) {
    endSession(true);
  }
};

// Receive and play remote audio stream + start recording
pc.ontrack = async (e) => {
  const stream = e.streams[0];
  ensureHiddenAudioEl();
  hiddenRemoteAudioEl.srcObject = stream;
  try { await hiddenRemoteAudioEl.play(); } catch {}
  startRecordingRemoteStream(stream);
};

Obter streams de mídia local

Faça uma única chamada a getUserMedia para solicitar áudio (obrigatório) e vídeo (opcional). A ativação do vídeo depende da caixa de seleção Enable video marcada pelo usuário.
const wantVideo = !!sendVideoCheckbox.checked;

const constraints = wantVideo
  ? {
      audio: true,
      video: {
        facingMode: { ideal: "user" },
        frameRate: { ideal: 30, max: 30 },
        width: { ideal: 640 },
        height: { ideal: 480 },
      }
    }
  : { audio: true };

localStream = await navigator.mediaDevices.getUserMedia(constraints);
Tanto o áudio quanto o vídeo são obtidos em uma única chamada a getUserMedia, e não separadamente. A pré-visualização do vídeo roda a 30 fps (para garantir fluidez na visualização local); já a taxa de quadros enviada é reduzida para 2 fps via Canvas.

Adicionar faixas de mídia ao PeerConnection

Adicionar faixa de áudio:
localStream.getAudioTracks().forEach(t => {
  pc.addTrack(t, localStream);
  gatedAudioTracks.push(t);
});
Adicionar faixa de vídeo (opcional, com redução de quadros via Canvas para 2 fps): As dimensões do Canvas são obtidas dinamicamente a partir da resolução real da câmera, em vez de serem definidas fixas no código:
const sendFps = 2;
const settings = localStream.getVideoTracks()[0].getSettings();
sendCanvas = document.createElement("canvas");
sendCanvas.width = settings.width || 640;   // Dynamically obtain actual width
sendCanvas.height = settings.height || 480;  // Dynamically obtain actual height
sendCanvasCtx = sendCanvas.getContext("2d", { alpha: false });

sendCanvasStream = sendCanvas.captureStream(sendFps); // 2fps
const lowFpsTrack = sendCanvasStream.getVideoTracks()[0];
pc.addTrack(lowFpsTrack, sendCanvasStream);
gatedVideoTracks.push(lowFpsTrack);

// requestAnimationFrame loop: draw camera frames to Canvas
const pump = () => {
  if (!sendCanvasCtx || !sendCanvas) return;
  try { sendCanvasCtx.drawImage(localVideo, 0, 0, sendCanvas.width, sendCanvas.height); } catch {}
  sendRafId = requestAnimationFrame(pump);
};
sendRafId = requestAnimationFrame(pump);
Controle de envio de mídia (crítico): Após adicionar as faixas, bloqueie o envio imediatamente para garantir que nenhuma mídia seja transmitida antes do recebimento de session.created:
// 1. Disable all track.enabled
gateMedia(false);  // track.enabled = false

// 2. Replace sender tracks with null to fully block sending
audioSender = pc.getSenders().find(s => s.track?.kind === 'audio');
videoSender = pc.getSenders().find(s => s.track?.kind === 'video');
audioTrack = audioSender?.track;
videoTrack = videoSender?.track;
await audioSender?.replaceTrack(null);
await videoSender?.replaceTrack(videoTrack ? null : undefined);
Isso equivale a enableSendMediaStream(false) em outros SDKs. O envio só deve ser restaurado após o recebimento de session.created.

Crie DataChannel

Crie um DataChannel chamado oai-events para trocar eventos de controle de sessão com o servidor de IA.
const dc = pc.createDataChannel('oai-events');

dc.onopen = () => console.log("DC open");
dc.onmessage = (e) => {
  handleDcMessage(e.data, dc);
};

// Also listen for DataChannels created by the server
pc.ondatachannel = (event) => {
  const ch = event.channel;
  ch.onmessage = (e) => {
    handleDcMessage(e.data, ch);
  };
};

Gerar Offer SDP

Chame createOffer() e defina a descrição local. Aguarde a conclusão da coleta de ICE antes de utilizar o Offer SDP completo.
pc.onicegatheringstatechange = () => {
  if (!pc) return;
  if (pc.iceGatheringState === "complete" && pc.localDescription?.sdp) {
    const sdp = pc.localDescription.sdp;
    // ICE gathering complete, Offer SDP ready
    // Auto-generate curl command for users
  }
};

const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
Aguarde até que iceGatheringState === "complete" antes de usar o SDP. Nesse momento, o SDP contém todas as informações dos candidatos ICE.

Trocar SDP (via comando curl ou AppServer)

Envie o Offer SDP ao servidor e obtenha o Answer SDP. No demo, isso é feito por meio de um comando curl:
curl -X POST 'https://{endpoint}/api/v1/webrtc/realtime?model=qwen3.5-omni-plus-realtime' \
  -H 'Content-Type: application/sdp' \
  -H 'Authorization: Bearer $DASHSCOPE_API_KEY' \
  --data-binary '<Offer SDP content>'
Em produção, faça o proxy dessa etapa por meio de um AppServer para evitar a exposição da chave de API no front-end. {endpoint} é o endpoint da Realtime API.

Definir Answer SDP para estabelecer a conexão

Configure o Answer SDP retornado pelo servidor como a descrição remota para estabelecer a conexão WebRTC. Normalize o formato do SDP antes de defini-lo:
function normalizeSdpForSetRemote(sdp) {
  sdp = String(sdp).trim().replace(/\r?\n/g, "\r\n");
  if (!sdp.endsWith("\r\n")) sdp += "\r\n";
  return sdp;
}

const answerSdp = normalizeSdpForSetRemote(txt);
await pc.setRemoteDescription({ type: 'answer', sdp: answerSdp });
A especificação SDP exige quebras de linha \r\n. A função normalizeSdpForSetRemote garante a compatibilidade das quebras de linha provenientes de diferentes fontes.

Configure sessão de IA (session.update)

Depois que a conexão é estabelecida, o servidor envia um evento session.created pelo DataChannel. Ao recebê-lo:
  1. Libere o controle de mídia para retomar o envio de áudio e vídeo
  2. Envie session.update para configurar os parâmetros da sessão
Liberar o controle e restaurar a mídia:
function handleDcMessage(data, channel) {
  let obj;
  try { obj = JSON.parse(data); } catch (err) { return; }

  if (obj?.type === "session.created") {
    // Release gate: restore track.enabled
    gateMedia(true);
    // Restore actual track to sender
    if (audioSender) audioSender.replaceTrack(audioTrack);
    if (videoSender && videoTrack) videoSender.replaceTrack(videoTrack);
    // Send session configuration
    sendUpdate(channel);
  }
}
Corpo da mensagem session.update:
const update = {
  event_id: `event_${Date.now()}`,
  type: "session.update",
  session: {
    input_audio_format: "pcm",
    input_audio_transcription: { model: "qwen3-asr-flash-realtime" },
    instructions: "You are a helpful assistant.",
    modalities: ["text", "audio"],
    output_audio_format: "pcm",
    smooth_output: false,
    turn_detection: {
      prefix_padding_ms: 500,
      silence_duration_ms: 800,
      threshold: 0.5,
      type: "server_vad",
    },
  },
};
if (channel && channel.readyState === "open") channel.send(JSON.stringify(update));
O campo turn_detection.type pode ser definido como server_vad (detecção baseada em volume) ou semantic_vad (detecção semântica). O modo VAD manual não tem suporte no modo WebRTC.

Conversa em tempo real

Com a conexão estabelecida, o áudio e o vídeo são transmitidos em tempo real via RTP. As respostas de voz da IA remota chegam pelo callback ontrack e são reproduzidas. O MediaRecorder grava as respostas para permitir o download posterior. Receber e gravar o áudio remoto:
pc.ontrack = async (e) => {
  const stream = e.streams[0];
  ensureHiddenAudioEl();
  hiddenRemoteAudioEl.srcObject = stream;
  try { await hiddenRemoteAudioEl.play(); } catch {}
  startRecordingRemoteStream(stream); // Start recording
};

function startRecordingRemoteStream(remoteStream) {
  const audioTracks = remoteStream.getAudioTracks();
  if (!audioTracks.length) return;
  const audioStream = new MediaStream(audioTracks);

  recordedChunks = [ ];

  mediaRecorder = new MediaRecorder(audioStream, { mimeType: 'audio/webm' });
  mediaRecorder.ondataavailable = (e) => {
    if (e.data && e.data.size > 0) recordedChunks.push(e.data);
  };
  mediaRecorder.onstop = () => {
    audioBlob = new Blob(recordedChunks, { type: 'audio/webm' });
    // Available for download after recording stops
  };
  mediaRecorder.start();
}
Exibição de eventos do DataChannel: Todos os eventos enviados e recebidos pelo DataChannel (incluindo session.created, response.audio_transcript.done, entre outros) aparecem no painel de eventos, com opção de expansão para visualizar o JSON completo:
function pushEventFromDataChannel(eventObj) {
  const ts = eventObj.timestamp || nowTs();
  events.unshift({ event: eventObj, timestamp: ts });
  renderEvents();
}

Encerrar sessão e liberar recursos

Ao encerrar uma chamada, libere todos os recursos na ordem correta. A sequência é importante:
function endSession(silent = false) {
  // 1. Stop Canvas frame-rate reduction loop
  if (sendRafId) cancelAnimationFrame(sendRafId);
  sendRafId = 0;
  if (sendCanvasStream) sendCanvasStream.getTracks().forEach(t => t.stop());
  sendCanvasStream = null; sendCanvasCtx = null; sendCanvas = null;

  // 2. Stop recording
  try { if (mediaRecorder && mediaRecorder.state !== "inactive") mediaRecorder.stop(); } catch {}
  mediaRecorder = null;

  // 3. Stop local media stream
  if (localStream) {
    localStream.getTracks().forEach(t => t.stop());
    localStream = null;
  }

  // 4. Close PeerConnection
  if (pc) { try { pc.close(); } catch {} pc = null; }

  // 5. Clean up remote audio element
  if (hiddenRemoteAudioEl) {
    try { hiddenRemoteAudioEl.pause(); } catch {}
    hiddenRemoteAudioEl.srcObject = null;
    hiddenRemoteAudioEl.remove();
    hiddenRemoteAudioEl = null;
  }
}
Após o término da sessão, use o botão Download remote audio para salve a gravação da resposta da IA no formato WebM.

Observações importantes

  1. O controle de mídia deve ser liberado após session.created: Dados de mídia enviados antes de o servidor emitir session.created serão descartados. Utilize replaceTrack(null) para bloquear totalmente o envio até esse momento.
  2. A redução de quadros de vídeo ocorre via Canvas: A pré-visualização local opera a 30 fps; apenas 2 fps são enviados ao servidor, controlados por captureStream(2). Essa abordagem economiza largura de banda.
  3. Normalização do formato SDP: Garanta que as quebras de linha sejam \r\n antes de definir o Answer SDP. Caso contrário, setRemoteDescription pode falhar.
  4. O vídeo é opcional: Se o usuário não ative o vídeo, apenas a permissão do microfone será solicitada — nenhum prompt de permissão da câmera aparecerá.
  5. O áudio remoto é gravado automaticamente: O MediaRecorder captura o stream de áudio da resposta da IA. Um arquivo WebM fica disponível para baixe após o fim da sessão.
  6. O WebRTC suporta apenas VAD no lado do servidor: O modo manual não tem suporte. Use server_vad (detecção por volume) ou semantic_vad (detecção semântica).

Baixe o demo completo

Baixe o código de exemplo completo: webrtc_demo.html.

Documentos relacionados

Plano de Tokens
Playground de Modelos
  • Music generation
Inferência do Modelo
Avaliação
Compressão de Modelos
Estatísticas e Monitoramento
Suporte
Uso do WebRTC com qwen3.5-omni-plus-realtime para chamadas em tempo real - Alibaba Cloud Model Studio