openvibers/openvibe.live Commits

Player: stop forcing a zero playout buffer on both receivers

OP OpenVibers committed 18 Aug 2026 · 017e344c99 · 2 files, +23 −7

The A/V desync is on openvibe.live too, and openvibe.live consumes mediasoup
directly with no relay in the path — so the relay was never the cause. It is
this, applied unconditionally to BOTH the audio and video receivers:

    receiver.jitterBufferTarget = 0
    receiver.playoutDelayHint   = 0

The browser aligns audio to video from RTCP sender reports, which requires
permission to hold audio back until the matching video frame has decoded. A
zero target on the AUDIO receiver leaves nowhere to hold it, so audio renders
the instant it arrives and runs ahead of video. That is the "audio plays early"
report, on every platform, and it is why changing OBS encoder settings did
nothing — the desync was being created in the player, after the network.

The same zero target on video leaves no buffer to absorb ordinary jitter, so a
slightly late frame is just late: a frozen frame. The old comment claimed the
receiver would still grow the buffer adaptively, but a hard 0 fights that
adaptation instead of cooperating with it.

Leave audio on the browser's adaptive buffer — that is what performs the sync —
and give video a small non-zero target (120ms) instead of 0. Note the units
differ: jitterBufferTarget is milliseconds, playoutDelayHint is seconds; with
both previously set to 0 that distinction was invisible.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Xw6Sf5zzSALw8m93XNu5JE

Changes

ddiff --git a/public/index.html b/public/index.html
iindex ecff6c05..a5ff58d9 100644
---- a/public/index.html
++++ b/public/index.html
@@@ -4971,7 +4971,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<script src="/js/theme-engine.js?v=4"></script>
<script src="/live-notify.js?v=3" defer></script>
<script src="/js/app.js?v=123" defer></script>
--<script src="/js/stream-player.js?v=18" defer></script>
++<script src="/js/stream-player.js?v=19" defer></script>
<script src="/js/chat-shared.js?v=3" defer></script>
<script src="/js/emotes.js?v=8" defer></script>
<script src="/js/gif-picker.js?v=1" defer></script>
ddiff --git a/public/js/stream-player.js b/public/js/stream-player.js
iindex a75ca7ee..8e90bd58 100644
---- a/public/js/stream-player.js
++++ b/public/js/stream-player.js
@@@ -2020,14 +2020,30 @@ async function handleSfuViewerReady(msg, ws, video, updateStatus, scheduleRewatc
rtpParameters: consumerParams.rtpParameters,
});
-- // Minimize glass-to-glass latency: ask the browser for the smallest playout /
-- // jitter buffer it will honor. This is a hint — the receiver still grows the
-- // buffer adaptively under real packet loss, so it's low-latency without freezing.
++ // Playout buffering. This used to force BOTH receivers to 0, which removed the
++ // very slack playback depends on:
++ //
++ // • A/V sync. The browser aligns audio to video from RTCP sender reports, and
++ // that means holding audio back until the matching video frame has decoded.
++ // A 0 target on the AUDIO receiver leaves nowhere to hold it, so audio renders
++ // the instant it arrives and runs ahead of video — the "audio plays early"
++ // desync, which is why it showed up on openvibe.live and not just on restreams.
++ // • Freezes. A 0 target on video leaves no buffer to absorb ordinary network
++ // jitter, so a slightly late frame is simply late — visible as a frozen frame.
++ //
++ // The old comment claimed the receiver still grows the buffer adaptively; a hard 0
++ // target fights that adaptation rather than cooperating with it.
++ //
++ // So: leave AUDIO on the browser's adaptive buffer — that is the thing doing the
++ // syncing — and give VIDEO a small but non-zero target. Still low latency, with
++ // enough headroom to stay smooth and aligned. Raise this if streams still stutter.
++ const VIDEO_JITTER_TARGET_MS = 120;
try {
const receiver = consumer.rtpReceiver;
-- if (receiver) {
-- if ('jitterBufferTarget' in receiver) receiver.jitterBufferTarget = 0;
-- if ('playoutDelayHint' in receiver) receiver.playoutDelayHint = 0;
++ if (receiver && consumer.kind === 'video') {
++ // jitterBufferTarget is in milliseconds; playoutDelayHint is in seconds.
++ if ('jitterBufferTarget' in receiver) receiver.jitterBufferTarget = VIDEO_JITTER_TARGET_MS;
++ if ('playoutDelayHint' in receiver) receiver.playoutDelayHint = VIDEO_JITTER_TARGET_MS / 1000;
}
} catch { /* non-fatal — playback still works at default latency */ }