All Products
Search
Document Center

Alibaba Cloud Model Studio:Menggunakan WebRTC dengan qwen3.5-omni-plus-realtime untuk panggilan real-time

Last Updated:Aug 26, 2026

Menjelaskan cara menghubungkan ke Realtime API di browser menggunakan WebRTC dan JavaScript untuk mengaktifkan panggilan audio/video real-time dengan model qwen3.5-omni-plus-realtime.

CatatanWebRTC cocok untuk skenario voice berbasis browser dengan latensi rendah. Audio ditransmisikan langsung melalui UDP dengan pembatalan gema (echo cancellation) dan pengurangan noise bawaan. WebRTC hanya mendukung mode VAD sisi server (server_vad atau semantic_vad). Mode manual tidak didukung.

Prasyarat dan catatan

  1. Konfigurasikan Kunci API dan tetapkan sebagai variabel lingkungan.
  2. Gunakan browser modern yang mendukung WebRTC (Chrome, Edge, Firefox, Safari, dan sebagainya).
  3. Browser harus memiliki izin mikrofon. Untuk panggilan video, izin kamera juga diperlukan.
  4. Browser tidak dapat langsung mengirim permintaan pertukaran SDP ke server karena pembatasan CORS. Dalam demo, jalankan perintah curl di terminal untuk menyelesaikan koneksi. Di lingkungan produksi, pembatasan ini tidak berlaku ketika permintaan diproksi melalui AppServer.

Implementasikan panggilan audio/video AI

Diagram urutan berikut menunjukkan alur lengkap panggilan audio/video WebRTC:

Diagram urutan panggilan audio/video WebRTC

image

Buat RTCPeerConnection

Panggil konstruktor RTCPeerConnection bawaan browser untuk membuat instans koneksi. Tidak perlu mengonfigurasi ICE server (server menangani Penelusuran NAT).

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

Daftarkan callback utama:

// Pendengar status koneksi
pc.onconnectionstatechange = () => {
  if (!pc) return;
  if (pc.connectionState === 'connected') {
    setStatus('Terhubung, silakan bicara', 'connected');
  } else if (["failed", "closed", "disconnected"].includes(pc.connectionState)) {
    endSession(true);
  }
};

// Terima dan putar aliran audio remote + mulai merekam
pc.ontrack = async (e) => {
  const stream = e.streams[0];
  ensureHiddenAudioEl();
  hiddenRemoteAudioEl.srcObject = stream;
  try { await hiddenRemoteAudioEl.play(); } catch {}
  startRecordingRemoteStream(stream);
};

Ambil aliran media lokal

Gunakan satu panggilan getUserMedia untuk meminta audio (wajib) dan video (opsional). Aktivasi video ditentukan oleh centang pengguna pada kotak Enable video.

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

CatatanAudio dan video keduanya diperoleh dalam satu panggilan getUserMedia, bukan secara terpisah. Pratinjau video berjalan pada 30 fps (untuk pratinjau lokal yang lancar); laju frame pengiriman dikurangi menjadi 2 fps melalui Canvas.

Tambahkan track media ke PeerConnection

Tambahkan track audio:
localStream.getAudioTracks().forEach(t => {
  pc.addTrack(t, localStream);
  gatedAudioTracks.push(t);
});
Tambahkan track video (opsional, dengan pengurangan frame Canvas menjadi 2 fps):

Dimensi Canvas diperoleh secara dinamis dari resolusi kamera aktual, bukan hard-coded:

const sendFps = 2;
const settings = localStream.getVideoTracks()[0].getSettings();
sendCanvas = document.createElement("canvas");
sendCanvas.width = settings.width || 640;   // Peroleh lebar aktual secara dinamis
sendCanvas.height = settings.height || 480;  // Peroleh tinggi aktual secara dinamis
sendCanvasCtx = sendCanvas.getContext("2d", { alpha: false });

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

// Loop requestAnimationFrame: gambar frame kamera ke 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 (kritis):

Setelah menambahkan track, segera blokir pengiriman agar tidak ada media yang didorong sebelum menerima session.created:

// 1. Nonaktifkan semua track.enabled
gateMedia(false);  // track.enabled = false

// 2. Ganti track pengirim dengan null untuk sepenuhnya memblokir pengiriman
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);

CatatanIni setara dengan enableSendMediaStream(false) di SDK lain. Pengiriman tidak boleh dipulihkan hingga menerima session.created.

Buat DataChannel

Buat DataChannel bernama oai-events untuk bertukar event kontrol sesi dengan server AI.

const dc = pc.createDataChannel('oai-events');

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

// Dengarkan juga DataChannel yang dibuat oleh server
pc.ondatachannel = (event) => {
  const ch = event.channel;
  ch.onmessage = (e) => {
    handleDcMessage(e.data, ch);
  };
};

Hasilkan Offer SDP

Panggil createOffer() dan atur deskripsi lokal. Tunggu hingga pengumpulan ICE selesai sebelum menggunakan Offer SDP lengkap.

pc.onicegatheringstatechange = () => {
  if (!pc) return;
  if (pc.iceGatheringState === "complete" && pc.localDescription?.sdp) {
    const sdp = pc.localDescription.sdp;
    // Pengumpulan ICE selesai, Offer SDP siap
    // Hasilkan otomatis perintah curl untuk pengguna
  }
};

const offer = await pc.createOffer();
await pc.setLocalDescription(offer);

CatatanTunggu hingga iceGatheringState === "complete" sebelum menggunakan SDP. Pada titik tersebut, SDP berisi semua informasi kandidat ICE.

Pertukaran SDP (melalui perintah curl atau AppServer)

Kirim Offer SDP ke server dan peroleh Answer SDP. Dalam demo, langkah ini dilakukan melalui perintah 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>'

CatatanDi lingkungan produksi, proksi langkah ini melalui AppServer untuk menghindari pengeksposan Kunci API di front end. {endpoint} adalah Titik akhir Realtime API.

Atur Answer SDP untuk menetapkan koneksi

Atur Answer SDP yang dikembalikan server sebagai deskripsi remote untuk menetapkan koneksi WebRTC. Normalisasi format SDP sebelum mengaturnya:

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

CatatanSpesifikasi SDP mengharuskan akhiran baris \r\n. normalizeSdpForSetRemote menangani kompatibilitas akhiran baris dari berbagai sumber.

Konfigurasi sesi AI (session.update)

Setelah koneksi ditetapkan, server mengirim event session.created melalui DataChannel. Saat menerimanya:

  1. Lepaskan pemblokiran media untuk memulihkan pengiriman audio/video
  2. Kirim session.update untuk mengonfigurasi parameter sesi
Lepaskan pemblokiran dan pulihkan media:
function handleDcMessage(data, channel) {
  let obj;
  try { obj = JSON.parse(data); } catch (err) { return; }

  if (obj?.type === "session.created") {
    // Lepaskan pemblokiran: pulihkan track.enabled
    gateMedia(true);
    // Pulihkan track aktual ke pengirim
    if (audioSender) audioSender.replaceTrack(audioTrack);
    if (videoSender && videoTrack) videoSender.replaceTrack(videoTrack);
    // Kirim konfigurasi sesi
    sendUpdate(channel);
  }
}
Isi pesan 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));

Catatanturn_detection.type dapat diatur ke server_vad (deteksi berbasis volume) atau semantic_vad (deteksi semantik). VAD manual tidak didukung dalam mode WebRTC.

Percakapan real-time

Setelah koneksi ditetapkan, audio/video ditransmisikan secara real-time melalui RTP. Tanggapan suara AI remote diterima melalui callback ontrack dan diputar kembali. MediaRecorder digunakan untuk merekam tanggapan guna diunduh.

Menerima dan Merekam Audio Jarak Jauh:
pc.ontrack = async (e) => {
  const stream = e.streams[0];
  ensureHiddenAudioEl();
  hiddenRemoteAudioEl.srcObject = stream;
  try { await hiddenRemoteAudioEl.play(); } catch {}
  startRecordingRemoteStream(stream); // Mulai merekam
};

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' });
    // Tersedia untuk diunduh setelah perekaman berhenti
  };
  mediaRecorder.start();
}
Tampilan event DataChannel:

Semua event yang dikirim dan diterima melalui DataChannel (termasuk session.created, response.audio_transcript.done, dan sebagainya) ditampilkan di panel event dengan dukungan ekspansi untuk melihat JSON lengkap:

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

Akhiri sesi dan lepaskan sumber daya

Lepaskan semua sumber daya secara berurutan saat mengakhiri panggilan. Urutan ini penting:

function endSession(silent = false) {
  // 1. Hentikan loop pengurangan frame-rate Canvas
  if (sendRafId) cancelAnimationFrame(sendRafId);
  sendRafId = 0;
  if (sendCanvasStream) sendCanvasStream.getTracks().forEach(t => t.stop());
  sendCanvasStream = null; sendCanvasCtx = null; sendCanvas = null;

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

  // 3. Hentikan aliran media lokal
  if (localStream) {
    localStream.getTracks().forEach(t => t.stop());
    localStream = null;
  }

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

  // 5. Bersihkan elemen audio remote
  if (hiddenRemoteAudioEl) {
    try { hiddenRemoteAudioEl.pause(); } catch {}
    hiddenRemoteAudioEl.srcObject = null;
    hiddenRemoteAudioEl.remove();
    hiddenRemoteAudioEl = null;
  }
}

CatatanSetelah sesi berakhir, gunakan tombol Download remote audio untuk menyimpan rekaman tanggapan AI dalam format WebM.

Catatan penting

  1. Pemblokiran media harus dilepas setelah session.created: Data media yang dikirim sebelum server mengirim session.created akan dibuang. Gunakan replaceTrack(null) untuk sepenuhnya memblokir pengiriman hingga saat itu.
  2. Pengurangan frame video diimplementasikan melalui Canvas: Pratinjau lokal berjalan pada 30 fps; hanya 2 fps yang dikirim ke server, dikendalikan oleh captureStream(2). Ini menghemat bandwidth.
  3. Normalisasi format SDP: Pastikan akhiran baris adalah \r\n sebelum mengatur Answer SDP. Jika tidak, setRemoteDescription mungkin gagal.
  4. Video bersifat opsional: Jika pengguna tidak mengaktifkan video, hanya izin mikrofon yang diminta — tidak muncul prompt izin kamera.
  5. Audio remote direkam secara otomatis: MediaRecorder merekam aliran audio tanggapan AI. File WebM tersedia untuk diunduh setelah sesi berakhir.
  6. WebRTC hanya mendukung VAD sisi server: Mode manual tidak didukung. Gunakan server_vad (deteksi volume) atau semantic_vad (deteksi semantik).

Unduh demo lengkap

Unduh kode contoh lengkap: webrtc_demo.html.

Dokumen terkait