From 3b76636bdf74f71e469dbd5d337abeaaccf4b487 Mon Sep 17 00:00:00 2001 From: keradoxchen Date: Mon, 9 Feb 2026 02:36:50 +0000 Subject: [PATCH 1/2] fix(h264): replace `self` with `this` in H264Context.decode() `self` refers to `window` in browser context, causing SPS parameters to be set on the global object instead of the H264Context instance. This prevents the decoder from ever being properly configured. --- core/decoders/h264.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/core/decoders/h264.js b/core/decoders/h264.js index a508b674..180106a3 100644 --- a/core/decoders/h264.js +++ b/core/decoders/h264.js @@ -196,9 +196,9 @@ export class H264Context { } if (parser.profileIdc !== null) { - self._profileIdc = parser.profileIdc; - self._constraintSet = parser.constraintSet; - self._levelIdc = parser.levelIdc; + this._profileIdc = parser.profileIdc; + this._constraintSet = parser.constraintSet; + this._levelIdc = parser.levelIdc; } if (this._decoder === null || this._decoder.state !== 'configured') { @@ -206,12 +206,12 @@ export class H264Context { Log.Warn("Missing key frame. Can't decode until one arrives"); continue; } - if (self._profileIdc === null) { + if (this._profileIdc === null) { Log.Warn('Cannot config decoder. Have not received SPS and PPS yet.'); continue; } - this._configureDecoder(self._profileIdc, self._constraintSet, - self._levelIdc); + this._configureDecoder(this._profileIdc, this._constraintSet, + this._levelIdc); } result = this._preparePendingFrame(timestamp); From 5f71939e0d7d0496ba46d9f9a6e2d48494448e5d Mon Sep 17 00:00:00 2001 From: keradoxchen Date: Mon, 9 Feb 2026 02:38:21 +0000 Subject: [PATCH 2/2] fix(display): prevent render queue deadlock with video decoder pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The render queue blocks on unready video frames and triggers the `_flushing` mechanism in rfb.js, which stops all VNC message processing. This starves the VideoDecoder of input, preventing it from producing output — creating a deadlock where the queue waits for decoder output that can never arrive. Video frames now skip the queue blocking and draw asynchronously via the decoder's output callback. The flip operation waits for all pending frames to resolve before executing, preserving visual correctness without blocking the decoder input pipeline. --- core/display.js | 69 +++++++++++++++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/core/display.js b/core/display.js index a7bb2d6c..98f31d8c 100644 --- a/core/display.js +++ b/core/display.js @@ -16,6 +16,7 @@ export default class Display { this._renderQ = []; // queue drawing actions for in-order rendering this._flushPromise = null; + this._pendingFrames = []; // video frames awaiting decoder output // the full frame buffer (logical canvas) size this._fbWidth = 0; @@ -479,6 +480,22 @@ export default class Display { } } + _drawVideoFrame(pendingFrame, rect) { + let frame = pendingFrame.frame; + if (!frame) { + return; + } + if (frame.codedWidth < rect.width || frame.codedHeight < rect.height) { + Log.Warn("Decoded video frame does not cover its full rectangle area. Expecting at least " + + rect.width + "x" + rect.height + " but got " + + frame.codedWidth + "x" + frame.codedHeight); + } + this.drawImage(frame, + 0, 0, rect.width, rect.height, + rect.x, rect.y, rect.width, rect.height); + frame.close(); + } + _renderQPush(action) { this._renderQ.push(action); if (this._renderQ.length === 1) { @@ -501,7 +518,21 @@ export default class Display { const a = this._renderQ[0]; switch (a.type) { case 'flip': - this.flip(true); + if (this._pendingFrames.length > 0) { + // Wait for all pending video frames to be + // decoded before flipping, so they are + // visible on screen. + let display = this; + let frames = this._pendingFrames; + this._pendingFrames = []; + Promise.all(frames.map(f => f.promise)).then(() => { + display.flip(true); + display._scanRenderQ(); + }); + ready = false; + } else { + this.flip(true); + } break; case 'copy': this.copyImage(a.oldX, a.oldY, a.x, a.y, a.width, a.height, true); @@ -533,32 +564,20 @@ export default class Display { } break; case 'frame': - if (a.frame.ready) { - // The encoded frame may be larger than the rect due to - // limitations of the encoder, so we need to crop the - // frame. - let frame = a.frame.frame; - if (frame.codedWidth < a.width || frame.codedHeight < a.height) { - Log.Warn("Decoded video frame does not cover its full rectangle area. Expecting at least " + - a.width + "x" + a.height + " but got " + - frame.codedWidth + "x" + frame.codedHeight); - } - const sx = 0; - const sy = 0; - const sw = a.width; - const sh = a.height; - const dx = a.x; - const dy = a.y; - const dw = sw; - const dh = sh; - this.drawImage(frame, sx, sy, sw, sh, dx, dy, dw, dh); - frame.close(); - } else { + if (!a.frame.ready) { + // Don't block the queue — the video decoder + // pipeline needs continued input to produce + // output. Register a callback to draw later, + // and let the queue keep feeding the decoder. let display = this; - a.frame.promise.then(() => { - display._scanRenderQ(); + let pendingFrame = a.frame; + let rect = { x: a.x, y: a.y, width: a.width, height: a.height }; + pendingFrame.promise.then(() => { + display._drawVideoFrame(pendingFrame, rect); }); - ready = false; + this._pendingFrames.push(pendingFrame); + } else { + this._drawVideoFrame(a.frame, a); } break; }