mirror of
https://github.com/sol1/rustguac.git
synced 2026-09-10 01:26:06 +00:00
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:
Generated
+1
-1
@@ -2911,7 +2911,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustguac"
|
||||
version = "0.9.2"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"axum",
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -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."
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -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 "
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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 ¶ms {
|
||||
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()],
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -548,18 +548,22 @@
|
||||
<span id="em-video-perf-arrow">▶</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 = '▶';
|
||||
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 = '▼';
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user