siva created GUACAMOLE-2292:
-------------------------------

             Summary: Feature Request: Instant Seek for Session Recording 
Playback
                 Key: GUACAMOLE-2292
                 URL: https://issues.apache.org/jira/browse/GUACAMOLE-2292
             Project: Guacamole
          Issue Type: New Feature
          Components: guacamole-client, guacamole-server
            Reporter: siva


h2. Summary

Add indexing and display snapshot support to Guacamole session recordings, 
enabling O(log N) seek-to-position without sequential frame replay. All 
metadata is embedded inline in the recording file itself — no sidecar files, no 
post-processing.
h2. Problem Statement

The current Guacamole session recording player has no seek optimization. To 
jump to a position T in a recording, the player must:
 # Load the recording file from the beginning
 # Parse and replay every protocol instruction sequentially up to position T
 # Render all intermediate display state changes

For a 1-hour recording, seeking to the 45-minute mark requires parsing and 
rendering ~75% of the file. This results in:
 * *Multi-second (or minute) seek times* for long recordings
 * *Poor user experience* — UI freezes during sequential replay
 * *No random access* — impossible to jump to an arbitrary point without 
sequential scan from the beginning
 * *CPU-intensive* — every frame between 0 and T must be fully rendered even 
though only the final state matters

h2. Background: Guacamole Recording Format

A Guacamole recording is a stream of length-prefixed protocol instructions:

 

{{4.size,1.0,4.1920,4.1080;
3.img,1.0,2.12,2.-1,1.0,1.0,9.image/png;
4.blob,2.12,8.iVBOR...;
3.end,2.12;
4.sync,13.1719344000000;}}
 * Instructions are delimited by {{;}}
 * Each element is {{<length>.<content>}}
 * {{sync}} instructions mark frame boundaries (with Unix timestamp in ms)
 * The protocol has no built-in index, seek table, or keyframe concept
 * Unknown opcodes are ignored by standard clients (forward-compatible)

h2. Proposed Solution
h3. Approach: Inline Index with Reverse-Linked Back-Pointers + Inline Snapshots

Emit two custom instruction types directly into the recording stream during 
capture:
 # *{{_snapshot}}* — Periodic full-display PNG capture (every ~10s)
 # *{{_index}}* — Per-frame metadata entry with a back-pointer to the previous 
index entry

This creates a reverse-linked-list of index entries embedded in the file, 
enabling O(log N) binary search using HTTP Range requests. No separate files. 
No post-session processing.
h3. Custom Protocol Instructions
h4. {{_snapshot}} — Display Capture

 

{{9._snapshot,<length>.<base64-encoded PNG data>;}}

A full-screen PNG rendered by a server-side Cairo compositor that tracks 
display state from the protocol stream. Emitted every ~10 seconds at a {{sync}} 
boundary, *before* the {{sync}} instruction so its byte offset is known when 
writing the subsequent {{{}_index{}}}.
h4. {{_index}} — Frame Index Entry

 

{{6._index,<timestamp>,<frame_start>,<frame_end>,<snap_offset>,<snap_length>,<seq>,<prev_index_offset>;}}
||Field||Type||Description||
|{{timestamp}}|int64|Unix timestamp in ms (from the {{sync}} instruction)|
|{{frame_start}}|uint64|Byte offset where this frame's protocol data begins|
|{{frame_end}}|uint64|Byte offset where this frame's protocol data ends|
|{{snap_offset}}|uint64|Byte offset of the nearest preceding {{_snapshot}} (0 
if none yet)|
|{{snap_length}}|uint64|Byte length of that {{{}_snapshot{}}}'s PNG data (0 if 
none yet)|
|{{seq}}|uint32|Monotonically increasing sequence number (0, 1, 2, ...)|
|{{prev_index_offset}}|uint64|Byte offset of the previous {{_index}} 
instruction (0 for the first entry)|

Emitted immediately after every {{sync}} instruction.
h3. Recording File Structure

 

{{[frame 0 protocol instructions...]
4.sync,13.1719344000000;
6._index,13.1719344000000,1.0,4.4523,1.0,1.0,1.0,1.0;

[frame 1 protocol instructions...]
4.sync,13.1719344000500;
6._index,13.1719344000500,4.4523,5.12400,1.0,1.0,1.1,4.4524;

[frame 2 protocol instructions...]
9._snapshot,65536.<base64 PNG data>;
4.sync,13.1719344010000;
6._index,13.1719344010000,5.12400,5.82000,5.12401,5.65540,1.2,5.12401;

[frame 3 protocol instructions...]
4.sync,13.1719344010500;
6._index,13.1719344010500,5.82000,5.95000,5.12401,5.65540,1.3,5.82001;

...continues for entire session...}}

Key properties:
 * Standard protocol instructions are unmodified
 * Custom instructions ({{{}_snapshot{}}}, {{{}_index{}}}) are interleaved
 * Back-pointers form a traversable chain from tail to head
 * Everything is written in a single forward pass — append only

h2. Server-Side Implementation
h3. Recording Pipeline

 

{{Protocol Stream → Recording Socket Wrapper → File
                                           ↓
                              Cairo Compositor (tracks display state)
                                           ↓
                              Snapshot PNG (every ~10s)}}
 # *Intercept writes:* Socket wrapper observes all protocol bytes flowing to 
the recording file
 # *Parse instructions:* Lightweight state machine identifies opcodes and 
arguments (no full parser needed)
 # *Feed compositor:* Every instruction is forwarded to a Cairo-based display 
compositor that maintains a pixel-accurate framebuffer
 # *On {{sync}} boundary (every ~10s):* Render compositor surface to PNG, emit 
{{_snapshot}} instruction to file
 # *On every {{{}sync{}}}:* Emit {{_index}} instruction with frame boundaries, 
snapshot reference, sequence number, and back-pointer
 # *On session end:* Close file handle. Done.

h3. Zero Post-Processing

All index and snapshot data is written inline during the session. When the 
session ends, the server simply closes the file descriptor. No trailer writing, 
no index compilation, no file merging — the recording is immediately ready for 
playback with seek support.
h3. Cairo Compositor

The server maintains a lightweight display compositor that processes:
 * {{size}} — Resize layer 0 surface
 * {{img}} / {{blob}} / {{end}} — Decode PNG image streams and composite onto 
surface
 * {{copy}} — Region copy within the display
 * {{cfill}} — Color fill operations
 * {{rect}} / {{transfer}} — Additional drawing primitives

This is a subset of the full Guacamole rendering instruction set — enough to 
produce accurate display snapshots.
h2. Client-Side Implementation
h3. Local File Seek (Current Player Architecture)

The existing player operates on local files. With the inline index, the local 
player can:
 # *On file open:* Scan backward from EOF (~last 2KB) to find the last 
{{_index}} entry
 # *Walk the back-pointer chain:* Follow {{prev_index_offset}} links to build a 
complete frame table in memory — O(N) sequential reads but only touching 
{{_index}} entries, skipping all protocol/snapshot data
 # *On seek:* Look up the target frame in the in-memory table, {{fseek()}} to 
the snapshot offset, decode the PNG, then {{fseek()}} to the gap frames and 
replay ~10s of protocol data

This is already a massive improvement over the current sequential replay: 
instead of rendering every frame from 0 to T, the player reads one snapshot + 
~10 seconds of protocol data.


h3. Complexity (Local File Playback)
||Operation||Cost||Notes||
|Build frame table (one-time)|O(N) fseek + read|Only reads {{_index}} entries, 
skips bulk data|
|Seek to any position|2 fseek + read|1 snapshot + gap frames|
|Gap replay|O(frames in 10s window)|At most ~10s of protocol data|

Compare to current: seek to 45min in a 1hr recording = parse and render every 
frame from the beginning (potentially minutes of CPU time).
h3. Playback (Non-Seek)

Normal frame-by-frame playback is unaffected. The player processes instructions 
sequentially and simply ignores {{_snapshot}} and {{_index}} opcodes (unknown 
opcode handling).
h2. Backward Compatibility
||Client||Behavior||
|Standard guacamole-common-js|Plays normally; skips unknown 
{{{}_snapshot{}}}/{{{}_index{}}} opcodes|
|Enhanced player (this feature)|Uses inline index for instant seek|
|guacd without this feature|Produces recordings without 
{{{}_snapshot{}}}/{{{}_index{}}} — player falls back to sequential playback|

No protocol specification changes required. Custom instructions use {{_}} 
prefix to signal non-standard extensions.
h2. Configuration
||Parameter||Default||Description||
|{{recording-index-enabled}}|{{true}}|Emit {{_index}} instructions in recording|
|{{recording-snapshots-enabled}}|{{true}}|Emit {{_snapshot}} instructions in 
recording|
|{{recording-snapshot-interval}}|{{10000}}|Milliseconds between snapshots|
|{{recording-snapshot-max-width}}|{{0}} (no limit)|Cap snapshot resolution to 
reduce file size|
h2. File Size Impact

For a typical 1-hour, 1920×1080 desktop recording:
||Component||Size||% of typical recording||
|{{_index}} entries (~7200 frames)|~600 KB|< 0.1%|
|{{_snapshot}} entries (~360 @ ~80KB avg)|~28 MB|~3-5%|
|*Total overhead*|*~29 MB*|*~3-5%*|

Snapshots dominate the overhead. The snapshot interval and resolution cap are 
tunable to trade seek granularity for file size.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to