QwenCloudBRASIL
Tradução de apoio para consulta. Código e identificadores técnicos preservados. Consultar referência original ↗

Chamada em tempo real com qwen3.8-omni-flash-realtime via WebRTC

Use qwen3.8-omni-flash-realtime Modelo através do protocolo WebRTC para chamadas em tempo real

Descreve como se conectar com o QwenCloud API em tempo real em um navegador usando WebRTC e JavaScript para ativar chamadas de áudio/vídeo em tempo real com o qwen3.8-omni-flash-realtime Modelo.

WebRTC é adequado para cenários de voz baseados em navegadores, de baixa latência. O áudio é transmitido diretamente sobre o UDP com cancelamento de eco integrado e redução de ruído. WebRTC suporta apenas o modo VAD do lado do servidor (server_vad Ou semantic_vad). Modo manual não é suportado.

Pré-requisitos e notas

  1. [Configure uma chave API] (/developer-guides/administration/api-keysE o define como uma variável de ambiente./api-reference/preparation/export-api-key-env).
  2. Use um navegador moderno que suporte WebRTC (Chrome, Edge, Firefox, Safari, e assim por diante).
  3. O navegador deve ter permissão para o microfone. Para chamadas de vídeo, a permissão da câmera também é necessária.
  4. Navegadores não podem enviar diretamente pedidos de troca SDP para o servidor devido às restrições CORS. Na demonstração, execute o comando curl em um terminal para completar a conexão. Na produção, essa restrição não se aplica quando o pedido é aproximado através de um AppServer.

Implementar chamadas de áudio e vídeo de IA

O seguinte diagrama de sequência mostra o fluxo completo de chamadas de áudio/vídeo WebRTC:

WebRTC audio / vídeo de sequência de chamadas diagrama

[Imagem] (https://help-static-aliyun-doc.aliyuncs.com/en-US/images/p1088079.png)

Crie a RTCPEerConnection

Chame o navegador de nativos. RTCPeerConnection Construtor para criar uma instância de conexão. Nenhum servidor de ICE precisa ser configurado (o servidor lida com NAT transversal).

pc = new RTCPeerConnection({ iceServers: [ ] });

Registro de chamadas de chave:

// 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 transmissões de mídia locais

Use um único getUserMedia Chamada para solicitar áudio (obrigatório) e vídeo (opcional). Se o vídeo está ativado é determinado pela caixa de seleção Ativar vídeo do 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 áudio quanto vídeo são obtidos em um único getUserMedia Chamada, não separadamente. A pré-visualização do vídeo é feita em 30 fps (para uma visualização local suave); a taxa de envio de quadros é reduzida para 2 fps via Canvas.

Adicione faixas de mídia para a conexão de pares

** Adicionar faixa de áudio: **

localStream.getAudioTracks().forEach(t => {
  pc.addTrack(t, localStream);
  gatedAudioTracks.push(t);
});

Adicione faixa de vídeo (opcional, com redução de tela para 2 fps):

Dimensões da tela são obtidas dinamicamente da resolução da câmera real em vez de codificadas:

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);

Media gating (crítico):

Depois de adicionar faixas, bloqueie o envio imediatamente para garantir que nenhuma mídia seja empurrada antes session.created é recebido:

// 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 é equivalente a enableSendMediaStream(false) em outros SDKs. O envio não deve ser restaurado até que session.created é recebido.

Crie o 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);
  };
};

Gere a oferta SDP.

Chamada. createOffer() e definir a descrição local. Espere a reunião da Imigração terminar antes de usar a SDP completa da Oferta.

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);

Espere. iceGatheringState === "complete" Antes de usar o SDP. Nesse ponto, o SDP contém todas as informações do candidato da Imigração.

Troca SDP (via comando curl ou AppServer)

Envie a oferta SDP para o QwenCloud servidor e obter a resposta SDP. Na demo, isso é feito através de um comando curl:

curl -X POST 'https://maas.qwencloudapi.com/api/v1/webrtc/realtime?model=qwen3.8-omni-flash-realtime' \
  -H 'Content-Type: application/sdp' \
  -H 'Authorization: Bearer $DASHSCOPE_API_KEY' \
  --data-binary '<Offer SDP content>'

Em produção, proxy este passo através de um AppServer para evitar expor a chave API na frente.

Preparar resposta SDP para estabelecer conexão

Defina o SDP Resposta retornado pelo servidor como a descrição remota para estabelecer a conexão WebRTC. Normalize o formato SDP antes de definir:

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 requer \r\n Finais de linha. normalizeSdpForSetRemote Lida com compatibilidade final de linhas de diferentes fontes.

Configure a sessão de IA (sessão.atualização)

Depois que a conexão for estabelecida, o servidor envia um session.created evento através do DataChannel. Ao recebê-lo:

  1. Libere o portal de mídia para restaurar o envio de áudio/vídeo.
  2. Enviar session.update para configurar parâmetros de sessão

Libertem o portal e restaurem 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);
  }
}

Sessão, atualização do corpo de mensagens:

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));

turn_detection.type Pode ser definido para server_vad (detecção baseada em volume) ou semantic_vad (Detecção semântica). VAD manual não é suportado no modo WebRTC.

Conversa em tempo real.

Depois que a conexão for estabelecida, áudio/vídeo é transmitido em tempo real sobre RTP. As respostas de voz remotas da IA são recebidas através da ontrack Ligando de volta e jogando de volta. MediaRecorder é usado para gravar respostas para download.

**Recebendo e gravando á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();
}

** DataChannel exposição de eventos: **

Todos os eventos enviados e recebidos através do DataChannel (incluindo session.created, response.audio_transcript.done, e assim por diante) são exibidos no painel de eventos com apoio para expandir para ver o JSON completo:

function pushEventFromDataChannel(eventObj) {
  const ts = eventObj.timestamp || nowTs();
  events.unshift({ event: eventObj, timestamp: ts });
  renderEvents();
}

Fim da sessão e liberar recursos

Libere todos os recursos para terminar uma ligação. A ordem é 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;
  }
}

Depois que a sessão terminar, use o botão Download de áudio remoto para salvar a gravação de resposta de IA no formato WebM.

Notas importantes.

  1. Os dados da mídia enviados antes do servidor enviar. session.created será descartado. Use replaceTrack(null) para bloquear o envio até lá.
  2. A redução de quadros de vídeo é implementada via Canvas, a pré-visualização local é feita em 30 fps, apenas 2 fps são enviados para o servidor, controlado por captureStream(2). Isso conserva largura de banda.
  3. Garanta que os finais da linha sejam \r\n Antes de definir o SDP Resposta. Caso contrário, setRemoteDescription pode falhar.
  4. Se o usuário não habilitar o vídeo, apenas a permissão do microfone é solicitada - nenhuma câmera é exibida.
  5. O áudio remoto é gravado automaticamente, o MediaRecorder grava o fluxo de áudio de resposta de IA. Um arquivo WebM está disponível para download após a sessão terminar.
  6. O WebRTC suporta apenas o VAD do lado do servidor. manual O modo não é suportado. Use server_vad (Detecção de volume) ou semantic_vad (Detecção semântica).

Baixe a demonstração completa.

Baixe o código completo de exemplo: [webrtc_demo.html] (https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/en-US/20260715/qehgnu/webrtc_demo.html).