From ce1c320e94f48884d359914feb2fbee3b869a8c2 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Fri, 2 Oct 2026 16:08:45 +0300 Subject: [PATCH 1/2] Give a view-only share guest a real player, native WebRTC first, and name the camera in the bar A view-only link got the camera's MJPEG in a bare : no sound, no snapshot, no full screen, and the heaviest way to move a 4K picture. It now walks the same chain as the camera's own WebUI player: 1. WebRTC: signalling over /ws/webrtc through the tunnel, media on its own peer connection straight to the camera with the share's ICE servers -- sub-second latency, the hardware decoder, and nothing heavy on the tunnel's data channel. 2. MSE over /ws/video through the tunnel, when WebRTC will not: a codec this browser's WebRTC does not offer, a camera whose media slots are taken ("busy"), a path the media cannot cross. 3. The camera's MJPEG, with the reason shown. With sound on request (renegotiated on whichever rung is playing), a snapshot that downloads, and full screen. The camera names itself in WELCOME now; the bar and the tab title show it, so a guest with links to two cameras can tell them apart. --- service/internal/sharerelay/static/index.html | 11 +- service/internal/sharerelay/static/player.js | 423 ++++++++++++++++++ service/internal/sharerelay/static/shell.js | 16 +- 3 files changed, 445 insertions(+), 5 deletions(-) create mode 100644 service/internal/sharerelay/static/player.js diff --git a/service/internal/sharerelay/static/index.html b/service/internal/sharerelay/static/index.html index 5d22004c..7ad0eff7 100644 --- a/service/internal/sharerelay/static/index.html +++ b/service/internal/sharerelay/static/index.html @@ -21,7 +21,14 @@ main { flex: 1; position: relative; min-height: 0; } iframe { border: 0; width: 100%; height: 100%; display: block; background: #fff; } .player { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: #000; } - .player img { max-width: 100%; max-height: 100%; } + .player { position: relative; } + .player img, .player video { max-width: 100%; max-height: 100%; } + .player video { width: 100%; height: 100%; object-fit: contain; } + .player-note { position: absolute; left: 50%; top: 16px; transform: translateX(-50%); max-width: min(560px, calc(100% - 32px)); padding: 8px 12px; border-radius: 8px; background: rgba(0,0,0,.7); color: #fff; text-align: center; } + .player-controls { position: absolute; right: 12px; bottom: 12px; display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; } + .player-controls button { font: inherit; min-height: 36px; padding: 6px 12px; border: 1px solid rgba(255,255,255,.35); border-radius: 6px; background: rgba(0,0,0,.55); color: #fff; cursor: pointer; } + .player-controls button:disabled { opacity: .6; cursor: default; } + [hidden] { display: none !important; } .notice { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; padding: 24px; text-align: center; } .notice h1 { font-size: 20px; margin: 0; font-weight: 600; } .notice p { margin: 0; color: var(--muted); max-width: 420px; } @@ -38,7 +45,7 @@