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:
@@ -32,6 +32,9 @@ struct AppState {
|
|||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
tracing_subscriber::fmt().init();
|
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 port: u16 = 8080;
|
||||||
let mut relay_addr: SocketAddr = "127.0.0.1:4433".parse()?;
|
let mut relay_addr: SocketAddr = "127.0.0.1:4433".parse()?;
|
||||||
|
|||||||
@@ -173,35 +173,45 @@ function startAudioCapture() {
|
|||||||
scriptNode.connect(audioCtx.destination);
|
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) {
|
function playAudio(pcmInt16) {
|
||||||
if (!audioCtx) return;
|
if (!audioCtx) return;
|
||||||
|
initPlayback();
|
||||||
|
|
||||||
const floatData = new Float32Array(pcmInt16.length);
|
const floatData = new Float32Array(pcmInt16.length);
|
||||||
for (let i = 0; i < pcmInt16.length; i++) {
|
for (let i = 0; i < pcmInt16.length; i++) {
|
||||||
floatData[i] = pcmInt16[i] / 32768.0;
|
floatData[i] = pcmInt16[i] / 32768.0;
|
||||||
}
|
}
|
||||||
|
playbackBuffer.push(floatData);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function startStatsUpdate() {
|
function startStatsUpdate() {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ SSH_KEY_NAME="wz"
|
|||||||
SSH_KEY_PATH="/Users/manwe/CascadeProjects/wzp"
|
SSH_KEY_PATH="/Users/manwe/CascadeProjects/wzp"
|
||||||
SERVER_NAME="wzp-builder-$(date +%s)"
|
SERVER_NAME="wzp-builder-$(date +%s)"
|
||||||
SERVER_TYPE="cx33"
|
SERVER_TYPE="cx33"
|
||||||
IMAGE="ubuntu-24.04"
|
IMAGE="debian-12"
|
||||||
REMOTE_USER="root"
|
REMOTE_USER="root"
|
||||||
OUTPUT_DIR="target/linux-x86_64"
|
OUTPUT_DIR="target/linux-x86_64"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user