zebra-spaces: predictive drain detector + self-calibrating floor (listener floor 1.3s)
Fox 2026-06-05: "both and see if we could lower to 1.3 secs as floor for listeners on the high quality stream." Two additions to the Double Dragon controller: 1. Predictive GROW (lead, not lag). The existing 5s-tick path watches loss/jitter — strictly a lagging indicator. We now ALSO watch the worklet's bufferedSeconds reports (every ~683ms) for rapid drain. If the buffer drops by more than 25% of its current target over a 1.5s window, GROW immediately — don't wait for loss to appear in the next 5s sample. Catches wiggle ~2s after it starts vs 5-10s on the lagging path. 2. Self-calibrating floor. Each publisher tracks its own maxWiggleDurationSec for the session. Floor for SHRINK is max(role-base, maxWiggle × 1.5). A stable publisher's listener can shrink to 1.3s. After observing a 1s wiggle the floor lifts to 1.5s; after a 4s wiggle, 6s (clamped to MAX). Resets when user leaves + rejoins. Constants: - DD_BASE_LISTENER_TARGET_SEC = 1.3 (was 2.0) - DD_WIGGLE_FLOOR_MULTIPLIER = 1.5 - DD_DRAIN_THRESH_FRAC = 0.25 - DD_DRAIN_WINDOW_SEC = 1.5 Telemetry log lines: - "double-dragon pub=XXXX PREDICTIVE GROW → 4s (buf drained Nms in N.Ns)" - "double-dragon pub=XXXX wiggle ended dur=N.Ns — floor now N.Ns" - "double-dragon pub=XXXX SHRINK target → N.Ns (clean N samples, role=X, max-wiggle=N.Ns)" This is the "lead + lag, AND self-tuned floor" loop. The 1.3s listener floor only applies to clean-history sessions. A wiggle-prone publisher will see the floor stay elevated automatically.
This commit is contained in:
parent
c5ac9e1a3b
commit
fa19f5df90
1 changed files with 103 additions and 14 deletions
|
|
@ -1986,14 +1986,18 @@ function installJitterBuffer(uuid, node){
|
|||
} else if (e.data.cmd === 'buffered'){
|
||||
/* worklet's current buffer depth (seconds). Used by lip-sync
|
||||
* to make video receivers track the audio's total delay.
|
||||
* stretchFactor is reported too — JS controller in Phase 3
|
||||
* uses it to drive the per-publisher target. */
|
||||
* stretchFactor is reported too — JS controller uses it. */
|
||||
const n = listenerAudioNodes.get(uuid);
|
||||
if (n){
|
||||
n.bufferedSeconds = e.data.seconds;
|
||||
n.stretchFactor = e.data.stretchFactor;
|
||||
}
|
||||
refreshLipSyncForUuid(uuid);
|
||||
/* Predictive drain detector — feed the Double Dragon
|
||||
* controller's lead-indicator path. ~683ms cadence is fast
|
||||
* enough to catch a wiggle within ~2s of it starting,
|
||||
* vs the 5s tick path which can be 5-10s late. */
|
||||
ddNoteWorkletBuffered(uuid, e.data.seconds);
|
||||
}
|
||||
};
|
||||
/* All roles now allow time-stretching — listeners benefit too
|
||||
|
|
@ -2185,12 +2189,31 @@ const DD_JITTER_THRESH_SEC = 0.030; /* > 30ms = unstable */
|
|||
const DD_UNSTABLE_SAMPLES_NEEDED = 2;
|
||||
const DD_CLEAN_SAMPLES_NEEDED = 6; /* 6 × 5s = 30s clean → recover */
|
||||
const DD_TELEMETRY_TICK_SEC = 5;
|
||||
const DD_BASE_TARGET_SEC = 0.5; /* speaker normal conversational cushion */
|
||||
const DD_BASE_LISTENER_TARGET_SEC = 2.0; /* listener clean-feed cushion — lower than initial 4s so latency feels less laggy when feed is good; controller grows back to MAX on any instability */
|
||||
const DD_BASE_TARGET_SEC = 0.5; /* speaker minimum on stable feed */
|
||||
const DD_BASE_LISTENER_TARGET_SEC = 1.3; /* listener minimum on stable high-quality feed — fox 2026-06-05 */
|
||||
const DD_MAX_TARGET_SEC = 4.0; /* maximum cushion under instability — same ceiling for everyone */
|
||||
/* Self-calibrating floor: after a wiggle, the floor for THAT publisher
|
||||
* lifts to wiggleDurationSec × DD_WIGGLE_FLOOR_MULTIPLIER. e.g. an
|
||||
* observed 1.5s wiggle → 2.25s floor. The floor only rises within a
|
||||
* session; resets when the user leaves + rejoins. */
|
||||
const DD_WIGGLE_FLOOR_MULTIPLIER = 1.5;
|
||||
/* Predictive drain detector: if the worklet's bufferedSeconds drops
|
||||
* by more than this fraction of its target over DD_DRAIN_WINDOW_SEC,
|
||||
* GROW immediately — don't wait for the 5s loss/jitter sample. This
|
||||
* is the "lead" half of the lead+lag system fox asked for. */
|
||||
const DD_DRAIN_THRESH_FRAC = 0.25;
|
||||
const DD_DRAIN_WINDOW_SEC = 1.5;
|
||||
function ddBaseTargetForRole(role){
|
||||
return role === 'listener' ? DD_BASE_LISTENER_TARGET_SEC : DD_BASE_TARGET_SEC;
|
||||
}
|
||||
/* Self-calibrating floor — max(role base, observed-wiggle × 1.5) */
|
||||
function ddEffectiveFloorForPub(pubHex){
|
||||
const base = ddBaseTargetForRole(myRole);
|
||||
const e = doubleDragon.get(pubHex);
|
||||
if (!e) return base;
|
||||
const adaptive = (e.maxWiggleDurationSec || 0) * DD_WIGGLE_FLOOR_MULTIPLIER;
|
||||
return Math.max(base, adaptive);
|
||||
}
|
||||
|
||||
function ddEntry(pubHex){
|
||||
let e = doubleDragon.get(pubHex);
|
||||
|
|
@ -2198,14 +2221,73 @@ function ddEntry(pubHex){
|
|||
/* Initial currentTargetSec matches what attachAudioStreamViaWorklet
|
||||
* actually set as the worklet's target — playoutDelayForRole(myRole).
|
||||
* Listeners start at 4s (RECV_PLAYOUT_DELAY_SEC), speakers at 0.5s.
|
||||
* Controller shrinks listener toward 2s on sustained clean stats. */
|
||||
* Controller shrinks listener toward DD_BASE_LISTENER_TARGET_SEC
|
||||
* on sustained clean stats. */
|
||||
e = { state: 'auto', unstableStreak: 0, cleanStreak: 0,
|
||||
lastLost: 0, lastSeenStatsAt: 0,
|
||||
currentTargetSec: playoutDelayForRole(myRole) };
|
||||
currentTargetSec: playoutDelayForRole(myRole),
|
||||
/* predictive drain detector: rolling history of buffered
|
||||
* seconds reports from the worklet (every ~683ms). 4 entries
|
||||
* ≈ 2.7s window — covers DD_DRAIN_WINDOW_SEC plus margin. */
|
||||
bufferedHistory: [],
|
||||
/* self-calibrating floor state */
|
||||
maxWiggleDurationSec: 0,
|
||||
wiggleStartedAt: 0 };
|
||||
doubleDragon.set(pubHex, e);
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
/* Called from the worklet's 'buffered' port message. Tracks the
|
||||
* recent bufferedSeconds for predictive drain detection (lead) and
|
||||
* records wiggle-event durations for the self-calibrating floor. */
|
||||
function ddNoteWorkletBuffered(uuid, bufferedSec){
|
||||
/* uuid → pubHex */
|
||||
const mm = members.get(uuid);
|
||||
if (!mm || !mm.pubkey) return;
|
||||
let pubHex;
|
||||
try { pubHex = hex(unb64(mm.pubkey)); } catch(_){ return; }
|
||||
const e = ddEntry(pubHex);
|
||||
if (e.state === 'manual-on' || e.state === 'manual-off') return;
|
||||
const now = Date.now();
|
||||
e.bufferedHistory.push({ t: now, v: bufferedSec });
|
||||
while (e.bufferedHistory.length > 0 &&
|
||||
(now - e.bufferedHistory[0].t) / 1000 > DD_DRAIN_WINDOW_SEC + 1.0){
|
||||
e.bufferedHistory.shift();
|
||||
}
|
||||
/* Predictive drain → GROW. If the buffer has dropped by more than
|
||||
* DD_DRAIN_THRESH_FRAC of the current target across the window,
|
||||
* something upstream is starving us. Lift the target NOW, before
|
||||
* loss appears in the next 5s tick. */
|
||||
if (e.bufferedHistory.length >= 3 && e.currentTargetSec < DD_MAX_TARGET_SEC){
|
||||
const oldest = e.bufferedHistory[0];
|
||||
const newest = e.bufferedHistory[e.bufferedHistory.length - 1];
|
||||
const dt = (newest.t - oldest.t) / 1000;
|
||||
const dv = newest.v - oldest.v;
|
||||
if (dt >= DD_DRAIN_WINDOW_SEC * 0.6 &&
|
||||
dv < -e.currentTargetSec * DD_DRAIN_THRESH_FRAC){
|
||||
e.currentTargetSec = DD_MAX_TARGET_SEC;
|
||||
ddSetTargetForPub(pubHex, DD_MAX_TARGET_SEC);
|
||||
e.wiggleStartedAt = now;
|
||||
logLine('', 'double-dragon pub='+pubHex.slice(0,4)+
|
||||
' PREDICTIVE GROW → '+DD_MAX_TARGET_SEC+'s (buf drained '+
|
||||
Math.abs(dv*1000).toFixed(0)+'ms in '+dt.toFixed(1)+'s)');
|
||||
}
|
||||
}
|
||||
/* Wiggle recovery: if a wiggle had been recorded and the buffer
|
||||
* comes back up to a healthy fraction of target, end the wiggle
|
||||
* and use its duration to lift the self-calibrating floor. */
|
||||
if (e.wiggleStartedAt > 0 && bufferedSec >= e.currentTargetSec * 0.85){
|
||||
const dur = (now - e.wiggleStartedAt) / 1000;
|
||||
e.wiggleStartedAt = 0;
|
||||
if (dur > e.maxWiggleDurationSec){
|
||||
e.maxWiggleDurationSec = dur;
|
||||
logLine('', 'double-dragon pub='+pubHex.slice(0,4)+
|
||||
' wiggle ended dur='+dur.toFixed(1)+'s — floor now '+
|
||||
ddEffectiveFloorForPub(pubHex).toFixed(2)+'s');
|
||||
}
|
||||
}
|
||||
}
|
||||
function ddNoteManualToggle(pubHex, isOn){
|
||||
const e = ddEntry(pubHex);
|
||||
e.state = isOn ? 'manual-on' : 'manual-off';
|
||||
|
|
@ -2261,7 +2343,6 @@ async function evaluateDoubleDragonForPub(pubHex){
|
|||
}
|
||||
});
|
||||
} catch(_){ return; }
|
||||
const baseTarget = ddBaseTargetForRole(myRole);
|
||||
const unstable = (lossRate > DD_LOSS_PER_SEC_THRESH) ||
|
||||
(jitter > DD_JITTER_THRESH_SEC);
|
||||
if (unstable){
|
||||
|
|
@ -2270,19 +2351,27 @@ async function evaluateDoubleDragonForPub(pubHex){
|
|||
e.currentTargetSec < DD_MAX_TARGET_SEC){
|
||||
e.currentTargetSec = DD_MAX_TARGET_SEC;
|
||||
ddSetTargetForPub(pubHex, DD_MAX_TARGET_SEC);
|
||||
if (e.wiggleStartedAt === 0) e.wiggleStartedAt = Date.now();
|
||||
logLine('', 'double-dragon pub='+pubHex.slice(0,4)+
|
||||
' GROW target → '+DD_MAX_TARGET_SEC+'s (loss/s='+
|
||||
lossRate.toFixed(1)+' jitter='+(jitter*1000).toFixed(0)+'ms)');
|
||||
}
|
||||
} else {
|
||||
e.cleanStreak++; e.unstableStreak = 0;
|
||||
/* Self-calibrating floor: a publisher that's caused a 2s wiggle
|
||||
* this session can't shrink below 3s; one that's been clean the
|
||||
* whole session can shrink to the role's base. fox 2026-06-05:
|
||||
* predictive lead + self-calibrating floor lets listeners hit
|
||||
* 1.3s on a clean high-quality stream. */
|
||||
const floor = ddEffectiveFloorForPub(pubHex);
|
||||
if (e.cleanStreak >= DD_CLEAN_SAMPLES_NEEDED &&
|
||||
e.currentTargetSec > baseTarget){
|
||||
e.currentTargetSec = baseTarget;
|
||||
ddSetTargetForPub(pubHex, baseTarget);
|
||||
e.currentTargetSec > floor){
|
||||
e.currentTargetSec = floor;
|
||||
ddSetTargetForPub(pubHex, floor);
|
||||
logLine('', 'double-dragon pub='+pubHex.slice(0,4)+
|
||||
' SHRINK target → '+baseTarget+'s (clean '+
|
||||
e.cleanStreak+' samples, role='+myRole+')');
|
||||
' SHRINK target → '+floor.toFixed(2)+'s (clean '+
|
||||
e.cleanStreak+' samples, role='+myRole+
|
||||
', max-wiggle='+e.maxWiggleDurationSec.toFixed(1)+'s)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6798,8 +6887,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');
|
|||
|
||||
<footer style="margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
|
||||
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> · built <span class="stamp-date">2026-06-05</span><br>
|
||||
md5 <span class="stamp-md5">1724c13cc98aa637c7462217cd46466d</span><br>
|
||||
sha256 <span class="stamp-sha">e6cb3dc549a0156290a35cd235fb2ec0ad6a9258a0d531eeb2e32c2589b186de</span><br>
|
||||
md5 <span class="stamp-md5">dda84f98199acdcd1bb7102cd2878ec9</span><br>
|
||||
sha256 <span class="stamp-sha">6a59cafee418659fbaeb94c6e0af9014a7359841a09c880a245901bb4657ac3c</span><br>
|
||||
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
|
||||
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify against these hashes; point at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
|
||||
</footer>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue