Flaky test pada pipeline audio WebRTC sering terjadi di lingkungan Continuous Integration (CI). Penyebab utamanya adalah variasi clock sistem virtualized dan penjadwalan thread yang memicu audio buffer underrun: kondisi saat thread audio consumer meminta sampel PCM baru, tetapi buffer internal kosong akibat jeda pengiriman paket.

Masalah ini semakin nyata pada arsitektur modern seperti LiveKit atau streaming bidirectional suara real-time (misalnya Gemini Live API via WebSockets/WebRTC). Artikel ini membahas cara membangun pengujian deterministik tanpa dependensi jaringan eksternal untuk menguji reliabilitas WebRTC audio secara konsisten.

Akar Masalah: Audio Buffer Underrun dan Non-Determinisme Jaringan

Audio playback memerlukan aliran data kontinu dengan interval ketat (contoh: frame 20ms atau 960 sampel pada 48kHz). Pada CI runner, virtual CPU sharing menyebabkan pengiriman paket RTP atau WebRTC data stream tertahan secara acak. Jeda singkat ini mengosongkan antrean audio frame reader, memicu kegagalan assertion saat tes memeriksa integritas durasi atau kontinuitas data.

Mengandalkan endpoint server eksternal saat CI berjalan memperparah masalah ini karena latensi internet publik tidak dapat diprediksi. Solusinya adalah memisahkan pengujian komponen menjadi loopback lokal murni dengan clock dan jitter simulation yang terkontrol.

Mock RTCPeerConnection Lokal Menggunakan Loopback In-Memory

Pengujian reliabilitas memerlukan kontrol penuh atas data stream tanpa ketergantungan pada STUN/TURN server atau infrastruktur WebRTC publik. Menggunakan loopback internal antara dua instance RTCPeerConnection lokal menghilangkan variabel jaringan eksternal.

import { RTCPeerConnection } from 'node-datachannel'; // atau implementasi webrtc node

export async function createLocalLoopback() {
  const pcA = new RTCPeerConnection({ iceServers: [] });
  const pcB = new RTCPeerConnection({ iceServers: [] });

  pcA.onicecandidate = (e) => {
    if (e.candidate) pcB.addIceCandidate(e.candidate);
  };
  pcB.onicecandidate = (e) => {
    if (e.candidate) pcA.addIceCandidate(e.candidate);
  };

  const channelA = pcA.createDataChannel('audio-stream');
  
  const bChannelPromise = new Promise((resolve) => {
    pcB.ondatachannel = (e) => resolve(e.channel);
  });

  const offer = await pcA.createOffer();
  await pcA.setLocalDescription(offer);
  await pcB.setRemoteDescription(offer);

  const answer = await pcB.createAnswer();
  await pcB.setLocalDescription(answer);
  await pcA.setRemoteDescription(answer);

  const channelB = await bChannelPromise;
  return { pcA, pcB, channelA, channelB };
}

Simulasi Jitter dan Verifikasi Toleransi Buffer Underrun

Untuk menguji ketahanan audio buffer terhadap underrun, sisipkan adaptor proksi di antara pengirim dan penerima. Adaptor ini mengintroduksi packet loss dan jitter buatan secara terukur sebelum data masuk ke ring buffer audio.

export class JitterLossBufferProxy {
  constructor(options = {}) {
    this.lossRate = options.lossRate || 0.0; // 0.0 sampai 1.0
    this.maxJitterMs = options.maxJitterMs || 0;
    this.onDeliver = options.onDeliver || (() => {});
  }

  push(packet) {
    // Simulasi Packet Loss
    if (Math.random() < this.lossRate) {
      return; // Drop paket
    }

    // Simulasi Network Jitter
    const delay = Math.floor(Math.random() * this.maxJitterMs);
    setTimeout(() => {
      this.onDeliver(packet);
    }, delay);
  }
}

Komponen Jitter Buffer yang Deterministik

Konsumen audio harus menggunakan buffer adaptif untuk meredam jitter tanpa membiarkan buffer kosong. Kode berikut memvalidasi bahwa sistem membangkitkan frame silence (PLC - Packet Loss Concealment sederhana) alih-alih melempar error saat underrun terjadi:

export class AudioPlaybackBuffer {
  constructor(frameSizeBytes = 960 * 2) {
    this.frameSizeBytes = frameSizeBytes;
    this.queue = [];
    this.underrunCount = 0;
  }

  enqueue(chunk) {
    this.queue.push(chunk);
  }

  // Dipanggil setiap tick timer audio (misal tiap 20ms)
  pullFrame() {
    if (this.queue.length === 0) {
      this.underrunCount++;
      // ponytail: return frame kosong untuk mencegah audio pipeline crash
      return new Uint8Array(this.frameSizeBytes);
    }
    return this.queue.shift();
  }
}

Pengujian Timeout Reconnect Deterministik di CI

Flaky test sering muncul saat menguji reconnection timeout menggunakan setTimeout nyata di lingkungan runner yang lambat. Gunakan fake timers untuk memajukan waktu pengujian secara deterministik tanpa delay wall-clock asinkron.

import { strict as assert } from 'node:assert';
import sinon from 'sinon';

describe('WebRTC Reconnection Reliability', () => {
  let clock;

  beforeEach(() => {
    clock = sinon.useFakeTimers();
  });

  afterEach(() => {
    clock.restore();
  });

  it('harus memicu disconnect event tepat saat timeout 5000ms tercapai', () => {
    let connectionState = 'connected';
    const RECONNECT_TIMEOUT_MS = 5000;

    function handleHeartbeatTimeout() {
      connectionState = 'disconnected';
    }

    const timer = setTimeout(handleHeartbeatTimeout, RECONNECT_TIMEOUT_MS);

    // Simulasi paket tidak kunjung datang, majukan clock buatan
    clock.tick(4999);
    assert.equal(connectionState, 'connected');

    clock.tick(1);
    assert.equal(connectionState, 'disconnected');
    
    clearTimeout(timer);
  });
});

Praktik Terbaik Pipeline CI untuk WebRTC Testing

  • Hindari Real Audio Drivers: Jalankan headless testing menggunakan file dummy/virtual sink (seperti dummy audio driver bawaan Chromium atau snd-dummy di Linux runner).
  • Gunakan Explicit Assertions: Hindari assertion berbasis waktu total runtime (misalnya expect(testTime).toBeLessThan(300)). Gunakan callback atau event counters spesifik seperti onUnderrun dan onTrackAdded.
  • Tetapkan Batas Loss Threshold: Saat menguji pipeline integrasi dengan LiveKit atau Gemini Live, verifikasi bahwa audio buffer tetap menghasilkan output frame dengan toleransi underrun spesifik daripada mengharapkan packet delivery 100%.

Mengisolasi jaringan menggunakan loopback in-memory dan mengontrol waktu pengujian via fake timers mengeliminasi kondisi balapan (race condition). Hasilnya adalah pipeline CI WebRTC yang deterministik, reliabel, dan bebas dari flaky test.