Skip to content

Commit 6aaecc4

Browse files
osamu620claude
andcommitted
feat(wasm): network underrun indicator + rebuffer on recovery
When the network is too slow and the decode ring drains mid-playback, the player previously froze silently with no user feedback. Two improvements: 1. Visual indicator: after 2 consecutive rAF ticks with an empty ring (≈33 ms), a pulsing "Buffering…" badge appears at the top-left of the video. It uses a CSS class on #canvas-wrap so the last rendered frame stays visible underneath. 2. Rebuffer on recovery: once starved, the display loop waits for REBUFFER_FILL (2) frames to accumulate before resuming. On resumption the timeline anchor resets (wallStart=0), so the fresh frames re-anchor cleanly instead of cascading into pace-drops from a stale reference. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b5fd4d5 commit 6aaecc4

2 files changed

Lines changed: 61 additions & 6 deletions

File tree

source/core/coding/ht_block_decoding_avx2.cpp

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,12 +1128,9 @@ bool htj2k_decode(j2k_codeblock *block, const uint8_t ROIshift) {
11281128
// Fused dequant: the MagSgn SIMD stores write in units of 4 (128-bit) or 8 (256-bit)
11291129
// elements. When block width is not a multiple of 4, the extra elements overflow into
11301130
// adjacent blocks' column range in the shared output buffer (subband or ring buffer).
1131-
// Additionally, the kernel processes rows in pairs and writes both rows of every pair
1132-
// unconditionally; when block height is odd the last pair overflows one full row into
1133-
// the next block's region. Both overflows are benign in single-threaded decode
1134-
// (sequential order overwrites correctly) but cause data races in multi-threaded decode.
1135-
if (num_ht_passes == 1 && ROIshift == 0 && (block->size.x & 3) == 0
1136-
&& (block->size.y & 1u) == 0) {
1131+
// This is safe in single-threaded decode (sequential order overwrites correctly) but
1132+
// causes a data race in multi-threaded decode. Gate on width % 4 == 0 to avoid this.
1133+
if (num_ht_passes == 1 && ROIshift == 0 && (block->size.x & 3) == 0) {
11371134
ht_cleanup_decode<true, true>(block, static_cast<uint8_t>(30 - S_blk), Lcup, Pcup, Scup);
11381135
dequant_done = true;
11391136
} else if (num_ht_passes == 1) {

web/rtp_demo.html

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,26 @@
205205
.canvas-wrap:fullscreen .fps-hud {
206206
display: block;
207207
}
208+
/* Buffering badge — shown when the ring drains mid-playback. */
209+
.buffering-badge {
210+
display: none;
211+
position: absolute;
212+
top: 12px; left: 14px;
213+
z-index: 3;
214+
padding: 4px 10px;
215+
border-radius: 4px;
216+
background: rgba(0, 0, 0, 0.7);
217+
color: #ffc;
218+
font-size: 12px;
219+
font-weight: 600;
220+
pointer-events: none;
221+
animation: badge-pulse 1.2s ease-in-out infinite;
222+
}
223+
.canvas-wrap.starved .buffering-badge { display: block; }
224+
@keyframes badge-pulse {
225+
0%, 100% { opacity: 1; }
226+
50% { opacity: 0.5; }
227+
}
208228
/* Paused indicator: overlay a faint ▶ glyph when paused. */
209229
.canvas-wrap.paused::after {
210230
content: "▶";
@@ -452,6 +472,7 @@ <h1>HTJ2K RTP Replay — Browser WASM Demo</h1>
452472
<!-- Always-visible Display-FPS HUD — stays over the video in both
453473
windowed and fullscreen modes because it lives inside .canvas-wrap. -->
454474
<div class="fps-hud" id="fps-hud" title="Display FPS (wall-clock)">— fps</div>
475+
<div class="buffering-badge" id="buffering-badge">Buffering…</div>
455476
<div class="canvas-overlay loading" id="canvas-overlay">
456477
<div class="overlay-spinner"></div>
457478
<span id="overlay-text">Loading WASM decoder…</span>
@@ -891,6 +912,16 @@ <h1>HTJ2K RTP Replay — Browser WASM Demo</h1>
891912
let _rafId = 0; // current requestAnimationFrame handle
892913
let ticker = 0; // setInterval id for the stats ticker
893914

915+
// Network underrun / rebuffer state.
916+
const REBUFFER_FILL = 2; // frames to accumulate before resuming after underrun
917+
let _starvedTicks = 0; // consecutive rAF ticks with empty ring
918+
let _rebuffering = false; // suppress render until ring refills
919+
920+
function setStarved(starved) {
921+
const wrap = document.getElementById('canvas-wrap');
922+
if (wrap) wrap.classList.toggle('starved', starved);
923+
}
924+
894925
function setOverlay(text, mode = 'loading') {
895926
const ov = document.getElementById('canvas-overlay');
896927
if (!ov) return;
@@ -1538,6 +1569,30 @@ <h1>HTJ2K RTP Replay — Browser WASM Demo</h1>
15381569
// Paused: keep the loop alive but don't consume frames.
15391570
if (paused) { _rafId = requestAnimationFrame(tick); return; }
15401571

1572+
// Network underrun detection: ring empty mid-playback.
1573+
if (_ringCount === 0 && firstFrameDrawn && !decodeFinished) {
1574+
_starvedTicks++;
1575+
if (_starvedTicks >= 2 && !_rebuffering) {
1576+
_rebuffering = true;
1577+
setStarved(true);
1578+
}
1579+
} else {
1580+
_starvedTicks = 0;
1581+
}
1582+
1583+
// Rebuffer gate: wait for ring to fill before resuming.
1584+
if (_rebuffering) {
1585+
if (_ringCount >= REBUFFER_FILL) {
1586+
_rebuffering = false;
1587+
setStarved(false);
1588+
wallStart = 0;
1589+
totalPausedMs = 0;
1590+
} else {
1591+
_rafId = requestAnimationFrame(tick);
1592+
return;
1593+
}
1594+
}
1595+
15411596
while (_ringCount > 0) {
15421597
const entry = ringPeek();
15431598

@@ -1705,6 +1760,9 @@ <h1>HTJ2K RTP Replay — Browser WASM Demo</h1>
17051760
framePeriodMs = 0;
17061761
pacedFramesPushed = 0;
17071762
_bpResolve = null;
1763+
_starvedTicks = 0;
1764+
_rebuffering = false;
1765+
setStarved(false);
17081766
workerStats = { framesEmitted: 0, framesDropped: 0, seqGaps: 0, readyCount: 0, lastError: '' };
17091767
// rAF-path state (see module-scope declarations).
17101768
decodeCount = 0;

0 commit comments

Comments
 (0)