diff options
Diffstat (limited to 'web/share/js/kvm/stream_media.js')
-rw-r--r-- | web/share/js/kvm/stream_media.js | 240 |
1 files changed, 240 insertions, 0 deletions
diff --git a/web/share/js/kvm/stream_media.js b/web/share/js/kvm/stream_media.js new file mode 100644 index 00000000..8c84e8de --- /dev/null +++ b/web/share/js/kvm/stream_media.js @@ -0,0 +1,240 @@ +/***************************************************************************** +# # +# KVMD - The main PiKVM daemon. # +# # +# Copyright (C) 2018-2024 Maxim Devaev <[email protected]> # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see <https://www.gnu.org/licenses/>. # +# # +*****************************************************************************/ + + +"use strict"; + + +import {tools, $} from "../tools.js"; + + +export function MediaStreamer(__setActive, __setInactive, __setInfo) { + var self = this; + + /************************************************************************/ + + var __stop = false; + var __ensuring = false; + + var __ws = null; + var __ping_timer = null; + var __missed_heartbeats = 0; + var __decoder = null; + var __codec = ""; + + var __state = null; + var __frames = 0; + + /************************************************************************/ + + self.getName = () => "HTTP H.264"; + self.getMode = () => "media"; + + self.getResolution = function() { + let el = $("stream-canvas"); + return { + // Разрешение видео или элемента + "real_width": (el.width || el.offsetWidth), + "real_height": (el.height || el.offsetHeight), + "view_width": el.offsetWidth, + "view_height": el.offsetHeight, + }; + }; + + self.ensureStream = function(state) { + __state = state; + __stop = false; + __ensureMedia(false); + }; + + self.stopStream = function() { + __stop = true; + __ensuring = false; + __wsForceClose(); + __setInfo(false, false, ""); + }; + + var __ensureMedia = function(internal) { + if (__ws === null && !__stop && (!__ensuring || internal)) { + __ensuring = true; + __setInactive(); + __setInfo(false, false, ""); + __logInfo("Starting Media ..."); + __ws = new WebSocket(`${tools.is_https ? "wss" : "ws"}://${location.host}/api/media/ws`); + __ws.binaryType = "arraybuffer"; + __ws.onopen = __wsOpenHandler; + __ws.onerror = __wsErrorHandler; + __ws.onclose = __wsCloseHandler; + __ws.onmessage = async (event) => { + if (typeof event.data === "string") { + __wsJsonHandler(JSON.parse(event.data)); + } else { // Binary + await __wsBinHandler(event.data); + } + }; + } + }; + + var __wsOpenHandler = function(event) { + __logInfo("Socket opened:", event); + __missed_heartbeats = 0; + __ping_timer = setInterval(__ping, 1000); + }; + + var __ping = function() { + try { + __missed_heartbeats += 1; + if (__missed_heartbeats >= 5) { + throw new Error("Too many missed heartbeats"); + } + __ws.send(new Uint8Array([0])); + + if (__decoder && __decoder.state === "configured") { + let online = !!(__state && __state.source.online); + let info = `${__frames} fps dynamic`; + __frames = 0; + __setInfo(true, online, info); + } + } catch (ex) { + __wsErrorHandler(ex.message); + } + }; + + var __wsForceClose = function() { + if (__ws) { + __ws.onclose = null; + __ws.close(); + } + __wsCloseHandler(null); + __setInactive(); + }; + + var __wsErrorHandler = function(event) { + __logInfo("Socket error:", event); + __setInfo(false, false, event); + __wsForceClose(); + }; + + var __wsCloseHandler = function(event) { + __logInfo("Socket closed:", event); + if (__ping_timer) { + clearInterval(__ping_timer); + __ping_timer = null; + } + if (__decoder) { + __decoder.close(); + __decoder = null; + } + __missed_heartbeats = 0; + __frames = 0; + __ws = null; + if (!__stop) { + setTimeout(() => __ensureMedia(true), 1000); + } + }; + + var __wsJsonHandler = function(event) { + if (event.event_type === "media") { + __decoderCreate(event.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}); + } + + 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(); + + if (formats.h264 === undefined) { + let msg = "No H.264 stream available on PiKVM"; + __setInfo(false, false, msg); + __logInfo(msg); + return; + } + if (!window.VideoDecoder) { + let msg = "This browser can't handle direct H.264 stream"; + if (!tools.is_https) { + msg = "Direct H.264 requires HTTPS"; + } + __setInfo(false, false, msg); + __logInfo(msg); + return; + } + + __decoder = new VideoDecoder({ // eslint-disable-line no-undef + "output": (frame) => { + try { + let canvas = $("stream-canvas"); + if (canvas.width !== frame.displayWidth || canvas.height !== frame.displayHeight) { + canvas.width = frame.displayWidth; + canvas.height = frame.displayHeight; + } + canvas.getContext("2d").drawImage(frame, 0, 0); + __frames += 1; + } finally { + frame.close(); + } + }, + "error": (err) => __logInfo(err.message), + }); + __codec = `avc1.${formats.h264.profile_level_id}`; + + __ws.send(JSON.stringify({ + "event_type": "start", + "event": {"kind": "video", "format": "h264"}, + })); + }; + + var __decoderDestroy = function() { + if (__decoder !== null) { + __decoder.close(); + __decoder = null; + __codec = ""; + } + }; + + var __logInfo = (...args) => tools.info("Stream [Media]:", ...args); +} + +MediaStreamer.is_videodecoder_available = function() { + return !!window.VideoDecoder; +}; |