diff --git a/kvmd/apps/media/server.py b/kvmd/apps/media/server.py index 1f96b353..347b0f3e 100644 --- a/kvmd/apps/media/server.py +++ b/kvmd/apps/media/server.py @@ -52,6 +52,9 @@ class _Source: clients: dict[WsSession, "_Client"] = dataclasses.field(default_factory=dict) key_required: bool = dataclasses.field(default=False) + def is_diff(self) -> bool: + return StreamerFormats.is_diff(self.streamer.get_format()) + @dataclasses.dataclass class _Client: @@ -98,6 +101,14 @@ class MediaServer(HttpServer): async def __ws_bin_ping_handler(self, ws: WsSession, _: bytes) -> None: await ws.send_bin(255, b"") # Ping-pong + @exposed_ws(1) + async def __ws_bin_key_handler(self, ws: WsSession, _: bytes) -> None: + for src in self.__srcs: + if ws in src.clients: + if src.is_diff(): + src.key_required = True + break + @exposed_ws("start") async def __ws_start_handler(self, ws: WsSession, event: dict) -> None: try: @@ -145,7 +156,7 @@ class MediaServer(HttpServer): # ===== async def __sender(self, client: _Client) -> None: - need_key = StreamerFormats.is_diff(client.src.streamer.get_format()) + need_key = client.src.is_diff() if need_key: client.src.key_required = True has_key = False diff --git a/web/share/js/kvm/stream_media.js b/web/share/js/kvm/stream_media.js index 0e81c7d9..6f73993c 100644 --- a/web/share/js/kvm/stream_media.js +++ b/web/share/js/kvm/stream_media.js @@ -37,6 +37,7 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo, __orient) { var __ws = null; var __ping_timer = null; var __missed_heartbeats = 0; + var __decoder = null; var __codec = ""; var __canvas = $("stream-canvas"); @@ -86,11 +87,15 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo, __orient) { __ws.onerror = __wsErrorHandler; __ws.onclose = __wsCloseHandler; __ws.onmessage = async (event) => { - if (typeof event.data === "string") { - event = JSON.parse(event.data); - __wsJsonHandler(event.event_type, event.event); - } else { // Binary - await __wsBinHandler(event.data); + try { + if (typeof event.data === "string") { + event = JSON.parse(event.data); + __wsJsonHandler(event.event_type, event.event); + } else { // Binary + await __wsBinHandler(event.data); + } + } catch (ex) { + __wsErrorHandler(ex); } }; } @@ -142,10 +147,7 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo, __orient) { clearInterval(__ping_timer); __ping_timer = null; } - if (__decoder) { - __decoder.close(); - __decoder = null; - } + __closeDecoder(); __missed_heartbeats = 0; __fps_accum = 0; __ws = null; @@ -156,38 +158,12 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo, __orient) { var __wsJsonHandler = function(event_type, event) { if (event_type === "media") { - __decoderCreate(event.video); + __setupCodec(event.video); } }; - var __wsBinHandler = async (data) => { - let header = new Uint8Array(data.slice(0, 2)); - - if (header[0] === 255) { // Pong - __missed_heartbeats = 0; - - } else if (header[0] === 1 && __decoder !== null) { // Video frame - let key = !!header[1]; - if (__decoder.state !== "configured") { - if (!key) { - return; - } - await __decoder.configure({"codec": __codec, "optimizeForLatency": true}); - __setActive(); - } - - let chunk = new EncodedVideoChunk({ // eslint-disable-line no-undef - "timestamp": (performance.now() + performance.timeOrigin) * 1000, - "type": (key ? "key" : "delta"), - "data": data.slice(2), - }); - await __decoder.decode(chunk); - } - }; - - var __decoderCreate = function(formats) { - __decoderDestroy(); - + var __setupCodec = function(formats) { + __closeDecoder(); if (formats.h264 === undefined) { let msg = "No H.264 stream available on PiKVM"; __setInfo(false, false, msg); @@ -203,19 +179,76 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo, __orient) { __logInfo(msg); return; } - - __decoder = new VideoDecoder({ // eslint-disable-line no-undef - "output": __drawFrame, - "error": (err) => __logInfo(err.message), - }); __codec = `avc1.${formats.h264.profile_level_id}`; - __ws.send(JSON.stringify({ "event_type": "start", "event": {"type": "video", "format": "h264"}, })); }; + var __wsBinHandler = async (data) => { + let header = new Uint8Array(data.slice(0, 2)); + if (header[0] === 255) { // Pong + __missed_heartbeats = 0; + } else if (header[0] === 1) { // Video frame + let key = !!header[1]; + if (await __ensureDecoder(key)) { + await __processFrame(key, data.slice(2)); + } + } + }; + + var __ensureDecoder = async (key) => { + if (__codec === "") { + return false; + } + if (__decoder === null || __decoder.state === "closed") { + let started = (__codec !== ""); + let codec = __codec; + __closeDecoder(); + __codec = codec; + __decoder = new VideoDecoder({ // eslint-disable-line no-undef + "output": __drawFrame, + "error": (err) => __logInfo(err.message), + }); + if (started) { + __ws.send(new Uint8Array([0])); + } + } + if (__decoder.state !== "configured") { + if (!key) { + return false; + } + await __decoder.configure({"codec": __codec, "optimizeForLatency": true}); + } + if (__decoder.state === "configured") { + __setActive(); + return true; + } + return false; + }; + + var __processFrame = async (key, raw) => { + let chunk = new EncodedVideoChunk({ // eslint-disable-line no-undef + "timestamp": (performance.now() + performance.timeOrigin) * 1000, + "type": (key ? "key" : "delta"), + "data": raw, + }); + await __decoder.decode(chunk); + }; + + var __closeDecoder = function() { + if (__decoder !== null) { + try { + __decoder.close(); + } catch { // eslint-disable-line no-empty + } finally { + __decoder = null; + __codec = ""; + } + } + }; + var __drawFrame = function(frame) { try { let width = frame.displayWidth; @@ -254,14 +287,6 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo, __orient) { } }; - var __decoderDestroy = function() { - if (__decoder !== null) { - __decoder.close(); - __decoder = null; - __codec = ""; - } - }; - var __logInfo = (...args) => tools.info("Stream [Media]:", ...args); }