172 lines
5.3 KiB
Markdown
172 lines
5.3 KiB
Markdown
# 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
|