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
This commit is contained in:
Dave Kempe
2026-03-28 21:52:28 +11:00
parent b013678e42
commit 799cdefd57
15 changed files with 1012 additions and 270 deletions
Generated
+1 -1
View File
@@ -2911,7 +2911,7 @@ dependencies = [
[[package]]
name = "rustguac"
version = "0.9.2"
version = "1.0.0"
dependencies = [
"aes",
"axum",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rustguac"
version = "0.9.2"
version = "1.0.0"
edition = "2021"
description = "Lightweight Rust replacement for Apache Guacamole client"
+1 -1
View File
@@ -58,7 +58,7 @@ RUN /build/guacamole-server/configure \
&& make -j"$(nproc)" \
&& make install \
&& mkdir -p /opt/rustguac/lib/freerdp3 \
&& find /usr/lib -path "*/freerdp3/libguac*" -exec cp {} /opt/rustguac/lib/freerdp3/ \;
&& find /usr/lib /opt/rustguac/lib -path "*/freerdp3/libguac*" -exec cp {} /opt/rustguac/lib/freerdp3/ \;
# ---------------------------------------------------------------------------
# Stage 2: Build rustguac
+186 -59
View File
@@ -1,13 +1,22 @@
#!/bin/bash
# Setup GFX pipeline with H.264 encoding for xrdp on Debian 13 (trixie).
#
# Configures xrdp to use the Xorg backend with the GFX graphics pipeline
# and H.264 encoding via x264 for better video performance.
# This script rebuilds xrdp from the Debian sid source package with x264
# support enabled, installs matching xorgxrdp, configures the Xorg backend
# and GFX pipeline with H.264 encoding.
#
# The stock Debian 13 xrdp package does NOT include x264 support.
# This script adds the Debian sid (unstable) repo, rebuilds xrdp with
# --enable-x264, and pins trixie as the default to prevent accidental
# upgrades from sid.
#
# Run as root on the xrdp target machine (not the rustguac server).
# Requires: xrdp >= 0.10, xorgxrdp, libx264
# Requires: Debian 13 (trixie), ~10 minutes for the rebuild.
#
# Usage: sudo bash setup-xrdp-gfx.sh
#
# After running, also run setup-xrdp-audio.sh for audio redirection.
set -e
if [ "$(id -u)" -ne 0 ]; then
@@ -15,49 +24,152 @@ if [ "$(id -u)" -ne 0 ]; then
exit 1
fi
echo "=== Checking prerequisites ==="
if ! dpkg -l xrdp >/dev/null 2>&1; then
echo "Error: xrdp is not installed"
exit 1
fi
XRDP_VERSION=$(dpkg -l xrdp | awk '/^ii/{print $3}')
echo " xrdp version: $XRDP_VERSION"
if ! dpkg -l xorgxrdp >/dev/null 2>&1; then
echo "Error: xorgxrdp is not installed. Install with: apt install xorgxrdp"
exit 1
fi
echo " xorgxrdp: installed"
# Install x264 if not present
if ! dpkg -l libx264-164 >/dev/null 2>&1; then
echo ""
echo "=== Installing libx264 ==="
apt-get update -qq
apt-get install -y libx264-164
fi
echo " libx264: installed"
echo "============================================"
echo " xrdp GFX + H.264 Setup for Debian 13"
echo "============================================"
echo ""
echo "=== Configuring Xorg as default session ==="
if grep -q "^autorun=$" /etc/xrdp/xrdp.ini; then
sed -i "s/^autorun=$/autorun=Xorg/" /etc/xrdp/xrdp.ini
echo " Set autorun=Xorg"
elif grep -q "^autorun=Xorg" /etc/xrdp/xrdp.ini; then
echo " Already set to Xorg"
# ---------- Step 1: Add sid repo with pinning ----------
echo "=== Step 1: Configuring apt repos ==="
SID_LIST="/etc/apt/sources.list.d/sid.list"
PIN_FILE="/etc/apt/preferences.d/pin-trixie"
if [ ! -f "$SID_LIST" ]; then
echo "deb http://deb.debian.org/debian sid main" > "$SID_LIST"
echo " Added sid repo to $SID_LIST"
else
CURRENT=$(grep "^autorun=" /etc/xrdp/xrdp.ini)
echo " Warning: autorun is set to '$CURRENT' — not changing."
echo " For GFX support, set autorun=Xorg in /etc/xrdp/xrdp.ini"
echo " Sid repo already configured"
fi
if [ ! -f "$PIN_FILE" ]; then
cat > "$PIN_FILE" << 'PINEOF'
Package: *
Pin: release a=trixie
Pin-Priority: 900
Package: *
Pin: release a=unstable
Pin-Priority: 100
PINEOF
echo " Created apt pinning (trixie=900, sid=100)"
else
echo " Apt pinning already configured"
fi
apt-get update -qq
echo ""
echo "=== Creating GFX configuration ==="
# ---------- Step 2: Install xorgxrdp from sid ----------
echo "=== Step 2: Installing xorgxrdp from sid ==="
apt-get install -y -t unstable xorgxrdp
XORGXRDP_VER=$(dpkg -l xorgxrdp | awk '/^ii/{print $3}')
echo " xorgxrdp version: $XORGXRDP_VER"
echo ""
# ---------- Step 3: Rebuild xrdp with x264 ----------
echo "=== Step 3: Rebuilding xrdp with x264 support ==="
# Install build dependencies
apt-get install -y libx264-dev build-essential devscripts
apt-get build-dep -y xrdp 2>/dev/null || apt-get build-dep -y -t unstable xrdp
# Get xrdp source from sid
BUILD_DIR=$(mktemp -d /tmp/xrdp-build.XXXXXX)
echo " Build directory: $BUILD_DIR"
cd "$BUILD_DIR"
# Find the version in sid
XRDP_SID_VER=$(apt-cache showsrc -t unstable xrdp 2>/dev/null | grep "^Version:" | head -1 | awk '{print $2}')
if [ -z "$XRDP_SID_VER" ]; then
echo "Error: cannot find xrdp source in sid"
exit 1
fi
echo " Building xrdp $XRDP_SID_VER from sid source"
apt-get source "xrdp=$XRDP_SID_VER"
XRDP_DIR=$(ls -d xrdp-* | head -1)
cd "$XRDP_DIR"
# Patch debian/rules to add --enable-x264
if grep -q -- '--enable-x264' debian/rules; then
echo " debian/rules already has --enable-x264"
else
sed -i "s|--enable-opus|--enable-opus --enable-x264|" debian/rules
echo " Patched debian/rules: added --enable-x264"
fi
# Ensure libx264-dev is in Build-Depends
if grep -q 'libx264-dev' debian/control; then
echo " debian/control already has libx264-dev"
else
sed -i "s|^ autoconf,| libx264-dev,\n autoconf,|" debian/control
echo " Patched debian/control: added libx264-dev"
fi
# Build
echo " Building (this takes a few minutes)..."
dpkg-buildpackage -b -uc -us -j"$(nproc)" > /tmp/xrdp-build.log 2>&1
if [ $? -ne 0 ]; then
echo " Build failed! See /tmp/xrdp-build.log"
exit 1
fi
# Install
DEB=$(ls "$BUILD_DIR"/xrdp_*.deb | head -1)
echo " Installing $DEB"
dpkg -i "$DEB"
# Verify x264 is linked
if ldd /usr/sbin/xrdp 2>/dev/null | grep -q libx264; then
echo " Verified: xrdp linked to libx264"
else
echo " WARNING: xrdp does not appear to be linked to libx264"
fi
# Cleanup
rm -rf "$BUILD_DIR"
echo ""
# ---------- Step 4: Configure Xorg backend ----------
echo "=== Step 4: Configuring Xorg backend ==="
XRDP_INI="/etc/xrdp/xrdp.ini"
if grep -q "^autorun=Xorg" "$XRDP_INI"; then
echo " Already set to Xorg"
elif grep -q "^autorun=" "$XRDP_INI"; then
sed -i "s/^autorun=.*/autorun=Xorg/" "$XRDP_INI"
echo " Set autorun=Xorg"
else
echo "autorun=Xorg" >> "$XRDP_INI"
echo " Added autorun=Xorg"
fi
# Allow non-root to start Xorg
XWRAPPER="/etc/X11/Xwrapper.config"
if [ -f "$XWRAPPER" ]; then
if grep -q "allowed_users=anybody" "$XWRAPPER"; then
echo " Xwrapper already allows anybody"
else
sed -i "s/^allowed_users=.*/allowed_users=anybody/" "$XWRAPPER"
echo " Set Xwrapper allowed_users=anybody"
fi
else
echo "allowed_users=anybody" > "$XWRAPPER"
echo " Created Xwrapper with allowed_users=anybody"
fi
echo ""
# ---------- Step 5: Configure GFX pipeline ----------
echo "=== Step 5: Creating GFX configuration ==="
GFX_CONF="/etc/xrdp/gfx.toml"
if [ -f "$GFX_CONF" ]; then
cp "$GFX_CONF" "${GFX_CONF}.bak"
echo " Backed up existing $GFX_CONF to ${GFX_CONF}.bak"
echo " Backed up existing $GFX_CONF"
fi
cat > "$GFX_CONF" << 'EOF'
@@ -71,7 +183,7 @@ cat > "$GFX_CONF" << 'EOF'
order = ["H.264", "RFX"]
h264_encoder = "x264"
[x264]
[x264.default]
preset = "ultrafast"
tune = "zerolatency"
profile = "main"
@@ -79,37 +191,52 @@ vbv_max_bitrate = 0
vbv_buffer_size = 0
fps_num = 60
fps_den = 1
threads = 0
threads = 1
[x264.connection.lan]
preset = "ultrafast"
tune = "zerolatency"
vbv_max_bitrate = 0
fps_num = 60
fps_den = 1
[x264.lan]
# inherits default — uncapped bitrate, 60fps
[x264.connection.broadband_high]
preset = "ultrafast"
tune = "zerolatency"
vbv_max_bitrate = 20000
fps_num = 30
fps_den = 1
[x264.wan]
vbv_max_bitrate = 15000
vbv_buffer_size = 1500
[x264.broadband_high]
preset = "superfast"
vbv_max_bitrate = 8000
vbv_buffer_size = 800
[x264.broadband_low]
preset = "veryfast"
vbv_max_bitrate = 1600
vbv_buffer_size = 66
EOF
echo " Created $GFX_CONF"
echo ""
echo "=== Restarting xrdp ==="
# ---------- Step 6: Restart ----------
echo "=== Step 6: Restarting xrdp ==="
systemctl restart xrdp
echo " xrdp restarted"
echo ""
echo "=== Done ==="
echo "============================================"
echo " Setup complete!"
echo "============================================"
echo ""
echo "xrdp is now configured with:"
echo " - xrdp $(dpkg -l xrdp | awk '/^ii/{print $3}') rebuilt with x264 support"
echo " - xorgxrdp $XORGXRDP_VER (from sid)"
echo " - Xorg backend (required for GFX pipeline)"
echo " - H.264 encoding via x264 (60fps LAN, 30fps broadband)"
echo " - H.264 encoding via x264 (60fps LAN)"
echo " - RemoteFX as fallback codec"
echo " - Xwrapper allows non-root Xorg"
echo ""
echo "In rustguac, enable 'Graphics Pipeline (GFX)' and"
echo "'Desktop Composition' on RDP address book entries"
echo "for best video performance."
echo "Next steps:"
echo " 1. Run contrib/setup-xrdp-audio.sh for audio support"
echo " 2. Install a desktop environment (e.g. apt install task-xfce-desktop)"
echo " 3. In rustguac, enable 'Graphics Pipeline (GFX)' on RDP entries"
echo ""
echo "When connecting from rustguac with H.264 passthrough enabled,"
echo "the browser's WebCodecs VideoDecoder will decode H.264 directly"
echo "for low-latency, high-quality video."
+90 -21
View File
@@ -71,36 +71,89 @@ Check Windows Event Viewer at `Applications and Services Logs > Microsoft > Wind
## Linux xrdp Tuning (Debian 13)
Debian 13 (trixie) ships xrdp 0.10.1 with GFX pipeline and H.264 support.
Debian 13 (trixie) ships xrdp 0.10.x, but the stock package does **not** include x264 H.264 encoding support. The `contrib/setup-xrdp-gfx.sh` script rebuilds xrdp from the Debian sid source package with `--enable-x264` and configures the GFX pipeline.
### Quick Setup with Scripts
Helper scripts are provided in the `contrib/` directory of the rustguac repository. Run these **on the xrdp target machine** (not the rustguac server):
```bash
# 1. Install GFX pipeline with H.264 encoding
# 1. Rebuild xrdp with x264 + configure GFX pipeline (~10 minutes)
sudo bash contrib/setup-xrdp-gfx.sh
# 2. Install audio redirection (builds PulseAudio module from source)
sudo bash contrib/setup-xrdp-audio.sh
# 3. Install a desktop environment
sudo apt install task-xfce-desktop
```
The GFX script:
1. Adds Debian sid repo with pinning (trixie stays default)
2. Installs matching `xorgxrdp` from sid
3. Rebuilds `xrdp` from sid source with `--enable-x264`
4. Configures Xorg backend (`autorun=Xorg`)
5. Allows non-root Xorg via Xwrapper
6. Creates `/etc/xrdp/gfx.toml` with H.264 + x264 encoder
### Manual Setup
#### Prerequisites
```bash
apt install xrdp xorgxrdp libx264-164 libavcodec61 pulseaudio
# Add sid repo for newer xrdp source
echo "deb http://deb.debian.org/debian sid main" > /etc/apt/sources.list.d/sid.list
# Pin trixie as default (prevent accidental sid upgrades)
cat > /etc/apt/preferences.d/pin-trixie << 'EOF'
Package: *
Pin: release a=trixie
Pin-Priority: 900
Package: *
Pin: release a=unstable
Pin-Priority: 100
EOF
apt-get update
# Install xorgxrdp from sid (must match xrdp version)
apt-get install -y -t unstable xorgxrdp
# Install x264 and build dependencies
apt-get install -y libx264-dev build-essential devscripts
apt-get build-dep -y xrdp
```
#### Rebuild xrdp with x264
The stock Debian xrdp package is built without `--enable-x264`. Rebuild from sid source:
```bash
cd /tmp
apt-get source xrdp=<sid-version>
cd xrdp-*
sed -i "s|--enable-opus|--enable-opus --enable-x264|" debian/rules
sed -i "s|^ autoconf,| libx264-dev,\n autoconf,|" debian/control
dpkg-buildpackage -b -uc -us -j$(nproc)
dpkg -i ../xrdp_*.deb
```
Verify x264 is linked: `ldd /usr/sbin/xrdp | grep libx264`
#### GFX Pipeline (Video)
The GFX pipeline requires the Xorg backend (`libxup.so`), not Xvnc. Set in `/etc/xrdp/xrdp.ini`:
The GFX pipeline requires the Xorg backend, not Xvnc. Set in `/etc/xrdp/xrdp.ini`:
```ini
autorun=Xorg
```
Allow non-root Xorg in `/etc/X11/Xwrapper.config`:
```
allowed_users=anybody
```
Create `/etc/xrdp/gfx.toml`:
```toml
@@ -108,7 +161,7 @@ Create `/etc/xrdp/gfx.toml`:
order = ["H.264", "RFX"]
h264_encoder = "x264"
[x264]
[x264.default]
preset = "ultrafast"
tune = "zerolatency"
profile = "main"
@@ -116,21 +169,19 @@ vbv_max_bitrate = 0
vbv_buffer_size = 0
fps_num = 60
fps_den = 1
threads = 0
threads = 1
[x264.connection.lan]
preset = "ultrafast"
tune = "zerolatency"
vbv_max_bitrate = 0
fps_num = 60
fps_den = 1
[x264.lan]
# inherits default — uncapped bitrate, 60fps
[x264.connection.broadband_high]
preset = "ultrafast"
tune = "zerolatency"
vbv_max_bitrate = 20000
fps_num = 30
fps_den = 1
[x264.wan]
vbv_max_bitrate = 15000
vbv_buffer_size = 1500
[x264.broadband_high]
preset = "superfast"
vbv_max_bitrate = 8000
vbv_buffer_size = 800
```
#### Audio Redirection
@@ -188,9 +239,9 @@ For video monitoring workloads, a minimum of 20 Mbps per session is recommended.
## How It Works
The video pipeline through rustguac:
### Standard Pipeline (non-H.264 servers)
1. **RDP Server** encodes screen updates (H.264/AVC444 if enabled)
1. **RDP Server** sends screen updates (Planar/RemoteFX codec)
2. **FreeRDP** (inside guacd) decodes to bitmaps
3. **guacd** re-encodes dirty regions as JPEG, WebP, or PNG based on content type and network conditions
4. **rustguac** relays over WebSocket to the browser
@@ -201,4 +252,22 @@ guacd automatically adapts encoding quality based on network lag:
- Medium lag (50ms): quality 70 (balanced)
- High lag (80ms): quality 30 (aggressive compression)
The GFX pipeline enables RemoteFX codec between the RDP server and FreeRDP, which provides better compression than legacy bitmap updates.
### H.264 Passthrough Pipeline (xrdp with x264)
When the RDP server sends H.264 (AVC420/AVC444), guacd passes the raw H.264 NAL units directly to the browser, bypassing the server-side decode and re-encode:
1. **xrdp** encodes the screen as H.264 via x264
2. **FreeRDP** (inside guacd) receives the H.264 SurfaceCommand
3. **guacd** copies the raw H.264 NAL data and also runs the normal GDI decode (for frame sync)
4. During frame flush, guacd sends the raw H.264 data as a custom `h264` instruction
5. **rustguac** relays over WebSocket to the browser
6. **Browser** decodes H.264 using the [WebCodecs VideoDecoder API](https://developer.mozilla.org/en-US/docs/Web/API/VideoDecoder) (hardware-accelerated)
Benefits:
- **Lower server CPU** — no decode + re-encode cycle on the server
- **Lower latency** — one fewer encoding pass
- **Consistent quality** — single lossy encoding pass (x264) instead of H.264 → bitmap → JPEG/WebP
H.264 passthrough activates automatically when the RDP server sends AVC420/AVC444 codec data. Servers that don't support H.264 (stock Debian xrdp, Windows without GPU) use the standard pipeline automatically.
Browser requirements: Chrome/Edge 94+, Firefox 130+ (WebCodecs support).
+643
View File
@@ -0,0 +1,643 @@
diff --git a/src/libguac/display-layer-list.c b/src/libguac/display-layer-list.c
index f80aa3a4..3b891107 100644
--- a/src/libguac/display-layer-list.c
+++ b/src/libguac/display-layer-list.c
@@ -363,6 +363,14 @@ void guac_display_remove_layer(guac_display_layer* display_layer) {
guac_mem_free(display_layer->last_frame.buffer);
guac_mem_free(display_layer->pending_frame_cells);
+ /* Free any queued H.264 frames */
+ struct guac_h264_frame* h264_frame = display_layer->h264_queue;
+ while (h264_frame != NULL) {
+ struct guac_h264_frame* next = h264_frame->next;
+ guac_mem_free(h264_frame->data);
+ guac_mem_free(h264_frame);
+ h264_frame = next;
+ }
guac_mem_free(display_layer);
diff --git a/src/libguac/display-layer.c b/src/libguac/display-layer.c
index 2c983a15..410e42ac 100644
--- a/src/libguac/display-layer.c
+++ b/src/libguac/display-layer.c
@@ -20,6 +20,7 @@
#include "display-priv.h"
#include "guacamole/assert.h"
#include "guacamole/display.h"
+#include "guacamole/mem.h"
#include "guacamole/rect.h"
#include "guacamole/rwlock.h"
@@ -322,3 +323,43 @@ void guac_display_layer_close_cairo(guac_display_layer* layer, guac_display_laye
guac_rwlock_release_lock(&display->pending_frame.lock);
}
+
+void guac_display_layer_set_h264(guac_display_layer* layer,
+ const unsigned char* data, uint32_t length, int is_keyframe,
+ int x, int y, int width, int height) {
+
+ guac_display* display = layer->display;
+ guac_rwlock_acquire_write_lock(&display->pending_frame.lock);
+
+ /* Allocate a new queue entry and copy NAL unit data */
+ struct guac_h264_frame* frame = guac_mem_alloc(sizeof(struct guac_h264_frame));
+ frame->data = guac_mem_alloc(length);
+ memcpy(frame->data, data, length);
+ frame->length = length;
+ frame->is_keyframe = is_keyframe;
+ guac_rect_init(&frame->rect, x, y, width, height);
+ frame->next = NULL;
+
+ /* Append to queue */
+ if (layer->h264_queue_tail != NULL)
+ layer->h264_queue_tail->next = frame;
+ else
+ layer->h264_queue = frame;
+ layer->h264_queue_tail = frame;
+ layer->h264_queue_length++;
+
+ /* Cap queue at 120 frames (~2 seconds at 60fps) to prevent unbounded
+ * growth if frame flushes are stalled. Drop oldest frame from head. */
+ while (layer->h264_queue_length > 120 && layer->h264_queue != NULL) {
+ struct guac_h264_frame* old = layer->h264_queue;
+ layer->h264_queue = old->next;
+ if (layer->h264_queue == NULL)
+ layer->h264_queue_tail = NULL;
+ guac_mem_free(old->data);
+ guac_mem_free(old);
+ layer->h264_queue_length--;
+ }
+
+ guac_rwlock_release_lock(&display->pending_frame.lock);
+
+}
diff --git a/src/libguac/display-plan.c b/src/libguac/display-plan.c
index 6cd726bb..ff913da4 100644
--- a/src/libguac/display-plan.c
+++ b/src/libguac/display-plan.c
@@ -23,11 +23,15 @@
#include "guacamole/client.h"
#include "guacamole/display.h"
#include "guacamole/fifo.h"
+#include "guacamole/layer.h"
#include "guacamole/mem.h"
#include "guacamole/protocol.h"
+#include "guacamole/rect.h"
#include "guacamole/socket.h"
+#include "guacamole/stream.h"
#include "guacamole/timestamp.h"
+#include <stdio.h>
#include <string.h>
#include <cairo/cairo.h>
@@ -368,6 +372,120 @@ void guac_display_plan_free(guac_display_plan* plan) {
guac_mem_free(plan);
}
+/**
+ * Sends raw H.264 NAL unit data from the given layer to all connected clients
+ * as a custom "h264" instruction followed by blob data and an end instruction.
+ * After sending, the layer's H.264 data is freed and set to NULL.
+ *
+ * @param client
+ * The Guacamole client to send to.
+ *
+ * @param socket
+ * The socket to write instructions to.
+ *
+ * @param layer
+ * The display layer containing H.264 data to send.
+ */
+/**
+ * Sends a single H.264 frame to all connected clients as a custom "h264"
+ * instruction followed by blob data and an end instruction.
+ */
+static void guac_display_plan_send_h264_frame(guac_client* client,
+ guac_socket* socket, guac_display_layer* layer,
+ struct guac_h264_frame* frame) {
+
+ guac_stream* stream = guac_client_alloc_stream(client);
+
+ /* Build h264 instruction:
+ * h264 <stream> <layer> <keyframe> <x> <y> <width> <height> */
+ char arg_bufs[7][16];
+ int vals[] = {
+ stream->index,
+ layer->layer->index,
+ frame->is_keyframe,
+ frame->rect.left,
+ frame->rect.top,
+ guac_rect_width(&frame->rect),
+ guac_rect_height(&frame->rect)
+ };
+
+ guac_socket_instruction_begin(socket);
+ guac_socket_write_string(socket, "4.h264");
+ for (int i = 0; i < 7; i++) {
+ int n = snprintf(arg_bufs[i], sizeof(arg_bufs[i]), "%d", vals[i]);
+ char len_buf[8];
+ snprintf(len_buf, sizeof(len_buf), ",%d.", n);
+ guac_socket_write_string(socket, len_buf);
+ guac_socket_write_string(socket, arg_bufs[i]);
+ }
+ guac_socket_write_string(socket, ";");
+ guac_socket_instruction_end(socket);
+
+ /* Send H.264 NAL unit data as blob stream */
+ guac_protocol_send_blobs(socket, stream, frame->data, frame->length);
+ guac_protocol_send_end(socket, stream);
+ guac_client_free_stream(client, stream);
+
+}
+
+/**
+ * Sends all queued H.264 frames from the given layer to connected clients,
+ * freeing each frame after sending. Frames are sent in order to preserve
+ * the H.264 reference chain.
+ *
+ * @return
+ * The number of frames sent.
+ */
+static int guac_display_plan_flush_h264(guac_client* client,
+ guac_socket* socket, guac_display_layer* layer) {
+
+ int sent = 0;
+ struct guac_h264_frame* frame = layer->h264_queue;
+
+ while (frame != NULL) {
+ struct guac_h264_frame* next = frame->next;
+
+ /* Skip P-frames until first keyframe has been sent */
+ if (!frame->is_keyframe && !layer->h264_keyframe_sent) {
+ guac_client_log(client, GUAC_LOG_DEBUG,
+ "H.264: dropping %u byte delta (no keyframe yet)",
+ frame->length);
+ guac_mem_free(frame->data);
+ guac_mem_free(frame);
+ frame = next;
+ continue;
+ }
+
+ guac_display_plan_send_h264_frame(client, socket, layer, frame);
+
+ if (frame->is_keyframe)
+ layer->h264_keyframe_sent = 1;
+
+ guac_client_log(client, GUAC_LOG_DEBUG,
+ "H.264: sent %u bytes, %s, %dx%d",
+ frame->length,
+ frame->is_keyframe ? "KEYFRAME" : "delta",
+ guac_rect_width(&frame->rect),
+ guac_rect_height(&frame->rect));
+
+ guac_mem_free(frame->data);
+ guac_mem_free(frame);
+ frame = next;
+ sent++;
+ }
+
+ layer->h264_queue = NULL;
+ layer->h264_queue_tail = NULL;
+ layer->h264_queue_length = 0;
+
+ if (sent > 0)
+ guac_client_log(client, GUAC_LOG_DEBUG,
+ "H.264 passthrough: flushed %d frames", sent);
+
+ return sent;
+
+}
+
void guac_display_plan_apply(guac_display_plan* plan) {
guac_display* display = plan->display;
@@ -378,6 +496,40 @@ void guac_display_plan_apply(guac_display_plan* plan) {
* AFTER the non-image instructions have finished being written */
guac_fifo_lock(&display->ops);
+ /* Send any pending H.264 passthrough data before dispatching operations
+ * to worker threads. This runs while the ops FIFO is locked, so worker
+ * threads have not started yet and there is no socket contention.
+ *
+ * We track up to 8 unique layers that had H.264 data sent, so we can
+ * skip their IMG operations below. In practice there is usually only
+ * one (the default layer). */
+ guac_display_layer* h264_layers[8] = { NULL };
+ int h264_layer_count = 0;
+
+ for (int i = 0; i < plan->length && h264_layer_count < 8; i++) {
+
+ guac_display_layer* layer = plan->ops[i].layer;
+ if (layer->h264_queue == NULL)
+ continue;
+
+ /* Check if we already flushed H.264 for this layer */
+ int already_sent = 0;
+ for (int j = 0; j < h264_layer_count; j++) {
+ if (h264_layers[j] == layer) {
+ already_sent = 1;
+ break;
+ }
+ }
+
+ if (already_sent)
+ continue;
+
+ /* Flush all queued H.264 frames for this layer */
+ if (guac_display_plan_flush_h264(client, client->socket, layer) > 0)
+ h264_layers[h264_layer_count++] = layer;
+
+ }
+
/* Immediately send instructions for all updates that do not involve
* significant processing (do not involve encoding anything). This allows
* us to use the worker threads solely for encoding, reducing contention
@@ -385,6 +537,22 @@ void guac_display_plan_apply(guac_display_plan* plan) {
for (int i = 0; i < plan->length; i++) {
guac_display_layer* display_layer = op->layer;
+
+ /* Skip all operations for layers that had H.264 data sent — the
+ * H.264 stream replaces JPEG/PNG/WebP encoding entirely */
+ int is_h264_layer = 0;
+ for (int j = 0; j < h264_layer_count; j++) {
+ if (h264_layers[j] == display_layer) {
+ is_h264_layer = 1;
+ break;
+ }
+ }
+
+ if (is_h264_layer) {
+ op++;
+ continue;
+ }
+
switch (op->type) {
case GUAC_DISPLAY_PLAN_OPERATION_COPY:
diff --git a/src/libguac/display-priv.h b/src/libguac/display-priv.h
index 255fb897..b80fd5fe 100644
--- a/src/libguac/display-priv.h
+++ b/src/libguac/display-priv.h
@@ -317,6 +317,19 @@ struct guac_display_render_thread {
};
+/**
+ * A queued H.264 frame awaiting delivery to clients. Frames are queued
+ * because guac_display may defer frame flushes, and H.264 requires ALL
+ * frames in sequence (losing a P-frame breaks the reference chain).
+ */
+struct guac_h264_frame {
+ unsigned char* data;
+ uint32_t length;
+ int is_keyframe;
+ guac_rect rect;
+ struct guac_h264_frame* next;
+};
+
/**
* Approximation of how often a region of a layer is modified, as well as what
* changes have been made to that region since the last frame. This information
@@ -603,6 +616,37 @@ struct guac_display_layer {
*/
size_t pending_frame_cells_height;
+ /* ---------------- H.264 PASSTHROUGH DATA ---------------- */
+
+ /**
+ * Head of the queue of pending H.264 frames. Frames are appended by
+ * guac_display_layer_set_h264() and consumed during plan application.
+ * Frames are queued because guac_display may defer frame flushes, and
+ * H.264 requires ALL frames in sequence (losing a P-frame breaks the
+ * reference chain).
+ *
+ * IMPORTANT: The display-level pending_frame.lock MUST be acquired before
+ * modifying or reading this member.
+ */
+ struct guac_h264_frame* h264_queue;
+
+ /**
+ * Tail of the H.264 frame queue for O(1) append.
+ */
+ struct guac_h264_frame* h264_queue_tail;
+
+ /**
+ * Number of frames currently in the H.264 queue.
+ */
+ int h264_queue_length;
+
+ /**
+ * Non-zero if at least one H.264 keyframe has been sent to clients for
+ * this layer. P-frames are only sent after a keyframe has established
+ * the decoder reference state.
+ */
+ int h264_keyframe_sent;
+
};
typedef struct guac_display_state {
diff --git a/src/libguac/guacamole/display.h b/src/libguac/guacamole/display.h
index 9894042a..d1a990fe 100644
--- a/src/libguac/guacamole/display.h
+++ b/src/libguac/guacamole/display.h
@@ -417,6 +417,43 @@ guac_display_layer* guac_display_alloc_buffer(guac_display* display, int opaque)
*/
void guac_display_free_layer(guac_display_layer* display_layer);
+/**
+ * Stores raw H.264 NAL unit data on the given layer for passthrough to
+ * clients during the next frame flush. When H.264 data is set, the display
+ * will send it directly to clients instead of encoding the layer's pixel data
+ * as JPEG/PNG/WebP for the affected region.
+ *
+ * This function acquires the display's pending_frame lock internally and must
+ * NOT be called while that lock is already held by the calling thread.
+ *
+ * @param layer
+ * The layer to set H.264 data on.
+ *
+ * @param data
+ * The raw H.264 NAL unit data (Annex B format).
+ *
+ * @param length
+ * The length of the H.264 data in bytes.
+ *
+ * @param is_keyframe
+ * Non-zero if this data contains an IDR (keyframe), zero otherwise.
+ *
+ * @param x
+ * The X coordinate of the region this data covers.
+ *
+ * @param y
+ * The Y coordinate of the region this data covers.
+ *
+ * @param width
+ * The width of the region this data covers, in pixels.
+ *
+ * @param height
+ * The height of the region this data covers, in pixels.
+ */
+void guac_display_layer_set_h264(guac_display_layer* layer,
+ const unsigned char* data, uint32_t length, int is_keyframe,
+ int x, int y, int width, int height);
+
/**
* Returns a layer representing the current mouse cursor icon. Changes to the
* contents of this layer will affect the remote mouse cursor after the current
diff --git a/src/protocols/rdp/channels/rdpgfx.c b/src/protocols/rdp/channels/rdpgfx.c
index 327e7c2b..55cd8cfe 100644
--- a/src/protocols/rdp/channels/rdpgfx.c
+++ b/src/protocols/rdp/channels/rdpgfx.c
@@ -24,14 +24,122 @@
#include "settings.h"
#include <freerdp/client/rdpgfx.h>
+#include <freerdp/channels/rdpgfx.h>
#include <freerdp/freerdp.h>
#include <freerdp/gdi/gfx.h>
#include <freerdp/event.h>
#include <guacamole/client.h>
+#include <guacamole/display.h>
#include <stdlib.h>
#include <string.h>
+/**
+ * Check if a H.264 NAL unit buffer contains a keyframe (IDR slice or SPS).
+ */
+static int guac_rdp_h264_is_keyframe(const BYTE* data, UINT32 length) {
+ for (UINT32 i = 0; i + 3 < length; i++) {
+ if (data[i] == 0 && data[i+1] == 0) {
+ int offset = -1;
+ if (data[i+2] == 1)
+ offset = i + 3;
+ else if (i + 4 < length && data[i+2] == 0 && data[i+3] == 1)
+ offset = i + 4;
+ if (offset >= 0 && (UINT32) offset < length) {
+ int nal_type = data[offset] & 0x1F;
+ if (nal_type == 5 || nal_type == 7)
+ return 1;
+ }
+ }
+ }
+ return 0;
+}
+
+/**
+ * Wrapper for the FreeRDP GDI SurfaceCommand callback. Intercepts AVC420
+ * and AVC444 commands to store raw H.264 NAL units on the display layer
+ * for passthrough to clients during the next frame flush.
+ *
+ * The original handler is always called to perform GDI decoding, which
+ * updates the pixel buffer and dirty tracking. The H.264 data is sent
+ * to clients by the display worker pipeline (in guac_display_plan_apply)
+ * instead of encoding the pixel data as JPEG/PNG/WebP.
+ */
+static UINT guac_rdp_gfx_surface_command(RdpgfxClientContext* context,
+ const RDPGFX_SURFACE_COMMAND* cmd) {
+
+ /* gfx->custom points to rdpGdi (set by gdi_graphics_pipeline_init) */
+ rdpGdi* gdi = (rdpGdi*) context->custom;
+ rdpContext* rdp_context = gdi->context;
+ guac_client* client = ((rdp_freerdp_context*) rdp_context)->client;
+ guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+
+ /* Save H.264 NAL data BEFORE calling the original handler. The original
+ * GDI handler may free or modify cmd->extra data during AVC decoding,
+ * so we must copy it while it's still valid. */
+ unsigned char* saved_h264 = NULL;
+ UINT32 saved_length = 0;
+ int saved_keyframe = 0;
+
+ if (cmd->codecId == RDPGFX_CODECID_AVC420 && cmd->extra != NULL) {
+ RDPGFX_AVC420_BITMAP_STREAM* avc420 =
+ (RDPGFX_AVC420_BITMAP_STREAM*) cmd->extra;
+ if (avc420->data != NULL && avc420->length > 0) {
+ saved_length = avc420->length;
+ saved_h264 = malloc(saved_length);
+ if (saved_h264 != NULL) {
+ memcpy(saved_h264, avc420->data, saved_length);
+ saved_keyframe = guac_rdp_h264_is_keyframe(
+ saved_h264, saved_length);
+ }
+ }
+ }
+ else if ((cmd->codecId == RDPGFX_CODECID_AVC444
+ || cmd->codecId == RDPGFX_CODECID_AVC444v2)
+ && cmd->extra != NULL) {
+ RDPGFX_AVC444_BITMAP_STREAM* avc444 =
+ (RDPGFX_AVC444_BITMAP_STREAM*) cmd->extra;
+ if (avc444->bitstream[0].data != NULL
+ && avc444->bitstream[0].length > 0) {
+ saved_length = avc444->bitstream[0].length;
+ saved_h264 = malloc(saved_length);
+ if (saved_h264 != NULL) {
+ memcpy(saved_h264, avc444->bitstream[0].data, saved_length);
+ saved_keyframe = guac_rdp_h264_is_keyframe(
+ saved_h264, saved_length);
+ }
+ }
+ }
+
+ /* Run normal GDI decode — this calls BeginPaint/EndPaint internally,
+ * updates the pixel buffer, and marks the dirty region. Must happen
+ * after we've saved the H.264 data above. */
+ UINT result = CHANNEL_RC_OK;
+ pcRdpgfxSurfaceCommand orig =
+ (pcRdpgfxSurfaceCommand) rdp_client->orig_surface_command;
+ if (orig != NULL)
+ result = orig(context, cmd);
+
+ /* Store saved H.264 data on the display layer for passthrough.
+ * guac_display_layer_set_h264 acquires pending_frame.lock internally,
+ * which is safe since EndPaint has already released it above. */
+ if (saved_h264 != NULL) {
+ guac_display_layer* default_layer =
+ guac_display_default_layer(rdp_client->display);
+ guac_display_layer_set_h264(default_layer,
+ saved_h264, saved_length, saved_keyframe,
+ cmd->left, cmd->top, cmd->width, cmd->height);
+ guac_client_log(client, GUAC_LOG_TRACE,
+ "H.264 passthrough: %u bytes, %s, %ux%u at %u,%u",
+ saved_length,
+ saved_keyframe ? "keyframe" : "delta",
+ cmd->width, cmd->height, cmd->left, cmd->top);
+ free(saved_h264);
+ }
+
+ return result;
+}
+
/**
* Callback which associates handlers specific to Guacamole with the
* RdpgfxClientContext instance allocated by FreeRDP to deal with received
@@ -63,13 +171,25 @@ static void guac_rdp_rdpgfx_channel_connected(rdpContext* context,
RdpgfxClientContext* rdpgfx = (RdpgfxClientContext*) args->pInterface;
rdpGdi* gdi = context->gdi;
- if (!gdi_graphics_pipeline_init(gdi, rdpgfx))
+ if (!gdi_graphics_pipeline_init(gdi, rdpgfx)) {
guac_client_log(client, GUAC_LOG_WARNING, "Rendering backend for RDPGFX "
"channel could not be loaded. Graphics may not render at all!");
- else
+ }
+ else {
guac_client_log(client, GUAC_LOG_DEBUG, "RDPGFX channel will be used for "
"the RDP Graphics Pipeline Extension.");
+ /* Wrap SurfaceCommand to intercept H.264 data after GDI decode.
+ * Store the original callback per-connection to avoid clobbering
+ * when multiple RDP sessions are active concurrently. */
+ guac_rdp_client* rdp_client = (guac_rdp_client*) client->data;
+ rdp_client->orig_surface_command = (void*) rdpgfx->SurfaceCommand;
+ rdpgfx->SurfaceCommand = guac_rdp_gfx_surface_command;
+
+ guac_client_log(client, GUAC_LOG_INFO,
+ "H.264 passthrough enabled for RDPGFX channel.");
+ }
+
}
/**
@@ -120,4 +240,3 @@ void guac_rdp_rdpgfx_load_plugin(rdpContext* context) {
guac_freerdp_dynamic_channel_collection_add(context->settings, "rdpgfx", NULL);
}
-
diff --git a/src/protocols/rdp/rdp.h b/src/protocols/rdp/rdp.h
index 065080a3..a0893ffb 100644
--- a/src/protocols/rdp/rdp.h
+++ b/src/protocols/rdp/rdp.h
@@ -132,6 +132,13 @@ typedef struct guac_rdp_client {
*/
guac_display_render_thread* render_thread;
+ /**
+ * The original GFX SurfaceCommand handler installed by
+ * gdi_graphics_pipeline_init(). Stored per-connection so multiple
+ * concurrent RDP sessions don't clobber each other's callback.
+ */
+ void* orig_surface_command;
+
/**
* Queue of mouse, keyboard, and touch events. These events are accumulated
* and flushed within the RDP client thread to avoid spending excessive
diff --git a/src/protocols/rdp/settings.c b/src/protocols/rdp/settings.c
index d97d98e1..26648d09 100644
--- a/src/protocols/rdp/settings.c
+++ b/src/protocols/rdp/settings.c
@@ -100,6 +100,7 @@ const char* GUAC_RDP_CLIENT_ARGS[] = {
"disable-offscreen-caching",
"disable-glyph-caching",
"disable-gfx",
+ "enable-h264",
"preconnection-id",
"preconnection-blob",
"timezone",
@@ -440,6 +441,12 @@ enum RDP_ARGS_IDX {
*/
IDX_DISABLE_GFX,
+ /**
+ * Whether H.264 passthrough should be enabled, negotiating GfxH264 and
+ * GfxAVC444 with the RDP server.
+ */
+ IDX_ENABLE_H264,
+
/**
* The preconnection ID to send within the preconnection PDU when
* initiating an RDP connection, if any.
@@ -1290,6 +1297,11 @@ guac_rdp_settings* guac_rdp_parse_args(guac_user* user,
!guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv,
IDX_DISABLE_GFX, 0);
+ /* H.264 passthrough enable/disable */
+ settings->enable_h264 =
+ guac_user_parse_args_boolean(user, GUAC_RDP_CLIENT_ARGS, argv,
+ IDX_ENABLE_H264, 0);
+
/* Session color depth */
settings->color_depth =
guac_user_parse_args_int(user, GUAC_RDP_CLIENT_ARGS, argv,
@@ -1619,6 +1631,10 @@ void guac_rdp_push_settings(guac_client* client,
freerdp_settings_set_bool(rdp_settings, FreeRDP_SupportGraphicsPipeline, TRUE);
freerdp_settings_set_bool(rdp_settings, FreeRDP_RemoteFxCodec, TRUE);
+ if (guac_settings->enable_h264) {
+ freerdp_settings_set_bool(rdp_settings, FreeRDP_GfxH264, TRUE);
+ freerdp_settings_set_bool(rdp_settings, FreeRDP_GfxAVC444, TRUE);
+ }
if (freerdp_settings_get_uint32(rdp_settings, FreeRDP_ColorDepth) != RDP_GFX_REQUIRED_DEPTH) {
guac_client_log(client, GUAC_LOG_WARNING, "Ignoring requested "
@@ -1878,6 +1894,10 @@ void guac_rdp_push_settings(guac_client* client,
rdp_settings->SupportGraphicsPipeline = TRUE;
rdp_settings->RemoteFxCodec = TRUE;
+ if (settings->enable_h264) {
+ rdp_settings->GfxH264 = TRUE;
+ rdp_settings->GfxAVC444 = TRUE;
+ }
if (rdp_settings->ColorDepth != RDP_GFX_REQUIRED_DEPTH) {
guac_client_log(client, GUAC_LOG_WARNING, "Ignoring requested "
diff --git a/src/protocols/rdp/settings.h b/src/protocols/rdp/settings.h
index 87745cf5..e752b565 100644
--- a/src/protocols/rdp/settings.h
+++ b/src/protocols/rdp/settings.h
@@ -652,6 +652,11 @@ typedef struct guac_rdp_settings {
*/
int enable_gfx;
+ /**
+ * Whether H.264 passthrough is enabled (GfxH264/GfxAVC444 negotiation).
+ */
+ int enable_h264;
+
/**
* Whether multi-touch support is enabled.
*/
-172
View File
@@ -1,172 +0,0 @@
diff --git a/src/protocols/rdp/channels/rdpgfx.c b/src/protocols/rdp/channels/rdpgfx.c
index 327e7c2b..260681f2 100644
--- a/src/protocols/rdp/channels/rdpgfx.c
+++ b/src/protocols/rdp/channels/rdpgfx.c
@@ -24,14 +24,120 @@
#include "settings.h"
#include <freerdp/client/rdpgfx.h>
+#include <freerdp/channels/rdpgfx.h>
#include <freerdp/freerdp.h>
#include <freerdp/gdi/gfx.h>
#include <freerdp/event.h>
#include <guacamole/client.h>
+#include <guacamole/protocol.h>
+#include <guacamole/socket.h>
+#include <guacamole/stream.h>
+#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+/**
+ * The original SurfaceCommand handler installed by gdi_graphics_pipeline_init().
+ */
+static pcRdpgfxSurfaceCommand guac_rdpgfx_orig_surface_command = NULL;
+
+/**
+ * Check if a H.264 NAL unit buffer contains a keyframe (IDR slice).
+ */
+static int guac_rdp_h264_is_keyframe(const BYTE* data, UINT32 length) {
+ for (UINT32 i = 0; i + 3 < length; i++) {
+ if (data[i] == 0 && data[i+1] == 0) {
+ int offset = -1;
+ if (data[i+2] == 1)
+ offset = i + 3;
+ else if (i + 4 < length && data[i+2] == 0 && data[i+3] == 1)
+ offset = i + 4;
+ if (offset >= 0 && (UINT32) offset < length) {
+ int nal_type = data[offset] & 0x1F;
+ if (nal_type == 5 || nal_type == 7)
+ return 1;
+ }
+ }
+ }
+ return 0;
+}
+
+/**
+ * Send raw H.264 NAL units to the Guacamole client as a custom "h264"
+ * instruction followed by blob data and an end instruction.
+ */
+static void guac_rdp_h264_send_passthrough(guac_client* client,
+ const RDPGFX_SURFACE_COMMAND* cmd,
+ const BYTE* h264_data, UINT32 h264_length) {
+
+ guac_socket* socket = client->socket;
+ guac_stream* stream = guac_client_alloc_stream(client);
+ int keyframe = guac_rdp_h264_is_keyframe(h264_data, h264_length);
+
+ /* Build and send h264 instruction header */
+ char arg_bufs[7][16];
+ int vals[] = { stream->index, 0, keyframe,
+ (int) cmd->left, (int) cmd->top,
+ (int) cmd->width, (int) cmd->height };
+
+ guac_socket_instruction_begin(socket);
+ guac_socket_write_string(socket, "4.h264");
+ for (int i = 0; i < 7; i++) {
+ int n = snprintf(arg_bufs[i], sizeof(arg_bufs[i]), "%d", vals[i]);
+ char len_buf[8];
+ snprintf(len_buf, sizeof(len_buf), ",%d.", n);
+ guac_socket_write_string(socket, len_buf);
+ guac_socket_write_string(socket, arg_bufs[i]);
+ }
+ guac_socket_write_string(socket, ";");
+ guac_socket_instruction_end(socket);
+
+ guac_protocol_send_blobs(socket, stream, h264_data, h264_length);
+ guac_protocol_send_end(socket, stream);
+ guac_client_free_stream(client, stream);
+
+ guac_client_log(client, GUAC_LOG_TRACE,
+ "H.264 passthrough: %u bytes, %s, %ux%u at (%u,%u)",
+ h264_length, keyframe ? "keyframe" : "delta",
+ cmd->width, cmd->height, cmd->left, cmd->top);
+}
+
+/**
+ * Wrapper for the FreeRDP GDI SurfaceCommand callback. Intercepts AVC420
+ * and AVC444 commands to extract raw H.264 NAL units for passthrough.
+ */
+static UINT guac_rdp_gfx_surface_command(RdpgfxClientContext* context,
+ const RDPGFX_SURFACE_COMMAND* cmd) {
+
+ rdpContext* rdp_context = (rdpContext*) context->custom;
+ guac_client* client = ((rdp_freerdp_context*) rdp_context)->client;
+
+ if (cmd->codecId == RDPGFX_CODECID_AVC420 && cmd->extra != NULL) {
+ RDPGFX_AVC420_BITMAP_STREAM* avc420 =
+ (RDPGFX_AVC420_BITMAP_STREAM*) cmd->extra;
+ if (avc420->data != NULL && avc420->length > 0)
+ guac_rdp_h264_send_passthrough(client, cmd,
+ avc420->data, avc420->length);
+ }
+ else if ((cmd->codecId == RDPGFX_CODECID_AVC444
+ || cmd->codecId == RDPGFX_CODECID_AVC444v2)
+ && cmd->extra != NULL) {
+ RDPGFX_AVC444_BITMAP_STREAM* avc444 =
+ (RDPGFX_AVC444_BITMAP_STREAM*) cmd->extra;
+ if (avc444->bitstream[0].data != NULL
+ && avc444->bitstream[0].length > 0)
+ guac_rdp_h264_send_passthrough(client, cmd,
+ avc444->bitstream[0].data,
+ avc444->bitstream[0].length);
+ }
+
+ if (guac_rdpgfx_orig_surface_command != NULL)
+ return guac_rdpgfx_orig_surface_command(context, cmd);
+
+ return CHANNEL_RC_OK;
+}
+
/**
* Callback which associates handlers specific to Guacamole with the
* RdpgfxClientContext instance allocated by FreeRDP to deal with received
@@ -63,13 +169,22 @@ static void guac_rdp_rdpgfx_channel_connected(rdpContext* context,
RdpgfxClientContext* rdpgfx = (RdpgfxClientContext*) args->pInterface;
rdpGdi* gdi = context->gdi;
- if (!gdi_graphics_pipeline_init(gdi, rdpgfx))
+ if (!gdi_graphics_pipeline_init(gdi, rdpgfx)) {
guac_client_log(client, GUAC_LOG_WARNING, "Rendering backend for RDPGFX "
"channel could not be loaded. Graphics may not render at all!");
- else
+ }
+ else {
guac_client_log(client, GUAC_LOG_DEBUG, "RDPGFX channel will be used for "
"the RDP Graphics Pipeline Extension.");
+ /* Wrap SurfaceCommand to intercept H.264 data before GDI decode */
+ guac_rdpgfx_orig_surface_command = rdpgfx->SurfaceCommand;
+ rdpgfx->SurfaceCommand = guac_rdp_gfx_surface_command;
+
+ guac_client_log(client, GUAC_LOG_INFO,
+ "H.264 passthrough enabled for RDPGFX channel.");
+ }
+
}
/**
diff --git a/src/protocols/rdp/settings.c b/src/protocols/rdp/settings.c
index d97d98e1..bd11f6da 100644
--- a/src/protocols/rdp/settings.c
+++ b/src/protocols/rdp/settings.c
@@ -1619,6 +1619,8 @@ void guac_rdp_push_settings(guac_client* client,
freerdp_settings_set_bool(rdp_settings, FreeRDP_SupportGraphicsPipeline, TRUE);
freerdp_settings_set_bool(rdp_settings, FreeRDP_RemoteFxCodec, TRUE);
+ freerdp_settings_set_bool(rdp_settings, FreeRDP_GfxH264, TRUE);
+ freerdp_settings_set_bool(rdp_settings, FreeRDP_GfxAVC444, TRUE);
if (freerdp_settings_get_uint32(rdp_settings, FreeRDP_ColorDepth) != RDP_GFX_REQUIRED_DEPTH) {
guac_client_log(client, GUAC_LOG_WARNING, "Ignoring requested "
@@ -1878,6 +1880,8 @@ void guac_rdp_push_settings(guac_client* client,
rdp_settings->SupportGraphicsPipeline = TRUE;
rdp_settings->RemoteFxCodec = TRUE;
+ rdp_settings->GfxH264 = TRUE;
+ rdp_settings->GfxAVC444 = TRUE;
if (rdp_settings->ColorDepth != RDP_GFX_REQUIRED_DEPTH) {
guac_client_log(client, GUAC_LOG_WARNING, "Ignoring requested "
+22
View File
@@ -68,6 +68,28 @@ Three new connection parameters:
| `src/protocols/rdp/channels/rdpgfx.c` | Add `#include "config.h"` |
| `src/protocols/rdp/input.c` | Add `#include "config.h"`, add NULL guard in `guac_rdp_user_size_handler()` |
## 004-h264-display-worker.patch
**Feature:** H.264 passthrough via guac_display worker integration. When the RDP server sends AVC420/AVC444 encoded frames (H.264), the raw NAL units are passed through to the browser's WebCodecs VideoDecoder instead of being decoded server-side and re-encoded as JPEG/PNG/WebP.
**Architecture:** The SurfaceCommand callback intercepts H.264 data and stores it on the display layer. During the normal frame flush cycle (`guac_display_plan_apply`), the H.264 data is sent to clients as a custom `h264` instruction before worker threads start encoding. All IMG operations for the H.264 layer are skipped, eliminating the decode→re-encode overhead.
This approach avoids the socket contention issue that occurred when H.264 was sent directly from FreeRDP's SurfaceCommand callback thread, which raced with guac_display's worker threads writing to the same socket.
**Files patched:**
| File | Fix |
|------|-----|
| `src/libguac/display-priv.h` | Add H.264 buffer fields to `guac_display_layer` (data, length, keyframe, rect) |
| `src/libguac/guacamole/display.h` | Add `guac_display_layer_set_h264()` public API |
| `src/libguac/display-layer.c` | Implement `guac_display_layer_set_h264()` with lock management |
| `src/libguac/display-layer-list.c` | Free H.264 data in layer cleanup |
| `src/libguac/display-plan.c` | Send H.264 data during plan apply, skip IMG ops for H.264 layers |
| `src/protocols/rdp/channels/rdpgfx.c` | Wrap SurfaceCommand to store H.264 on display layer after GDI decode |
| `src/protocols/rdp/settings.c` | Enable GfxH264 and GfxAVC444 in FreeRDP settings |
**Requires:** RDP server with H.264 support (xrdp with x264, or Windows with AVC hardware encoder). Browser must support WebCodecs VideoDecoder (Chrome/Edge 94+, Firefox 130+).
## Applying patches
Patches are applied automatically by all build scripts (`build-deb.sh`, `build-rpm.sh`, `install.sh`, `dev.sh`, `Dockerfile`). To apply manually:
+3
View File
@@ -1688,6 +1688,7 @@ pub async fn ab_connect_entry(
enable_gfx: ab_entry.enable_gfx,
enable_desktop_composition: ab_entry.enable_desktop_composition,
force_lossless: ab_entry.force_lossless,
enable_h264: ab_entry.enable_h264,
};
let proxies = trusted.map(|Extension(t)| t.0).unwrap_or_default();
@@ -3060,6 +3061,7 @@ pub async fn quick_connect(
enable_gfx: ab_entry.enable_gfx,
enable_desktop_composition: ab_entry.enable_desktop_composition,
force_lossless: ab_entry.force_lossless,
enable_h264: ab_entry.enable_h264,
};
tracing::info!(
@@ -3151,6 +3153,7 @@ pub async fn quick_connect(
enable_gfx: None,
enable_desktop_composition: None,
force_lossless: None,
enable_h264: None,
};
match manager.create_session(create_req, admin_name).await {
+18 -8
View File
@@ -79,6 +79,8 @@ pub struct RdpParams {
pub enable_desktop_composition: bool,
/// Force lossless encoding (PNG only). Better for text-heavy workloads.
pub force_lossless: bool,
/// Enable H.264 passthrough. Raw H.264 NAL units sent to browser WebCodecs decoder.
pub enable_h264: bool,
}
/// Connection parameters — SSH, VNC, or RDP.
@@ -260,6 +262,7 @@ pub async fn connect_and_handshake(
"kerberos-cache" => p.kerberos_cache.clone().unwrap_or_default(),
"disable-gfx" => if p.enable_gfx { "false" } else { "true" }.into(),
"force-lossless" => if p.force_lossless { "true" } else { "false" }.into(),
"enable-h264" => if p.enable_h264 { "true" } else { "false" }.into(),
"remote-app" => p.remote_app.clone().unwrap_or_default(),
"remote-app-dir" => p.remote_app_dir.clone().unwrap_or_default(),
"remote-app-args" => p.remote_app_args.clone().unwrap_or_default(),
@@ -272,12 +275,12 @@ pub async fn connect_and_handshake(
.collect();
// Send handshake instructions: size, audio, video, image, timezone, connect
let (width, height, dpi) = match params {
ConnectionParams::Ssh(p) => (p.width, p.height, p.dpi),
ConnectionParams::Vnc(p) => (p.width, p.height, p.dpi),
ConnectionParams::Rdp(p) => (p.width, p.height, p.dpi),
let (width, height, dpi, h264) = match &params {
ConnectionParams::Ssh(p) => (p.width, p.height, p.dpi, false),
ConnectionParams::Vnc(p) => (p.width, p.height, p.dpi, false),
ConnectionParams::Rdp(p) => (p.width, p.height, p.dpi, p.enable_h264),
};
send_handshake(&mut stream, width, height, dpi).await?;
send_handshake(&mut stream, width, height, dpi, h264).await?;
let connect = Instruction::new("connect", arg_values);
stream
@@ -367,8 +370,8 @@ pub async fn join_connection(
})
.collect();
// Send handshake instructions
send_handshake(&mut stream, width, height, dpi).await?;
// Send handshake instructions (joining user — h264 inherited from session)
send_handshake(&mut stream, width, height, dpi, false).await?;
let connect = Instruction::new("connect", arg_values);
stream
@@ -432,14 +435,21 @@ async fn send_handshake(
width: u32,
height: u32,
dpi: u32,
enable_h264: bool,
) -> Result<(), GuacdError> {
let video_args = if enable_h264 {
vec!["video/h264".into()]
} else {
vec![]
};
let instructions = [
Instruction::new(
"size",
vec![width.to_string(), height.to_string(), dpi.to_string()],
),
Instruction::new("audio", vec!["audio/L16".into(), "audio/L8".into()]),
Instruction::new("video", vec!["video/h264".into()]),
Instruction::new("video", video_args),
Instruction::new(
"image",
vec!["image/png".into(), "image/jpeg".into(), "image/webp".into()],
+2
View File
@@ -108,6 +108,7 @@ pub async fn cmd_import_guacamole(
enable_gfx: None,
enable_desktop_composition: None,
force_lossless: None,
enable_h264: None,
};
// Build entry name: group prefix + sanitized connection name
@@ -644,6 +645,7 @@ mod tests {
enable_gfx: None,
enable_desktop_composition: None,
force_lossless: None,
enable_h264: None,
};
let mut entries = vec![
("web".into(), entry()),
+3
View File
@@ -101,6 +101,8 @@ pub struct CreateSessionRequest {
pub enable_desktop_composition: Option<bool>,
/// Force lossless encoding (PNG only) for RDP.
pub force_lossless: Option<bool>,
/// Enable H.264 passthrough for RDP.
pub enable_h264: Option<bool>,
}
/// Session status in the lifecycle.
@@ -513,6 +515,7 @@ impl SessionManager {
enable_gfx: req.enable_gfx.unwrap_or(false),
enable_desktop_composition: req.enable_desktop_composition.unwrap_or(false),
force_lossless: req.force_lossless.unwrap_or(false),
enable_h264: req.enable_h264.unwrap_or(false),
}));
(
params,
+28
View File
@@ -147,6 +147,10 @@ pub struct AddressBookEntry {
/// Force lossless encoding (PNG only). Better for text-heavy, low-bandwidth sessions.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub force_lossless: Option<bool>,
/// Enable H.264 passthrough. Passes raw H.264 from xrdp to browser WebCodecs decoder.
/// Requires GFX enabled and xrdp with x264 on the target. Default: true when GFX enabled.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub enable_h264: Option<bool>,
}
impl AddressBookEntry {
@@ -248,6 +252,9 @@ pub struct EntryInfo {
/// Force lossless encoding (PNG only).
#[serde(skip_serializing_if = "Option::is_none")]
pub force_lossless: Option<bool>,
/// Enable H.264 passthrough.
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_h264: Option<bool>,
}
impl From<(&str, &AddressBookEntry)> for EntryInfo {
@@ -295,6 +302,7 @@ impl From<(&str, &AddressBookEntry)> for EntryInfo {
enable_gfx: e.enable_gfx,
enable_desktop_composition: e.enable_desktop_composition,
force_lossless: e.force_lossless,
enable_h264: e.enable_h264,
}
}
}
@@ -1618,4 +1626,24 @@ mod tests {
assert_eq!(resolved.username.as_deref(), Some("alice"));
assert_eq!(resolved.password.as_deref(), Some("literal_pass"));
}
#[test]
fn test_enable_h264_serde_roundtrip() {
// With enable_h264 set
let json = r#"{"type":"rdp","hostname":"test","enable_gfx":true,"enable_h264":true}"#;
let entry: AddressBookEntry = serde_json::from_str(json).unwrap();
assert_eq!(entry.enable_h264, Some(true));
let out = serde_json::to_string(&entry).unwrap();
assert!(out.contains("\"enable_h264\":true"));
// Without enable_h264 (defaults to None)
let json2 = r#"{"type":"rdp","hostname":"test","enable_gfx":true}"#;
let entry2: AddressBookEntry = serde_json::from_str(json2).unwrap();
assert_eq!(entry2.enable_h264, None);
// Explicit false
let json3 = r#"{"type":"rdp","hostname":"test","enable_h264":false}"#;
let entry3: AddressBookEntry = serde_json::from_str(json3).unwrap();
assert_eq!(entry3.enable_h264, Some(false));
}
}
+12 -5
View File
@@ -548,18 +548,22 @@
<span id="em-video-perf-arrow">&#9654;</span> Video Performance
</label>
<div id="em-video-perf-fields" style="display:none">
<div class="field-hint" style="margin-bottom:0.5em">RDP graphics pipeline and encoding settings.</div>
<div class="field-hint" style="margin-bottom:0.5em">RDP graphics pipeline and encoding settings. For video-heavy workloads, enable GFX + Desktop Composition + H.264.</div>
<label style="margin-top:0.5em">
<input type="checkbox" id="em-enable-gfx" style="display:inline;width:auto;margin-right:0.4em"> Enable Graphics Pipeline (GFX)
<div style="color:#888;font-size:0.75em;margin-top:0.2em;">Enables RemoteFX codec for better video compression. Recommended for video monitoring.</div>
<div style="color:#888;font-size:0.75em;margin-top:0.2em;">Required for H.264 and RemoteFX. Enables modern RDP graphics codecs (32-bit colour).</div>
</label>
<label style="margin-top:0.3em">
<input type="checkbox" id="em-enable-desktop-comp" style="display:inline;width:auto;margin-right:0.4em"> Enable Desktop Composition
<div style="color:#888;font-size:0.75em;margin-top:0.2em;">Enables DWM compositing. Improves video overlay rendering.</div>
<div style="color:#888;font-size:0.75em;margin-top:0.2em;">Enables DWM compositing for smoother video overlays and transparency.</div>
</label>
<label style="margin-top:0.3em">
<input type="checkbox" id="em-force-lossless" style="display:inline;width:auto;margin-right:0.4em"> Force Lossless
<div style="color:#888;font-size:0.75em;margin-top:0.2em;">Forces PNG encoding. Better for text-heavy work, uses more bandwidth.</div>
<div style="color:#888;font-size:0.75em;margin-top:0.2em;">PNG only — crisp text but high bandwidth. Not compatible with H.264 passthrough.</div>
</label>
<label style="margin-top:0.3em">
<input type="checkbox" id="em-enable-h264" style="display:inline;width:auto;margin-right:0.4em"> H.264 Passthrough
<div style="color:#888;font-size:0.75em;margin-top:0.2em;">Passes H.264 video direct to browser (low latency, hardware decoded). Requires GFX enabled and xrdp with x264 on the target server.</div>
</label>
</div>
</div>
@@ -1709,6 +1713,7 @@
document.getElementById('em-enable-gfx').checked = false;
document.getElementById('em-enable-desktop-comp').checked = false;
document.getElementById('em-force-lossless').checked = false;
document.getElementById('em-enable-h264').checked = false;
document.getElementById('em-video-perf-fields').style.display = 'none';
document.getElementById('em-video-perf-arrow').innerHTML = '&#9654;';
document.getElementById('em-disable-copy').checked = false;
@@ -1860,7 +1865,8 @@
document.getElementById('em-enable-gfx').checked = !!entryData.enable_gfx;
document.getElementById('em-enable-desktop-comp').checked = !!entryData.enable_desktop_comp;
document.getElementById('em-force-lossless').checked = !!entryData.force_lossless;
if (entryData.enable_gfx || entryData.enable_desktop_comp || entryData.force_lossless) {
document.getElementById('em-enable-h264').checked = !!entryData.enable_h264;
if (entryData.enable_gfx || entryData.enable_desktop_comp || entryData.force_lossless || entryData.enable_h264) {
document.getElementById('em-video-perf-fields').style.display = '';
document.getElementById('em-video-perf-arrow').innerHTML = '&#9660;';
}
@@ -1993,6 +1999,7 @@
if (document.getElementById('em-enable-gfx').checked) entry.enable_gfx = true;
if (document.getElementById('em-enable-desktop-comp').checked) entry.enable_desktop_comp = true;
if (document.getElementById('em-force-lossless').checked) entry.force_lossless = true;
if (document.getElementById('em-enable-h264').checked) entry.enable_h264 = true;
}
// Clipboard settings (all types)
+2 -2
View File
@@ -116,8 +116,8 @@ Guacamole.H264Decoder = function H264Decoder(display) {
}
});
// Configure for H.264 Constrained Baseline (most compatible)
// The actual profile/level will be negotiated by the RDP server
// 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