Files
rustguac/static/guac/H264Decoder.js
T
Dave Kempe 799cdefd57 v1.0.0: H.264 passthrough, per-entry toggle, Docker fix
H.264 passthrough for RDP via guac_display worker integration:
- Raw H.264 NAL units from xrdp x264 pass through to browser
  WebCodecs VideoDecoder, bypassing server-side decode/re-encode
- Frame queue preserves all H.264 frames across deferred flushes
- Per-entry toggle in address book (enable_h264 field)
- Keyframe gate ensures decoder initializes correctly
- Per-connection callback (no static global concurrency bug)
- Queue capped at 120 frames to prevent unbounded growth

guacd patch (004-h264-display-worker):
- display-priv.h: h264 frame queue on guac_display_layer
- display-plan.c: flush h264 queue during plan_apply, skip IMG ops
- display-layer.c: set_h264 API with queue append and cap
- rdpgfx.c: SurfaceCommand wrapper saves NAL before GDI handler
- settings.c/h: enable-h264 connection parameter, conditional GfxH264

Other changes:
- Docker: fix FreeRDP plugin path for drive/audio channels (#87)
- contrib/setup-xrdp-gfx.sh: full xrdp x264 rebuild from Debian sid
- docs/rdp-video-performance.md: H.264 passthrough documentation
- Address book UI: H.264 checkbox with dependency descriptions
2026-03-28 21:52:28 +11:00

211 lines
5.7 KiB
JavaScript

/*
* H.264 decoder for Guacamole using the WebCodecs API.
* Decodes H.264 NAL units received via the "h264" instruction and
* renders decoded frames to a Guacamole Display layer.
*
* Copyright (C) 2026 Sol1 Pty Ltd. Licensed under Apache 2.0.
*/
var Guacamole = Guacamole || {};
/**
* H.264 video decoder that uses the WebCodecs VideoDecoder API for
* hardware-accelerated decoding of H.264 NAL units received from guacd.
*
* @constructor
* @param {!Guacamole.Display} display
* The Guacamole display to render decoded frames to.
*/
Guacamole.H264Decoder = function H264Decoder(display) {
/**
* The WebCodecs VideoDecoder instance, or null if not yet initialised
* or if WebCodecs is not supported.
*
* @private
* @type {?VideoDecoder}
*/
var decoder = null;
/**
* Whether the decoder has been configured with codec parameters.
*
* @private
* @type {boolean}
*/
var configured = false;
/**
* Monotonic timestamp counter for EncodedVideoChunk (microseconds).
*
* @private
* @type {number}
*/
var timestamp = 0;
/**
* The target layer for rendering decoded frames.
*
* @private
* @type {?Guacamole.Display.VisibleLayer}
*/
var targetLayer = null;
/**
* Pending draw position for the current frame.
*
* @private
*/
var pendingX = 0;
var pendingY = 0;
/**
* Total frames decoded.
*
* @type {number}
*/
this.framesDecoded = 0;
/**
* Total frames dropped or errored.
*
* @type {number}
*/
this.framesDropped = 0;
/**
* Reference to this for closures.
*/
var self = this;
/**
* Initialise the VideoDecoder if not already done.
*
* @private
* @param {number} width - Expected frame width.
* @param {number} height - Expected frame height.
*/
function ensureDecoder(width, height) {
if (decoder && configured)
return;
if (typeof VideoDecoder === 'undefined') {
console.warn('[rustguac] WebCodecs VideoDecoder not available');
return;
}
decoder = new VideoDecoder({
output: function(frame) {
self.framesDecoded++;
try {
// Draw the decoded VideoFrame directly to the layer's canvas
if (targetLayer) {
var canvas = targetLayer.getCanvas();
var ctx = canvas.getContext('2d');
ctx.drawImage(frame, pendingX, pendingY);
}
} finally {
// CRITICAL: always close VideoFrame to release GPU memory
frame.close();
}
},
error: function(e) {
self.framesDropped++;
console.error('[rustguac] H.264 decode error:', e.message);
}
});
// Configure for H.264 Constrained Baseline
// Let the decoder auto-detect level from the SPS NAL in the stream
decoder.configure({
codec: 'avc1.42001f', // Baseline profile, level 3.1
optimizeForLatency: true
});
configured = true;
console.log('[rustguac] H.264 WebCodecs decoder initialised (' + width + 'x' + height + ')');
}
/**
* Decode a complete H.264 NAL unit buffer and render to the given layer.
*
* @param {!Guacamole.Display.VisibleLayer} layer
* The layer to draw the decoded frame to.
* @param {number} x - X position on the layer.
* @param {number} y - Y position on the layer.
* @param {number} width - Frame width.
* @param {number} height - Frame height.
* @param {!ArrayBuffer} nalData - Raw H.264 NAL unit data (Annex B format).
* @param {boolean} isKeyFrame - Whether this contains an IDR/keyframe.
*/
this.decode = function(layer, x, y, width, height, nalData, isKeyFrame) {
ensureDecoder(width, height);
if (!decoder || decoder.state === 'closed')
return;
targetLayer = layer;
pendingX = x;
pendingY = y;
try {
var chunk = new EncodedVideoChunk({
type: isKeyFrame ? 'key' : 'delta',
timestamp: timestamp,
data: nalData
});
timestamp += 33333; // ~30fps in microseconds
decoder.decode(chunk);
} catch (e) {
self.framesDropped++;
console.error('[rustguac] H.264 chunk error:', e.message);
}
};
/**
* Reset the decoder (e.g. after reconnection or error recovery).
* The next frame must be a keyframe.
*/
this.reset = function() {
if (decoder && decoder.state !== 'closed') {
try {
decoder.reset();
configured = false;
timestamp = 0;
console.log('[rustguac] H.264 decoder reset');
} catch (e) {
// Decoder may be in error state
}
}
};
/**
* Close and release the decoder.
*/
this.destroy = function() {
if (decoder && decoder.state !== 'closed') {
try {
decoder.close();
} catch (e) {
// Ignore
}
}
decoder = null;
configured = false;
};
};
/**
* Check if the browser supports H.264 decoding via WebCodecs.
*
* @returns {boolean}
* true if WebCodecs VideoDecoder is available and supports H.264.
*/
Guacamole.H264Decoder.isSupported = function isSupported() {
return typeof VideoDecoder !== 'undefined';
};