fix: pull-based audio playback eliminates drift + rustls crypto provider

Web playback rewritten from push-scheduling to pull-based ring buffer:
- ScriptProcessorNode pulls frames from buffer every ~21ms
- Buffer capped at 10 frames (~200ms) — drops oldest on overflow
- Latency permanently bounded, no drift over time

Also: install ring crypto provider for rustls TLS on Linux,
       build on debian-12 to match mequ glibc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-27 19:26:59 +04:00
parent 61d6fb173d
commit 4de72e2d98
3 changed files with 35 additions and 22 deletions

View File

@@ -32,6 +32,9 @@ struct AppState {
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt().init();
rustls::crypto::ring::default_provider()
.install_default()
.expect("failed to install rustls crypto provider");
let mut port: u16 = 8080;
let mut relay_addr: SocketAddr = "127.0.0.1:4433".parse()?;

View File

@@ -173,35 +173,45 @@ function startAudioCapture() {
scriptNode.connect(audioCtx.destination);
}
let nextPlayTime = 0;
// Ring buffer playback using AudioWorklet-style approach
let playbackBuffer = [];
let playbackNode = null;
function initPlayback() {
if (playbackNode) return;
// Use a ScriptProcessorNode as a pull-based audio sink.
// It asks for audio every ~21ms (1024 samples at 48kHz).
// We feed it from our ring buffer of received frames.
playbackNode = audioCtx.createScriptProcessor(1024, 1, 1);
playbackNode.onaudioprocess = (e) => {
const output = e.outputBuffer.getChannelData(0);
// Pull from buffer — drop old frames if we're behind
while (playbackBuffer.length > 10) {
playbackBuffer.shift(); // drop oldest, keeps latency bounded
}
if (playbackBuffer.length > 0) {
const frame = playbackBuffer.shift();
// frame is 960 samples, output is 1024 — copy what we can
const len = Math.min(frame.length, output.length);
for (let i = 0; i < len; i++) output[i] = frame[i];
for (let i = len; i < output.length; i++) output[i] = 0;
} else {
// Underrun — silence
for (let i = 0; i < output.length; i++) output[i] = 0;
}
};
playbackNode.connect(audioCtx.destination);
}
function playAudio(pcmInt16) {
if (!audioCtx) return;
initPlayback();
const floatData = new Float32Array(pcmInt16.length);
for (let i = 0; i < pcmInt16.length; i++) {
floatData[i] = pcmInt16[i] / 32768.0;
}
const buffer = audioCtx.createBuffer(1, floatData.length, SAMPLE_RATE);
buffer.getChannelData(0).set(floatData);
const source = audioCtx.createBufferSource();
source.buffer = buffer;
source.connect(audioCtx.destination);
const now = audioCtx.currentTime;
const MAX_LATENCY = 0.3; // 300ms max buffer — reset if we drift beyond this
if (nextPlayTime < now) {
// Fell behind — reset
nextPlayTime = now + 0.04;
} else if (nextPlayTime > now + MAX_LATENCY) {
// Too far ahead — skip to reduce latency
nextPlayTime = now + 0.04;
}
source.start(nextPlayTime);
nextPlayTime += buffer.duration;
playbackBuffer.push(floatData);
}
function startStatsUpdate() {

View File

@@ -12,7 +12,7 @@ SSH_KEY_NAME="wz"
SSH_KEY_PATH="/Users/manwe/CascadeProjects/wzp"
SERVER_NAME="wzp-builder-$(date +%s)"
SERVER_TYPE="cx33"
IMAGE="ubuntu-24.04"
IMAGE="debian-12"
REMOTE_USER="root"
OUTPUT_DIR="target/linux-x86_64"