wraith/docs/spikes/rdp-frame-transport-results.md
Vantz Stockwell d42f000f8f spike: multi-window and RDP frame transport research results
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 06:35:00 -04:00

5.3 KiB

Spike: RDP Frame Transport Mechanisms

Status: Research-based (not yet benchmarked) Date: 2026-03-17 Target platform: Windows (developing on macOS) Wails version: v3.0.0-alpha.74


Context

When Wraith connects to a remote desktop via FreeRDP, the Go backend receives raw bitmap frames that must be delivered to the frontend for rendering on an HTML <canvas>. This spike evaluates three transport approaches, estimating throughput for a 1920x1080 session at 30 fps.


Approach 1: Local HTTP Endpoint

How it works

  1. Go spins up a local HTTP server on a random high port (net.Listen("tcp", "127.0.0.1:0")).
  2. Each frame is JPEG-encoded and served at a predictable URL (e.g., http://127.0.0.1:{port}/frame?session=3).
  3. The frontend fetches frames via fetch(), <img> tag, or ReadableStream for chunked delivery.

Throughput estimate

Metric Value
1080p RGBA raw ~8 MB/frame
1080p JPEG (quality 80) ~100-200 KB/frame
At 30 fps (JPEG) ~3-6 MB/s
Loopback bandwidth >1 GB/s

Loopback HTTP can handle this with headroom to spare.

Pros

  • No base64 overhead — binary JPEG bytes transfer directly.
  • Standard HTTP semantics; easy to debug with browser DevTools.
  • Can use Transfer-Encoding: chunked or Server-Sent Events for push-based delivery.
  • Can serve multiple sessions on the same server with different paths.

Cons

  • Requires an extra listening port on localhost.
  • Potential firewall or endpoint-security issues on locked-down Windows enterprise machines.
  • Slightly more complex setup (port allocation, CORS headers for Wails webview origin).

Approach 2: Wails Bindings (Base64)

How it works

  1. Go encodes each frame as a JPEG, then base64-encodes the result.
  2. A Wails-bound method (SessionService.GetFrame(sessionID)) returns the base64 string.
  3. The frontend decodes the string, creates an ImageBitmap or sets it as a data URI, and draws it on a <canvas>.

Throughput estimate

Metric Value
1080p JPEG (quality 80) ~100-200 KB/frame
Base64 of JPEG (+33%) ~133-270 KB/frame
At 30 fps ~4-8 MB/s of string data
Wails IPC overhead Negligible for this payload size

This is feasible. Modern JavaScript engines handle base64 decoding at several hundred MB/s.

Pros

  • No extra ports — everything flows through the existing Wails IPC channel.
  • Works out of the box with Wails bindings; no additional infrastructure.
  • No firewall concerns.

Cons

  • 33% base64 size overhead on every frame.
  • CPU cost of base64.StdEncoding.EncodeToString() in Go and atob() in JS on every frame (though both are fast).
  • Polling-based unless combined with Wails events to signal frame availability.
  • May bottleneck at very high resolutions (4K) or high FPS (60+).

Approach 3: Wails Events (Streaming)

How it works

  1. Go emits each frame as a Wails event: app.EmitEvent("frame:3", base64JpegString).
  2. The frontend subscribes: wails.Events.On("frame:3", handler).
  3. The handler decodes and renders on canvas.

Throughput estimate

Same as Approach 2 — the payload is identical (base64 JPEG). The difference is delivery mechanism (push vs. pull).

Pros

  • Push-based — the frontend receives frames as soon as they are available with no polling delay.
  • Natural Wails pattern; aligns with how other real-time data (connection status, notifications) already flows.

Cons

  • Same 33% base64 overhead as Approach 2.
  • Wails event bus may not be optimised for high-frequency, large-payload events. This is unvalidated.
  • Harder to apply backpressure — if the frontend cannot keep up, events queue without flow control.

Throughput Summary

Approach Payload/frame 30 fps throughput Extra infra
1 — Local HTTP ~150 KB (binary JPEG) ~4.5 MB/s Localhost port
2 — Wails bindings ~200 KB (base64 JPEG) ~6 MB/s None
3 — Wails events ~200 KB (base64 JPEG) ~6 MB/s None

All three approaches are within comfortable limits for 1080p at 30 fps. The differentiator is operational simplicity, not raw throughput.


Recommendation

Start with Approach 2 (base64 JPEG via Wails bindings).

Rationale:

  1. JPEG compression brings 1080p frames down to ~200 KB, making the 33% base64 overhead manageable (~6 MB/s at 30 fps).
  2. No extra ports or firewall concerns — important for enterprise Windows environments where Wraith will be deployed.
  3. Simple implementation: one Go method, one frontend call per frame.
  4. If polling latency is a problem, upgrade to Approach 3 (events) with minimal code change — the payload encoding is identical.

If benchmarking reveals issues (dropped frames, high CPU from encoding), fall back to Approach 1 (local HTTP) which eliminates base64 overhead entirely. The migration path is straightforward: replace the fetch(dataUri) call with fetch(httpUrl).


Next Step

Benchmark during Phase 3 when FreeRDP integration is in progress and real frame data is available. Key metrics to capture:

  • End-to-end frame latency (Go encode to canvas paint)
  • CPU utilisation on both Go and browser sides
  • Frame drop rate at 30 fps and 60 fps
  • Memory pressure from base64 string allocation/GC