summaryrefslogtreecommitdiff
path: root/web/share/js/kvm/stream_media.js
diff options
context:
space:
mode:
Diffstat (limited to 'web/share/js/kvm/stream_media.js')
-rw-r--r--web/share/js/kvm/stream_media.js240
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;
+};