4308a2f112
Builds on the M6a-g MVP (d07e98e) with five user-facing features that
take the in-browser remote-control client from "demo toy" to "workable
for daily use", plus user-facing documentation.
M6h — Text clipboard sync (both directions)
- Host → browser via Clipboard{format: Text} → navigator.clipboard.writeText.
- Browser → host via Cmd-V/Ctrl-V keydown intercept → navigator.clipboard
.readText → Message{clipboard}, sent BEFORE the V keystroke so the
host's paste hotkey lands on the freshly-synced text.
- Handles both single-format Clipboard (older peers) and MultiClipboards
wrapper (peers ≥ 1.3.0; gated by clipboard.rs:is_support_multi_clipboard).
- Switched printable-with-modifier hotkeys (Ctrl-C etc.) from Unicode to
Chr payload — host's process_unicode does key_sequence(char) which
ignores modifiers, breaking copy/cut; process_chr respects them.
- Firefox refuses navigator.clipboard.readText() by default — accepted as
a known browser limitation, host → browser direction works regardless.
M6i — Multi-monitor switching
- HUD picker shown when peer_info.displays > 1.
- On change: SwitchDisplay + CaptureDisplays{set:[idx]} two-message dance
— required for clients ≥ 1.2.4 (we send "1.4.0"). Without the follow-
up, switch_display_to leaves both video services subscribed and
switching display 0 → 1 → 0 doesn't restore display 0.
- Mouse coords offset by the active display's virtual-desktop origin
(DisplayInfo.x, .y). Without this, clicks on display 2 landed on
display 1 because both share canvas (0,0) but only display 1 has
origin (0,0) in virtual-desktop space.
M6j — Quality / FPS / mute controls
- Image quality preset (Low/Balanced/Best) → Misc{option: {image_quality}}.
- Custom FPS (15/30/60) → Misc{option: {custom_fps}}; host caps at 30
unless allow_more_fps is advertised.
- Mute toggle additionally sends Misc{option: {disable_audio: Yes/No}}
so the peer stops encoding audio while muted (saves CPU + bandwidth).
M6k — Auto-reconnect on transient drops
- session.recv() throw → reconnect with exponential backoff: 1s, 2s, 4s,
8s, 16s, 30s, 30s, capped at 30s, max 10 attempts.
- Dim overlay sits on top of the canvas during retry; canvas keeps
last-known frame for visual continuity.
- Auth errors (password/signature) bail immediately — no point retrying.
- User options (mute, image_quality, custom_fps, current display)
re-applied to host on each successful reconnect, since host treats
every session as fresh and resets to defaults.
- Architecture: `session` is a let-binding mutated on reconnect; HUD
button closures read it at click-time so they automatically retarget.
Input modules (mouse/keyboard/clipboard) get a Proxy that forwards
method calls to whatever session is current — avoids re-binding
window/canvas listeners on each reconnect.
M6l — H.264 video decode (Annex-B + SPS-derived codec string)
- decode/bitstream.ts: iterate Annex-B NAL units, derive avc1.PPCCLL
from the keyframe's inline SPS (host's hwcodec defaults to high
profile; a hardcoded baseline string would make WebCodecs refuse the
stream).
- Defer H.264 decoder configure until first keyframe arrives.
- VP9 codec string corrected from level 1.0 (vp09.00.10.08) to level
5.0 (vp09.00.50.08) — wrong level was probably forcing software
decode in some browsers.
- Default prefer flipped to VP8 (cheapest software encoder; H.264 path
stays implemented for hosts with hwcodec/nvenc).
M6m — docs/CONFIGURATION.md "Web client" section: routes, browser
matrix, network requirements (relay reachability + reverse-proxy WS
upgrade), feature status table, codec selection rationale, the
recv/dec/draw HUD diagnostic, build commands.
Bundle: 535 KB / ~75 KB gzipped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
183 lines
3.3 KiB
CSS
183 lines
3.3 KiB
CSS
/* RustDesk web client — minimal, dark theme to match the admin dashboard. */
|
|
|
|
html, body {
|
|
margin: 0;
|
|
padding: 0;
|
|
height: 100%;
|
|
background: #0f172a;
|
|
color: #e2e8f0;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
}
|
|
|
|
#root {
|
|
height: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.placeholder {
|
|
background: #1e293b;
|
|
border: 1px solid #334155;
|
|
border-radius: 8px;
|
|
padding: 32px 40px;
|
|
max-width: 540px;
|
|
text-align: center;
|
|
}
|
|
|
|
.placeholder h1 {
|
|
margin: 0 0 16px;
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.placeholder p {
|
|
margin: 8px 0;
|
|
font-size: 14px;
|
|
color: #cbd5e1;
|
|
}
|
|
|
|
.placeholder code {
|
|
background: #0f172a;
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
font-size: 12px;
|
|
color: #7dd3fc;
|
|
}
|
|
|
|
.muted { color: #64748b !important; font-size: 12px !important; }
|
|
|
|
.pw-form {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-top: 16px;
|
|
align-items: stretch;
|
|
}
|
|
.pw-form input[type="password"] {
|
|
flex: 1;
|
|
background: #0f172a;
|
|
border: 1px solid #334155;
|
|
color: #e2e8f0;
|
|
padding: 8px 10px;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
font-family: inherit;
|
|
}
|
|
.pw-form input[type="password"]:focus {
|
|
outline: none;
|
|
border-color: #38bdf8;
|
|
}
|
|
.pw-form button {
|
|
background: #0284c7;
|
|
border: 0;
|
|
color: #f0f9ff;
|
|
padding: 8px 16px;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
}
|
|
.pw-form button:hover { background: #0369a1; }
|
|
|
|
.error-inline {
|
|
background: rgba(220, 38, 38, 0.15);
|
|
border: 1px solid rgba(220, 38, 38, 0.4);
|
|
color: #fca5a5;
|
|
padding: 8px 10px;
|
|
border-radius: 4px;
|
|
font-size: 13px;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
/* ------- Live session ------- */
|
|
|
|
.session {
|
|
position: fixed;
|
|
inset: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: #000;
|
|
}
|
|
|
|
.rd-canvas {
|
|
flex: 1;
|
|
width: 100%;
|
|
height: 100%;
|
|
/* Letterbox: keep aspect ratio while fitting the browser viewport. */
|
|
object-fit: contain;
|
|
display: block;
|
|
background: #000;
|
|
}
|
|
|
|
.hud {
|
|
position: fixed;
|
|
top: 8px;
|
|
left: 8px;
|
|
background: rgba(0, 0, 0, 0.55);
|
|
color: #cbd5e1;
|
|
font-size: 11px;
|
|
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
z-index: 10;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.hud-fps {
|
|
pointer-events: none;
|
|
}
|
|
|
|
.hud-btn {
|
|
background: #334155;
|
|
border: 0;
|
|
color: #e2e8f0;
|
|
padding: 3px 8px;
|
|
border-radius: 3px;
|
|
font-size: 11px;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
}
|
|
.hud-btn:hover { background: #475569; }
|
|
|
|
.hud-select {
|
|
background: #334155;
|
|
border: 0;
|
|
color: #e2e8f0;
|
|
padding: 2px 4px;
|
|
border-radius: 3px;
|
|
font-size: 11px;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
}
|
|
.hud-select:hover { background: #475569; }
|
|
|
|
.reconnect-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(15, 23, 42, 0.65);
|
|
backdrop-filter: blur(2px);
|
|
-webkit-backdrop-filter: blur(2px);
|
|
color: #e2e8f0;
|
|
font-family: inherit;
|
|
font-size: 15px;
|
|
z-index: 20;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.error {
|
|
background: rgba(220, 38, 38, 0.15);
|
|
border: 1px solid rgba(220, 38, 38, 0.4);
|
|
border-radius: 8px;
|
|
padding: 24px 32px;
|
|
color: #fca5a5;
|
|
max-width: 640px;
|
|
}
|
|
.error h1 { margin: 0 0 12px; font-size: 18px; }
|
|
.error pre { white-space: pre-wrap; font-size: 13px; }
|