mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-01-29 09:01:54 +08:00
Add support for PiKVM Switch and related features
This commit introduces several new components and improvements: - Added Switch module with firmware update and configuration support - Implemented new media streaming capabilities - Updated various UI elements and CSS styles - Enhanced keyboard and mouse event handling - Added new validators and configuration options - Updated Python version support to 3.13 - Improved error handling and logging
This commit is contained in:
@@ -32,6 +32,7 @@ export function Atx(__recorder) {
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
var __has_switch = null; // Or true/false
|
||||
var __state = null;
|
||||
|
||||
var __init__ = function() {
|
||||
@@ -54,12 +55,12 @@ export function Atx(__recorder) {
|
||||
}
|
||||
if (state.enabled !== undefined) {
|
||||
__state.enabled = state.enabled;
|
||||
tools.feature.setEnabled($("atx-dropdown"), __state.enabled);
|
||||
tools.feature.setEnabled($("atx-dropdown"), (__state.enabled && !__has_switch));
|
||||
}
|
||||
if (__state.enabled !== undefined) {
|
||||
if (state.busy !== undefined) {
|
||||
__updateButtons(!state.busy);
|
||||
__state.busy = state.busy;
|
||||
__updateButtons(!__state.busy);
|
||||
}
|
||||
if (state.leds !== undefined) {
|
||||
__state.leds = state.leds;
|
||||
@@ -75,6 +76,11 @@ export function Atx(__recorder) {
|
||||
}
|
||||
};
|
||||
|
||||
self.setHasSwitch = function(has_switch) {
|
||||
__has_switch = has_switch;
|
||||
self.setState(__state);
|
||||
};
|
||||
|
||||
var __updateLeds = function(power, hdd, busy) {
|
||||
$("atx-power-led").className = (busy ? "led-yellow" : (power ? "led-green" : "led-gray"));
|
||||
$("atx-hdd-led").className = (hdd ? "led-red" : "led-gray");
|
||||
@@ -101,7 +107,7 @@ export function Atx(__recorder) {
|
||||
if ($("atx-ask-switch").checked) {
|
||||
wm.confirm(`
|
||||
Are you sure you want to press the <b>${button}</b> button?<br>
|
||||
Warning! This could case data loss on the server.
|
||||
Warning! This could cause data loss on the server.
|
||||
`).then(function(ok) {
|
||||
if (ok) {
|
||||
click_button();
|
||||
|
||||
@@ -52,6 +52,7 @@ export function Keyboard(__recordWsEvent) {
|
||||
window.addEventListener("focusin", __updateOnlineLeds);
|
||||
window.addEventListener("focusout", __updateOnlineLeds);
|
||||
|
||||
tools.storage.bindSimpleSwitch($("hid-keyboard-bad-link-switch"), "hid.keyboard.bad_link", false);
|
||||
tools.storage.bindSimpleSwitch($("hid-keyboard-swap-cc-switch"), "hid.keyboard.swap_cc", false);
|
||||
};
|
||||
|
||||
@@ -140,11 +141,16 @@ export function Keyboard(__recordWsEvent) {
|
||||
}
|
||||
let event = {
|
||||
"event_type": "key",
|
||||
"event": {"key": code, "state": state},
|
||||
"event": {
|
||||
"key": code,
|
||||
"state": state,
|
||||
"finish": $("hid-keyboard-bad-link-switch").checked,
|
||||
},
|
||||
};
|
||||
if (__ws && !$("hid-mute-switch").checked) {
|
||||
__ws.sendHidEvent(event);
|
||||
}
|
||||
delete event.event.finish;
|
||||
__recordWsEvent(event);
|
||||
};
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ export function Msd() {
|
||||
|
||||
tools.hidden.setVisible($("msd-message-offline"), (state && !state.online));
|
||||
tools.hidden.setVisible($("msd-message-image-broken"), (o && d.image && !d.image.complete && !s.uploading));
|
||||
tools.hidden.setVisible($("msd-message-too-big-for-cdrom"), (o && d.cdrom && d.image && d.image.size >= 2359296000));
|
||||
tools.hidden.setVisible($("msd-message-too-big-for-dvd"), (o && d.cdrom && d.image && d.image.size >= 33957083136));
|
||||
tools.hidden.setVisible($("msd-message-out-of-storage"), (o && d.image && !d.image.in_storage));
|
||||
tools.hidden.setVisible($("msd-message-rw-enabled"), (o && d.rw));
|
||||
tools.hidden.setVisible($("msd-message-another-user-uploads"), (o && s.uploading && !__http));
|
||||
|
||||
@@ -184,7 +184,7 @@ export function Ocr(__getGeometry) {
|
||||
"ocr_left": __sel.left,
|
||||
"ocr_top": __sel.top,
|
||||
"ocr_right": __sel.right,
|
||||
"orc_bottom": __sel.bottom,
|
||||
"ocr_bottom": __sel.bottom,
|
||||
};
|
||||
tools.httpGet("/api/streamer/snapshot", params, function(http) {
|
||||
if (http.status === 200) {
|
||||
|
||||
@@ -34,15 +34,10 @@ export function Paste(__recorder) {
|
||||
|
||||
var __init__ = function() {
|
||||
tools.storage.bindSimpleSwitch($("hid-pak-ask-switch"), "hid.pak.ask", true);
|
||||
tools.storage.bindSimpleSwitch($("hid-pak-slow-switch"), "hid.pak.slow", false);
|
||||
tools.storage.bindSimpleSwitch($("hid-pak-secure-switch"), "hid.pak.secure", false, function(value) {
|
||||
$("hid-pak-text").style.setProperty("-webkit-text-security", (value ? "disc" : "none"));
|
||||
});
|
||||
tools.feature.setEnabled($("hid-pak-secure"), (
|
||||
tools.browser.is_chrome
|
||||
|| tools.browser.is_safari
|
||||
|| tools.browser.is_opera
|
||||
));
|
||||
|
||||
$("hid-pak-keymap-selector").addEventListener("change", function() {
|
||||
tools.storage.set("hid.pak.keymap", $("hid-pak-keymap-selector").value);
|
||||
});
|
||||
@@ -73,10 +68,11 @@ export function Paste(__recorder) {
|
||||
tools.el.setEnabled($("hid-pak-keymap-selector"), false);
|
||||
|
||||
let keymap = $("hid-pak-keymap-selector").value;
|
||||
let slow = $("hid-pak-slow-switch").checked;
|
||||
|
||||
tools.debug(`HID: paste-as-keys ${keymap}: ${text}`);
|
||||
|
||||
tools.httpPost("/api/hid/print", {"limit": 0, "keymap": keymap}, function(http) {
|
||||
tools.httpPost("/api/hid/print", {"limit": 0, "keymap": keymap, "slow": slow}, function(http) {
|
||||
tools.el.setEnabled($("hid-pak-text"), true);
|
||||
tools.el.setEnabled($("hid-pak-button"), true);
|
||||
tools.el.setEnabled($("hid-pak-keymap-selector"), true);
|
||||
@@ -86,7 +82,7 @@ export function Paste(__recorder) {
|
||||
} else if (http.status !== 200) {
|
||||
wm.error("HID paste error", http.responseText);
|
||||
} else if (http.status === 200) {
|
||||
__recorder.recordPrintEvent(text, keymap);
|
||||
__recorder.recordPrintEvent(text, keymap, slow);
|
||||
}
|
||||
}, text, "text/plain");
|
||||
};
|
||||
|
||||
@@ -67,8 +67,8 @@ export function Recorder() {
|
||||
__recordEvent(event);
|
||||
};
|
||||
|
||||
self.recordPrintEvent = function(text, keymap) {
|
||||
__recordEvent({"event_type": "print", "event": {"text": text, "keymap": keymap}});
|
||||
self.recordPrintEvent = function(text, keymap, slow) {
|
||||
__recordEvent({"event_type": "print", "event": {"text": text, "keymap": keymap, "slow": slow}});
|
||||
};
|
||||
|
||||
self.recordAtxButtonEvent = function(button) {
|
||||
@@ -159,9 +159,12 @@ export function Recorder() {
|
||||
|
||||
} else if (event.event_type === "print") {
|
||||
__checkType(event.event.text, "string", "Non-string print text");
|
||||
if (event.event.keymap) {
|
||||
if (event.event.keymap !== undefined) {
|
||||
__checkType(event.event.keymap, "string", "Non-string keymap");
|
||||
}
|
||||
if (event.event.slow !== undefined) {
|
||||
__checkType(event.event.slow, "boolean", "Non-bool slow");
|
||||
}
|
||||
|
||||
} else if (event.event_type === "key") {
|
||||
__checkType(event.event.key, "string", "Non-string key code");
|
||||
@@ -284,9 +287,12 @@ export function Recorder() {
|
||||
|
||||
} else if (event.event_type === "print") {
|
||||
let params = {"limit": 0};
|
||||
if (event.event.keymap) {
|
||||
if (event.event.keymap !== undefined) {
|
||||
params["keymap"] = event.event.keymap;
|
||||
}
|
||||
if (event.event.slow !== undefined) {
|
||||
params["slow"] = event.event.slow;
|
||||
}
|
||||
tools.httpPost("/api/hid/print", params, function(http) {
|
||||
if (http.status === 413) {
|
||||
wm.error("Too many text for paste!");
|
||||
@@ -330,7 +336,11 @@ export function Recorder() {
|
||||
});
|
||||
return;
|
||||
|
||||
} else if (["key", "mouse_button", "mouse_move", "mouse_wheel", "mouse_relative"].includes(event.event_type)) {
|
||||
} else if (event.event_type === "key") {
|
||||
event.event.finish = $("hid-keyboard-bad-link-switch").checked;
|
||||
__ws.sendHidEvent(event);
|
||||
|
||||
} else if (["mouse_button", "mouse_move", "mouse_wheel", "mouse_relative"].includes(event.event_type)) {
|
||||
__ws.sendHidEvent(event);
|
||||
|
||||
} else if (event.event_type === "mouse_move_random") {
|
||||
|
||||
@@ -34,6 +34,7 @@ import {Msd} from "./msd.js";
|
||||
import {Streamer} from "./stream.js";
|
||||
import {Gpio} from "./gpio.js";
|
||||
import {Ocr} from "./ocr.js";
|
||||
import {Switch} from "./switch.js";
|
||||
|
||||
|
||||
export function Session() {
|
||||
@@ -54,6 +55,7 @@ export function Session() {
|
||||
var __msd = new Msd();
|
||||
var __gpio = new Gpio(__recorder);
|
||||
var __ocr = new Ocr(__streamer.getGeometry);
|
||||
var __switch = new Switch();
|
||||
|
||||
var __info_hw_state = null;
|
||||
var __info_fan_state = null;
|
||||
@@ -291,7 +293,7 @@ export function Session() {
|
||||
|
||||
tools.httpGet("/api/auth/check", null, function(http) {
|
||||
if (http.status === 200) {
|
||||
__ws = new WebSocket(`${tools.is_https ? "wss" : "ws"}://${location.host}/api/ws?legacy=0`);
|
||||
__ws = new WebSocket(`${tools.is_https ? "wss" : "ws"}://${location.host}/api/ws`);
|
||||
__ws.sendHidEvent = (event) => __sendHidEvent(__ws, event.event_type, event.event);
|
||||
__ws.onopen = __wsOpenHandler;
|
||||
__ws.onmessage = __wsMessageHandler;
|
||||
@@ -314,6 +316,9 @@ export function Session() {
|
||||
if (event_type == "key") {
|
||||
let data = __ascii_encoder.encode("\x01\x00" + event.key);
|
||||
data[1] = (event.state ? 1 : 0);
|
||||
if (event.finish === true) { // Optional
|
||||
data[1] |= 0x02;
|
||||
}
|
||||
ws.send(data);
|
||||
|
||||
} else if (event_type == "mouse_button") {
|
||||
@@ -363,14 +368,29 @@ export function Session() {
|
||||
let data = JSON.parse(event.data);
|
||||
switch (data.event_type) {
|
||||
case "pong": __missed_heartbeats = 0; break;
|
||||
case "info_state": __setInfoState(data.event); break;
|
||||
case "gpio_state": __gpio.setState(data.event); break;
|
||||
case "hid_state": __hid.setState(data.event); break;
|
||||
case "hid_keymaps_state": __paste.setState(data.event); break;
|
||||
case "atx_state": __atx.setState(data.event); break;
|
||||
case "msd_state": __msd.setState(data.event); break;
|
||||
case "streamer_state": __streamer.setState(data.event); break;
|
||||
case "ocr_state": __ocr.setState(data.event); break;
|
||||
case "info": __setInfoState(data.event); break;
|
||||
case "gpio": __gpio.setState(data.event); break;
|
||||
case "hid": __hid.setState(data.event); break;
|
||||
case "hid_keymaps": __paste.setState(data.event); break;
|
||||
case "atx": __atx.setState(data.event); break;
|
||||
case "streamer": __streamer.setState(data.event); break;
|
||||
case "ocr": __ocr.setState(data.event); break;
|
||||
|
||||
case "msd":
|
||||
if (data.event.online === false) {
|
||||
__switch.setMsdConnected(false);
|
||||
} else if (data.event.drive !== undefined) {
|
||||
__switch.setMsdConnected(data.event.drive.connected);
|
||||
}
|
||||
__msd.setState(data.event);
|
||||
break;
|
||||
|
||||
case "switch":
|
||||
if (data.event.model) {
|
||||
__atx.setHasSwitch(data.event.model.ports.length > 0);
|
||||
}
|
||||
__switch.setState(data.event);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -401,6 +421,7 @@ export function Session() {
|
||||
__streamer.setState(null);
|
||||
__ocr.setState(null);
|
||||
__recorder.setSocket(null);
|
||||
__switch.setState(null);
|
||||
__ws = null;
|
||||
|
||||
setTimeout(function() {
|
||||
|
||||
@@ -27,6 +27,7 @@ import {tools, $} from "../tools.js";
|
||||
import {wm} from "../wm.js";
|
||||
|
||||
import {JanusStreamer} from "./stream_janus.js";
|
||||
import {MediaStreamer} from "./stream_media.js";
|
||||
import {MjpegStreamer} from "./stream_mjpeg.js";
|
||||
|
||||
|
||||
@@ -93,6 +94,15 @@ export function Streamer() {
|
||||
__resetStream();
|
||||
}
|
||||
}
|
||||
tools.el.setEnabled($("stream-mic-switch"), !!value);
|
||||
});
|
||||
|
||||
tools.storage.bindSimpleSwitch($("stream-mic-switch"), "stream.mic", false, function(allow_mic) {
|
||||
if (__streamer.getMode() === "janus") {
|
||||
if (__streamer.isMicAllowed() !== allow_mic) {
|
||||
__resetStream();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tools.el.setOnClick($("stream-screenshot-button"), __clickScreenshotButton);
|
||||
@@ -174,17 +184,20 @@ export function Streamer() {
|
||||
if (state.features) {
|
||||
let f = state.features;
|
||||
let l = state.limits;
|
||||
let has_webrtc = JanusStreamer.is_webrtc_available();
|
||||
let has_h264 = JanusStreamer.is_h264_available();
|
||||
let has_janus = (__janus_imported && f.h264 && has_webrtc); // Don't check has_h264 for sure
|
||||
let sup_h264 = $("stream-video").canPlayType("video/mp4; codecs=\"avc1.42E01F\"");
|
||||
let sup_vd = MediaStreamer.is_videodecoder_available();
|
||||
let sup_webrtc = JanusStreamer.is_webrtc_available();
|
||||
let has_media = (f.h264 && sup_vd); // Don't check sup_h264 for sure
|
||||
let has_janus = (__janus_imported && f.h264 && sup_webrtc); // Same
|
||||
|
||||
tools.info(
|
||||
`Stream: Janus WebRTC state: features.h264=${f.h264},`
|
||||
+ ` webrtc=${has_webrtc}, h264=${has_h264}, janus_imported=${__janus_imported}`
|
||||
+ ` webrtc=${sup_webrtc}, h264=${sup_h264}, janus_imported=${__janus_imported}`
|
||||
);
|
||||
|
||||
tools.hidden.setVisible($("stream-message-no-webrtc"), __janus_imported && f.h264 && !has_webrtc);
|
||||
tools.hidden.setVisible($("stream-message-no-h264"), __janus_imported && f.h264 && !has_h264);
|
||||
tools.hidden.setVisible($("stream-message-no-webrtc"), __janus_imported && f.h264 && !sup_webrtc);
|
||||
tools.hidden.setVisible($("stream-message-no-vd"), f.h264 && !sup_vd);
|
||||
tools.hidden.setVisible($("stream-message-no-h264"), __janus_imported && f.h264 && !sup_h264);
|
||||
|
||||
tools.slider.setRange($("stream-desired-fps-slider"), l.desired_fps.min, l.desired_fps.max);
|
||||
if (f.resolution) {
|
||||
@@ -196,21 +209,28 @@ export function Streamer() {
|
||||
} else {
|
||||
$("stream-resolution-selector").options.length = 0;
|
||||
}
|
||||
if (has_janus) {
|
||||
if (f.h264) {
|
||||
tools.slider.setRange($("stream-h264-bitrate-slider"), l.h264_bitrate.min, l.h264_bitrate.max);
|
||||
tools.slider.setRange($("stream-h264-gop-slider"), l.h264_gop.min, l.h264_gop.max);
|
||||
}
|
||||
|
||||
// tools.feature.setEnabled($("stream-quality"), f.quality); // Only on s.encoder.quality
|
||||
tools.feature.setEnabled($("stream-resolution"), f.resolution);
|
||||
tools.feature.setEnabled($("stream-h264-bitrate"), has_janus);
|
||||
tools.feature.setEnabled($("stream-h264-gop"), has_janus);
|
||||
tools.feature.setEnabled($("stream-mode"), has_janus);
|
||||
if (!has_janus) {
|
||||
tools.feature.setEnabled($("stream-h264-bitrate"), f.h264);
|
||||
tools.feature.setEnabled($("stream-h264-gop"), f.h264);
|
||||
tools.feature.setEnabled($("stream-mode"), f.h264);
|
||||
if (!f.h264) {
|
||||
tools.feature.setEnabled($("stream-audio"), false);
|
||||
tools.feature.setEnabled($("stream-mic"), false);
|
||||
}
|
||||
|
||||
let mode = (has_janus ? tools.storage.get("stream.mode", "janus") : "mjpeg");
|
||||
let mode = tools.storage.get("stream.mode", "janus");
|
||||
if (mode === "janus" && !has_janus) {
|
||||
mode = "media";
|
||||
}
|
||||
if (mode === "media" && !has_media) {
|
||||
mode = "mjpeg";
|
||||
}
|
||||
tools.radio.clickValue("stream-mode-radio", mode);
|
||||
}
|
||||
|
||||
@@ -287,14 +307,19 @@ export function Streamer() {
|
||||
__streamer.stopStream();
|
||||
if (mode === "janus") {
|
||||
__streamer = new JanusStreamer(__setActive, __setInactive, __setInfo,
|
||||
tools.storage.getInt("stream.orient", 0), !$("stream-video").muted);
|
||||
tools.storage.getInt("stream.orient", 0), !$("stream-video").muted, $("stream-mic-switch").checked);
|
||||
// Firefox doesn't support RTP orientation:
|
||||
// - https://bugzilla.mozilla.org/show_bug.cgi?id=1316448
|
||||
tools.feature.setEnabled($("stream-orient"), !tools.browser.is_firefox);
|
||||
} else { // mjpeg
|
||||
__streamer = new MjpegStreamer(__setActive, __setInactive, __setInfo);
|
||||
} else {
|
||||
if (mode === "media") {
|
||||
__streamer = new MediaStreamer(__setActive, __setInactive, __setInfo);
|
||||
} else { // mjpeg
|
||||
__streamer = new MjpegStreamer(__setActive, __setInactive, __setInfo);
|
||||
}
|
||||
tools.feature.setEnabled($("stream-orient"), false);
|
||||
tools.feature.setEnabled($("stream-audio"), false); // Enabling in stream_janus.js
|
||||
tools.feature.setEnabled($("stream-mic"), false); // Ditto
|
||||
}
|
||||
if (wm.isWindowVisible($("stream-window"))) {
|
||||
__streamer.ensureStream((__state && __state.streamer !== undefined) ? __state.streamer : null);
|
||||
@@ -305,7 +330,8 @@ export function Streamer() {
|
||||
let mode = tools.radio.getValue("stream-mode-radio");
|
||||
tools.storage.set("stream.mode", mode);
|
||||
if (mode !== __streamer.getMode()) {
|
||||
tools.hidden.setVisible($("stream-image"), (mode !== "janus"));
|
||||
tools.hidden.setVisible($("stream-canvas"), (mode === "media"));
|
||||
tools.hidden.setVisible($("stream-image"), (mode === "mjpeg"));
|
||||
tools.hidden.setVisible($("stream-video"), (mode === "janus"));
|
||||
__resetStream(mode);
|
||||
}
|
||||
|
||||
@@ -29,9 +29,13 @@ import {tools, $} from "../tools.js";
|
||||
var _Janus = null;
|
||||
|
||||
|
||||
export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, __allow_audio) {
|
||||
export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, __allow_audio, __allow_mic) {
|
||||
var self = this;
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
__allow_mic = (__allow_audio && __allow_mic); // XXX: Mic only with audio
|
||||
|
||||
var __stop = false;
|
||||
var __ensuring = false;
|
||||
|
||||
@@ -45,10 +49,22 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
var __state = null;
|
||||
var __frames = 0;
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
self.getOrientation = () => __orient;
|
||||
self.isAudioAllowed = () => __allow_audio;
|
||||
self.isMicAllowed = () => __allow_mic;
|
||||
|
||||
self.getName = () => (__allow_audio ? "H.264 + Audio" : "H.264");
|
||||
self.getName = function() {
|
||||
let name = "WebRTC H.264";
|
||||
if (__allow_audio) {
|
||||
name += " + Audio";
|
||||
if (__allow_mic) {
|
||||
name += " + Mic";
|
||||
}
|
||||
}
|
||||
return name;
|
||||
};
|
||||
self.getMode = () => "janus";
|
||||
|
||||
self.getResolution = function() {
|
||||
@@ -75,9 +91,9 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
|
||||
var __ensureJanus = function(internal) {
|
||||
if (__janus === null && !__stop && (!__ensuring || internal)) {
|
||||
__ensuring = true;
|
||||
__setInactive();
|
||||
__setInfo(false, false, "");
|
||||
__ensuring = true;
|
||||
__logInfo("Starting Janus ...");
|
||||
__janus = new _Janus({
|
||||
"server": `${tools.is_https ? "wss" : "ws"}://${location.host}/janus/ws`,
|
||||
@@ -148,6 +164,16 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
el.srcObject = new MediaStream();
|
||||
}
|
||||
el.srcObject.addTrack(track);
|
||||
// FIXME: Задержка уменьшается, но начинаются заикания на кейфреймах.
|
||||
// XXX: Этот пример переехал из януса 0.x, перед использованием адаптировать к 1.x.
|
||||
// - https://github.com/Glimesh/janus-ftl-plugin/issues/101
|
||||
/*if (__handle && __handle.webrtcStuff && __handle.webrtcStuff.pc) {
|
||||
for (let receiver of __handle.webrtcStuff.pc.getReceivers()) {
|
||||
if (receiver.track && receiver.track.kind === "video" && receiver.playoutDelayHint !== undefined) {
|
||||
receiver.playoutDelayHint = 0;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
};
|
||||
|
||||
var __removeTrack = function(track) {
|
||||
@@ -215,6 +241,7 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
__setInfo(false, false, "");
|
||||
} else if (msg.result.status === "features") {
|
||||
tools.feature.setEnabled($("stream-audio"), msg.result.features.audio);
|
||||
tools.feature.setEnabled($("stream-mic"), msg.result.features.mic);
|
||||
}
|
||||
} else if (msg.error_code || msg.error) {
|
||||
__logError("Got uStreamer error message:", msg.error_code, "-", msg.error);
|
||||
@@ -237,17 +264,13 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
__logInfo("Handling SDP:", jsep);
|
||||
let tracks = [{"type": "video", "capture": false, "recv": true, "add": true}];
|
||||
if (__allow_audio) {
|
||||
tracks.push({"type": "audio", "capture": false, "recv": true, "add": true});
|
||||
tracks.push({"type": "audio", "capture": __allow_mic, "recv": true, "add": true});
|
||||
}
|
||||
__handle.createAnswer({
|
||||
"jsep": jsep,
|
||||
|
||||
// Janus 1.x
|
||||
"tracks": tracks,
|
||||
|
||||
// Janus 0.x
|
||||
"media": {"audioSend": false, "videoSend": false, "data": false},
|
||||
|
||||
// Chrome is playing OPUS as mono without this hack
|
||||
// - https://issues.webrtc.org/issues/41481053 - IT'S NOT FIXED!
|
||||
// - https://github.com/ossrs/srs/pull/2683/files
|
||||
@@ -288,50 +311,6 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
}
|
||||
},
|
||||
|
||||
// Janus 0.x
|
||||
"onremotestream": function(stream) {
|
||||
if (stream === null) {
|
||||
// https://github.com/pikvm/pikvm/issues/1084
|
||||
// Этого вообще не должно происходить, но почему-то янусу в unmute
|
||||
// может прилететь null-эвент. Костыляем, наблюдаем.
|
||||
__logError("Got invalid onremotestream(null). Restarting Janus...");
|
||||
__destroyJanus();
|
||||
return;
|
||||
}
|
||||
|
||||
let tracks = stream.getTracks();
|
||||
__logInfo("Got a remote stream changes:", stream, tracks);
|
||||
|
||||
let has_video = false;
|
||||
for (let track of tracks) {
|
||||
if (track.kind == "video") {
|
||||
has_video = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_video && __isOnline()) {
|
||||
// Chrome sends `muted` notifiation for tracks in `disconnected` ICE state
|
||||
// and Janus.js just removes muted track from list of available tracks.
|
||||
// But track still exists actually so it's safe to just ignore that case.
|
||||
return;
|
||||
}
|
||||
|
||||
_Janus.attachMediaStream($("stream-video"), stream);
|
||||
__sendKeyRequired();
|
||||
__startInfoInterval();
|
||||
|
||||
// FIXME: Задержка уменьшается, но начинаются заикания на кейфреймах.
|
||||
// - https://github.com/Glimesh/janus-ftl-plugin/issues/101
|
||||
/*if (__handle && __handle.webrtcStuff && __handle.webrtcStuff.pc) {
|
||||
for (let receiver of __handle.webrtcStuff.pc.getReceivers()) {
|
||||
if (receiver.track && receiver.track.kind === "video" && receiver.playoutDelayHint !== undefined) {
|
||||
receiver.playoutDelayHint = 0;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
},
|
||||
|
||||
"oncleanup": function() {
|
||||
__logInfo("Got a cleanup notification");
|
||||
__stopInfoInterval();
|
||||
@@ -388,11 +367,12 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
|
||||
var __sendWatch = function() {
|
||||
if (__handle) {
|
||||
__logInfo(`Sending WATCH(orient=${__orient}, audio=${__allow_audio}) + FEATURES ...`);
|
||||
__logInfo(`Sending WATCH(orient=${__orient}, audio=${__allow_audio}, mic=${__allow_mic}) + FEATURES ...`);
|
||||
__handle.send({"message": {"request": "features"}});
|
||||
__handle.send({"message": {"request": "watch", "params": {
|
||||
"orientation": __orient,
|
||||
"audio": __allow_audio,
|
||||
"mic": __allow_mic,
|
||||
}}});
|
||||
}
|
||||
};
|
||||
@@ -447,11 +427,3 @@ JanusStreamer.ensure_janus = function(callback) {
|
||||
JanusStreamer.is_webrtc_available = function() {
|
||||
return !!window.RTCPeerConnection;
|
||||
};
|
||||
|
||||
JanusStreamer.is_h264_available = function() {
|
||||
let ok = true;
|
||||
if ($("stream-video").canPlayType) {
|
||||
ok = $("stream-video").canPlayType("video/mp4; codecs=\"avc1.42E01F\"");
|
||||
}
|
||||
return ok;
|
||||
};
|
||||
|
||||
241
web/share/js/kvm/stream_media.js
Normal file
241
web/share/js/kvm/stream_media.js
Normal file
@@ -0,0 +1,241 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# 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 __canvas = $("stream-canvas");
|
||||
var __ctx = __canvas.getContext("2d");
|
||||
|
||||
var __state = null;
|
||||
var __frames = 0;
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
self.getName = () => "HTTP H.264";
|
||||
self.getMode = () => "media";
|
||||
|
||||
self.getResolution = function() {
|
||||
return {
|
||||
// Разрешение видео или элемента
|
||||
"real_width": (__canvas.width || __canvas.offsetWidth),
|
||||
"real_height": (__canvas.height || __canvas.offsetHeight),
|
||||
"view_width": __canvas.offsetWidth,
|
||||
"view_height": __canvas.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});
|
||||
__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();
|
||||
|
||||
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 {
|
||||
if (__canvas.width !== frame.displayWidth || __canvas.height !== frame.displayHeight) {
|
||||
__canvas.width = frame.displayWidth;
|
||||
__canvas.height = frame.displayHeight;
|
||||
}
|
||||
__ctx.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": {"type": "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;
|
||||
};
|
||||
@@ -41,7 +41,7 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) {
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
self.getName = () => "MJPEG";
|
||||
self.getName = () => "HTTP MJPEG";
|
||||
self.getMode = () => "mjpeg";
|
||||
|
||||
self.getResolution = function() {
|
||||
|
||||
610
web/share/js/kvm/switch.js
Normal file
610
web/share/js/kvm/switch.js
Normal file
@@ -0,0 +1,610 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# KVMD - The main PiKVM daemon. #
|
||||
# #
|
||||
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# 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";
|
||||
import {wm} from "../wm.js";
|
||||
|
||||
|
||||
export function Switch() {
|
||||
var self = this;
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
var __state = null;
|
||||
var __msd_connected = false;
|
||||
|
||||
var __init__ = function() {
|
||||
tools.selector.addOption($("switch-edid-selector"), "Default", "default");
|
||||
$("switch-edid-selector").onchange = __selectEdid;
|
||||
|
||||
tools.el.setOnClick($("switch-edid-add-button"), __clickAddEdidButton);
|
||||
tools.el.setOnClick($("switch-edid-remove-button"), __clickRemoveEdidButton);
|
||||
tools.el.setOnClick($("switch-edid-copy-data-button"), __clickCopyEdidDataButton);
|
||||
|
||||
tools.storage.bindSimpleSwitch($("switch-atx-ask-switch"), "switch.atx.ask", true);
|
||||
|
||||
for (let role of ["inactive", "active", "flashing", "beacon", "bootloader"]) {
|
||||
let el_brightness = $(`switch-color-${role}-brightness-slider`);
|
||||
tools.slider.setParams(el_brightness, 0, 255, 1, 0);
|
||||
el_brightness.onchange = $(`switch-color-${role}-input`).onchange = tools.partial(__selectColor, role);
|
||||
tools.el.setOnClick($(`switch-color-${role}-default-button`), tools.partial(__clickSetDefaultColorButton, role));
|
||||
}
|
||||
};
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
self.setMsdConnected = function(connected) {
|
||||
__msd_connected = connected;
|
||||
};
|
||||
|
||||
self.setState = function(state) {
|
||||
if (state) {
|
||||
if (!__state) {
|
||||
__state = {};
|
||||
}
|
||||
if (state.model) {
|
||||
__state = {};
|
||||
__applyModel(state.model);
|
||||
}
|
||||
if (__state.model) {
|
||||
if (state.summary) {
|
||||
__applySummary(state.summary);
|
||||
}
|
||||
if (state.beacons) {
|
||||
__applyBeacons(state.beacons);
|
||||
}
|
||||
if (state.usb) {
|
||||
__applyUsb(state.usb);
|
||||
}
|
||||
if (state.video) {
|
||||
__applyVideo(state.video);
|
||||
}
|
||||
if (state.atx) {
|
||||
__applyAtx(state.atx);
|
||||
}
|
||||
if (state.edids) {
|
||||
__applyEdids(state.edids);
|
||||
}
|
||||
if (state.colors) {
|
||||
__applyColors(state.colors);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tools.feature.setEnabled($("switch-dropdown"), false);
|
||||
$("switch-chain").innerText = "";
|
||||
$("switch-active-port").innerText = "N/A";
|
||||
__setPowerLedState($("switch-atx-power-led"), false, false);
|
||||
__setLedState($("switch-atx-hdd-led"), "red", false);
|
||||
__state = null;
|
||||
}
|
||||
};
|
||||
|
||||
var __applyColors = function(colors) {
|
||||
for (let role in colors) {
|
||||
let color = colors[role];
|
||||
$(`switch-color-${role}-input`).value = (
|
||||
"#"
|
||||
+ color.red.toString(16).padStart(2, "0")
|
||||
+ color.green.toString(16).padStart(2, "0")
|
||||
+ color.blue.toString(16).padStart(2, "0")
|
||||
);
|
||||
$(`switch-color-${role}-brightness-slider`).value = color.brightness;
|
||||
}
|
||||
__state.colors = colors;
|
||||
};
|
||||
|
||||
var __selectColor = function(role) {
|
||||
let el_color = $(`switch-color-${role}-input`);
|
||||
let el_brightness = $(`switch-color-${role}-brightness-slider`);
|
||||
let color = __state.colors[role];
|
||||
let brightness = parseInt(el_brightness.value);
|
||||
let rgbx = (
|
||||
el_color.value.slice(1)
|
||||
+ ":" + brightness.toString(16).padStart(2, "0")
|
||||
+ ":" + color.blink_ms.toString(16).padStart(4, "0")
|
||||
);
|
||||
__sendPost("/api/switch/set_colors", {[role]: rgbx}, function() {
|
||||
el_color.value = (
|
||||
"#"
|
||||
+ color.red.toString(16).padStart(2, "0")
|
||||
+ color.green.toString(16).padStart(2, "0")
|
||||
+ color.blue.toString(16).padStart(2, "0")
|
||||
);
|
||||
el_brightness.value = color.brightness;
|
||||
});
|
||||
};
|
||||
|
||||
var __clickSetDefaultColorButton = function(role) {
|
||||
__sendPost("/api/switch/set_colors", {[role]: "default"});
|
||||
};
|
||||
|
||||
var __applyEdids = function(edids) {
|
||||
let el = $("switch-edid-selector");
|
||||
let old_edid_id = el.value;
|
||||
el.options.length = 1;
|
||||
for (let kv of Object.entries(edids.all)) {
|
||||
if (kv[0] !== "default") {
|
||||
tools.selector.addOption(el, kv[1].name, kv[0]);
|
||||
}
|
||||
}
|
||||
el.value = (old_edid_id in edids.all ? old_edid_id : "default");
|
||||
|
||||
for (let port in __state.model.ports) {
|
||||
let custom = (edids.used[port] !== "default");
|
||||
$(`__switch-custom-edid-p${port}`).style.visibility = (custom ? "unset" : "hidden");
|
||||
}
|
||||
|
||||
__state.edids = edids;
|
||||
__selectEdid();
|
||||
};
|
||||
|
||||
var __selectEdid = function() {
|
||||
let edid_id = $("switch-edid-selector").value;
|
||||
let edid = null;
|
||||
try { edid = __state.edids.all[edid_id]; } catch { edid_id = ""; }
|
||||
let parsed = (edid ? edid.parsed : null);
|
||||
let na = "<i><Not Available></i>";
|
||||
$("switch-edid-info-mfc-id").innerHTML = (parsed ? tools.escape(parsed.mfc_id) : na);
|
||||
$("switch-edid-info-product-id").innerHTML = (parsed ? tools.escape(`0x${parsed.product_id.toString(16).toUpperCase()}`) : na);
|
||||
$("switch-edid-info-serial").innerHTML = (parsed ? tools.escape(`0x${parsed.serial.toString(16).toUpperCase()}`) : na);
|
||||
$("switch-edid-info-monitor-name").innerHTML = ((parsed && parsed.monitor_name) ? tools.escape(parsed.monitor_name) : na);
|
||||
$("switch-edid-info-monitor-serial").innerHTML = ((parsed && parsed.monitor_serial) ? tools.escape(parsed.monitor_serial) : na);
|
||||
$("switch-edid-info-audio").innerHTML = (parsed ? (parsed.audio ? "Yes" : "No") : na);
|
||||
tools.el.setEnabled($("switch-edid-remove-button"), (edid_id && (edid_id !== "default")));
|
||||
tools.el.setEnabled($("switch-edid-copy-data-button"), !!edid_id);
|
||||
};
|
||||
|
||||
var __clickAddEdidButton = function() {
|
||||
let create_content = function(el_parent, el_ok_button) {
|
||||
tools.el.setEnabled(el_ok_button, false);
|
||||
el_parent.innerHTML = `
|
||||
<table>
|
||||
<tr>
|
||||
<td>Name:</td>
|
||||
<td><input
|
||||
type="text" autocomplete="off" id="__switch-edid-new-name-input"
|
||||
placeholder="Enter some meaningful name"
|
||||
style="width:100%"
|
||||
/></td>
|
||||
</tr>
|
||||
<tr><td colspan="2">HEX data:</td></tr>
|
||||
<tr>
|
||||
<td colspan="2"><textarea
|
||||
id="__switch-edid-new-data-text" placeholder="Like 0123ABCD..."
|
||||
style="min-width:350px"
|
||||
></textarea><td>
|
||||
</table>
|
||||
`;
|
||||
let el_name = $("__switch-edid-new-name-input");
|
||||
let el_data = $("__switch-edid-new-data-text");
|
||||
el_name.oninput = el_data.oninput = function() {
|
||||
let name = el_name.value.replace(/\s+/g, "");
|
||||
let data = el_data.value.replace(/\s+/g, "");
|
||||
tools.el.setEnabled(el_ok_button, ((name.length > 0) && /[0-9a-fA-F]{512}/.test(data)));
|
||||
};
|
||||
};
|
||||
|
||||
wm.modal("Add new EDID", create_content, true, true).then(function(ok) {
|
||||
if (ok) {
|
||||
let name = $("__switch-edid-new-name-input").value;
|
||||
let data = $("__switch-edid-new-data-text").value;
|
||||
__sendPost("/api/switch/edids/create", {"name": name, "data": data});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var __clickRemoveEdidButton = function() {
|
||||
let edid_id = $("switch-edid-selector").value;
|
||||
if (edid_id && __state && __state.edids) {
|
||||
let name = __state.edids.all[edid_id].name;
|
||||
let html = "Are you sure to remove this EDID?<br>Ports that used it will change it to the default.";
|
||||
wm.confirm(html, name).then(function(ok) {
|
||||
if (ok) {
|
||||
__sendPost("/api/switch/edids/remove", {"id": edid_id});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var __clickCopyEdidDataButton = function() {
|
||||
let edid_id = $("switch-edid-selector").value;
|
||||
if (edid_id && __state && __state.edids) {
|
||||
let data = __state.edids.all[edid_id].data;
|
||||
data = data.replace(/(.{32})/g, "$1\n");
|
||||
wm.copyTextToClipboard(data);
|
||||
}
|
||||
};
|
||||
|
||||
var __applyUsb = function(usb) {
|
||||
for (let port = 0; port < __state.model.ports.length; ++port) {
|
||||
if (!__state.usb || __state.usb.links[port] !== usb.links[port]) {
|
||||
__setLedState($(`__switch-usb-led-p${port}`), "green", usb.links[port]);
|
||||
}
|
||||
}
|
||||
__state.usb = usb;
|
||||
};
|
||||
|
||||
var __applyVideo = function(video) {
|
||||
for (let port = 0; port < __state.model.ports.length; ++port) {
|
||||
if (!__state.video || __state.video.links[port] !== video.links[port]) {
|
||||
__setLedState($(`__switch-video-led-p${port}`), "green", video.links[port]);
|
||||
}
|
||||
}
|
||||
__state.video = video;
|
||||
};
|
||||
|
||||
var __applyAtx = function(atx) {
|
||||
for (let port = 0; port < __state.model.ports.length; ++port) {
|
||||
let busy = atx.busy[port];
|
||||
if (!__state.atx || __state.atx.leds.power[port] !== atx.leds.power[port] || __state.atx.busy[port] !== busy) {
|
||||
let power = atx.leds.power[port];
|
||||
__setPowerLedState($(`__switch-atx-power-led-p${port}`), power, busy);
|
||||
if (port === __state.summary.active_port) {
|
||||
// summary есть всегда, если есть model, и atx обновляется последним в setState()
|
||||
__setPowerLedState($("switch-atx-power-led"), power, busy);
|
||||
}
|
||||
}
|
||||
if (!__state.atx || __state.atx.leds.hdd[port] !== atx.leds.hdd[port]) {
|
||||
let hdd = atx.leds.hdd[port];
|
||||
__setLedState($(`__switch-atx-hdd-led-p${port}`), "red", hdd);
|
||||
if (port === __state.summary.active_port) {
|
||||
__setLedState($("switch-atx-hdd-led"), "red", hdd);
|
||||
}
|
||||
}
|
||||
if (!__state.atx || __state.atx.busy[port] !== busy) {
|
||||
tools.el.setEnabled($(`__switch-atx-power-button-p${port}`), !busy);
|
||||
tools.el.setEnabled($(`__switch-atx-power-long-button-p${port}`), !busy);
|
||||
tools.el.setEnabled($(`__switch-atx-reset-button-p${port}`), !busy);
|
||||
}
|
||||
}
|
||||
__state.atx = atx;
|
||||
};
|
||||
|
||||
var __applyBeacons = function(beacons) {
|
||||
for (let unit = 0; unit < __state.model.units.length; ++unit) {
|
||||
if (!__state.beacons || __state.beacons.uplinks[unit] !== beacons.uplinks[unit]) {
|
||||
__setLedState($(`__switch-beacon-led-u${unit}`), "green", beacons.uplinks[unit]);
|
||||
}
|
||||
if (!__state.beacons || __state.beacons.downlinks[unit] !== beacons.downlinks[unit]) {
|
||||
__setLedState($(`__switch-beacon-led-d${unit}`), "green", beacons.downlinks[unit]);
|
||||
}
|
||||
}
|
||||
for (let port = 0; port < __state.model.ports.length; ++port) {
|
||||
if (!__state.beacons || __state.beacons.ports[port] !== beacons.ports[port]) {
|
||||
__setLedState($(`__switch-beacon-led-p${port}`), "green", beacons.ports[port]);
|
||||
}
|
||||
}
|
||||
__state.beacons = beacons;
|
||||
};
|
||||
|
||||
var __applySummary = function(summary) {
|
||||
let active = summary.active_port;
|
||||
if (!__state.summary || __state.summary.active_port !== active) {
|
||||
if (active < 0 || active >= __state.model.ports.length) {
|
||||
$("switch-active-port").innerText = "N/A";
|
||||
} else {
|
||||
$("switch-active-port").innerText = "p" + __formatPort(__state.model, active);
|
||||
}
|
||||
for (let port = 0; port < __state.model.ports.length; ++port) {
|
||||
__setLedState($(`__switch-port-led-p${port}`), "green", (port === active));
|
||||
}
|
||||
}
|
||||
if (__state.atx) {
|
||||
// Синхронизация светодиодов ATX при смене порта
|
||||
let power = false;
|
||||
let busy = false;
|
||||
let hdd = false;
|
||||
if (active >= 0 && active < __state.model.ports.length) {
|
||||
power = __state.atx.leds.power[active];
|
||||
hdd = __state.atx.leds.hdd[active];
|
||||
busy = __state.atx.busy[active];
|
||||
}
|
||||
__setPowerLedState($("switch-atx-power-led"), power, busy);
|
||||
__setLedState($("switch-atx-hdd-led"), "red", hdd);
|
||||
}
|
||||
__state.summary = summary;
|
||||
};
|
||||
|
||||
var __applyModel = function(model) {
|
||||
tools.feature.setEnabled($("switch-dropdown"), model.ports.length);
|
||||
|
||||
let content = "";
|
||||
let unit = -1;
|
||||
for (let port = 0; port < model.ports.length; ++port) {
|
||||
let pa = model.ports[port]; // pa == port attrs
|
||||
if (unit !== pa.unit) {
|
||||
unit = pa.unit;
|
||||
content += `${unit > 0 ? "<tr><td colspan=100><hr></td></tr>" : ""}
|
||||
<tr>
|
||||
<td></td><td></td><td></td>
|
||||
<td class="value">Unit: ${unit + 1}</td>
|
||||
<td></td>
|
||||
<td colspan=100>
|
||||
<div class="buttons-row">
|
||||
<button id="__switch-beacon-button-u${unit}" class="small" title="Toggle uplink Beacon Led">
|
||||
<img id="__switch-beacon-led-u${unit}" class="inline-lamp led-gray" src="/share/svg/led-beacon.svg"/>
|
||||
Uplink
|
||||
</button>
|
||||
<button id="__switch-beacon-button-d${unit}" class="small" title="Toggle downlink Beacon Led">
|
||||
<img id="__switch-beacon-led-d${unit}" class="inline-lamp led-gray" src="/share/svg/led-beacon.svg"/>
|
||||
Downlink
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td colspan=100><hr></td></tr>
|
||||
`;
|
||||
}
|
||||
content += `
|
||||
<tr>
|
||||
<td>Port:</td>
|
||||
<td class="value">${__formatPort(model, port)}</td>
|
||||
<td> </td>
|
||||
<td>
|
||||
<div class="buttons-row">
|
||||
<button id="__switch-port-button-p${port}" title="Activate this port">
|
||||
<img id="__switch-port-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-circle.svg"/>
|
||||
</button>
|
||||
<button id="__switch-params-button-p${port}" title="Configure this port">
|
||||
<img id="__switch-params-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-gear.svg"/>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
id="__switch-custom-edid-p${port}" style="visibility:hidden"
|
||||
title="A non-default EDID is used on this port"
|
||||
>
|
||||
⚹
|
||||
</span>
|
||||
|
||||
${pa.name.length > 0 ? tools.escape(pa.name) : ("Host " + (port + 1))}
|
||||
|
||||
</td>
|
||||
<td style="font-size:1em">
|
||||
<button id="__switch-beacon-button-p${port}" class="small" title="Toggle Beacon Led on this port">
|
||||
<img id="__switch-beacon-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-beacon.svg"/>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<img id="__switch-video-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-video.svg" title="Video Link"/>
|
||||
<img id="__switch-usb-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-usb.svg" title="USB Link"/>
|
||||
<img id="__switch-atx-power-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-atx-power.svg" title="Power Led"/>
|
||||
<img id="__switch-atx-hdd-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-atx-hdd.svg" title="HDD Led"/>
|
||||
</td>
|
||||
<td>
|
||||
<div class="buttons-row">
|
||||
<button id="__switch-atx-power-button-p${port}" class="small">Power <sup><i>short</i></sup></button>
|
||||
<button id="__switch-atx-power-long-button-p${port}" class="small"><sup><i>long</i></sup></button>
|
||||
<button id="__switch-atx-reset-button-p${port}" class="small">Reset</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
$("switch-chain").innerHTML = content;
|
||||
|
||||
if (model.units.length > 0) {
|
||||
tools.hidden.setVisible($("switch-message-update"), (model.firmware.version > model.units[0].firmware.version));
|
||||
}
|
||||
|
||||
for (let unit = 0; unit < model.units.length; ++unit) {
|
||||
tools.el.setOnClick($(`__switch-beacon-button-u${unit}`), tools.partial(__switchUplinkBeacon, unit));
|
||||
tools.el.setOnClick($(`__switch-beacon-button-d${unit}`), tools.partial(__switchDownlinkBeacon, unit));
|
||||
}
|
||||
|
||||
for (let port = 0; port < model.ports.length; ++port) {
|
||||
tools.el.setOnClick($(`__switch-port-button-p${port}`), tools.partial(__switchActivePort, port));
|
||||
tools.el.setOnClick($(`__switch-params-button-p${port}`), tools.partial(__showParamsDialog, port));
|
||||
tools.el.setOnClick($(`__switch-beacon-button-p${port}`), tools.partial(__switchPortBeacon, port));
|
||||
tools.el.setOnClick($(`__switch-atx-power-button-p${port}`), tools.partial(__atxClick, port, "power"));
|
||||
tools.el.setOnClick($(`__switch-atx-power-long-button-p${port}`), tools.partial(__atxClick, port, "power_long"));
|
||||
tools.el.setOnClick($(`__switch-atx-reset-button-p${port}`), tools.partial(__atxClick, port, "reset"));
|
||||
}
|
||||
|
||||
__setPowerLedState($("switch-atx-power-led"), false, false);
|
||||
__setLedState($("switch-atx-hdd-led"), "red", false);
|
||||
|
||||
__state.model = model;
|
||||
};
|
||||
|
||||
var __showParamsDialog = function(port) {
|
||||
if (!__state || !__state.model || !__state.edids) {
|
||||
return;
|
||||
}
|
||||
|
||||
let model = __state.model;
|
||||
let edids = __state.edids;
|
||||
|
||||
let atx_actions = {
|
||||
"power": "ATX power click",
|
||||
"power_long": "Power long",
|
||||
"reset": "Reset click",
|
||||
};
|
||||
|
||||
let add_edid_option = function(el, attrs, id) {
|
||||
tools.selector.addOption(el, attrs.name, id, (edids.used[port] === id));
|
||||
if (attrs.parsed !== null) {
|
||||
let parsed = attrs.parsed;
|
||||
let text = "\xA0\xA0\xA0\xA0\xA0\u2570 ";
|
||||
text += (parsed.monitor_name !== null ? parsed.monitor_name : parsed.mfc_id);
|
||||
text += (parsed.audio ? "; +Audio" : "; -Audio");
|
||||
tools.selector.addComment(el, text);
|
||||
}
|
||||
};
|
||||
|
||||
let create_content = function(el_parent) {
|
||||
let html = `
|
||||
<table>
|
||||
<tr>
|
||||
<td>Port name:</td>
|
||||
<td><input
|
||||
type="text" autocomplete="off" id="__switch-port-name-input"
|
||||
value="${tools.escape(model.ports[port].name)}" placeholder="Host ${port + 1}"
|
||||
style="width:100%"
|
||||
/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>EDID:</td>
|
||||
<td><select id="__switch-port-edid-selector" style="width: 100%"></select></td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr>
|
||||
<table>
|
||||
`;
|
||||
for (let kv of Object.entries(atx_actions)) {
|
||||
html += `
|
||||
<tr>
|
||||
<td style="white-space: nowrap">${tools.escape(kv[1])}:</td>
|
||||
<td style="width: 100%"><input type="range" id="__switch-port-atx-click-${kv[0]}-delay-slider"/></td>
|
||||
<td id="__switch-port-atx-click-${kv[0]}-delay-value"></td>
|
||||
<td> </td>
|
||||
<td><button
|
||||
id="__switch-port-atx-click-${kv[0]}-delay-default-button"
|
||||
class="small" title="Reset default"
|
||||
>↻</button></td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
html += "</table>";
|
||||
el_parent.innerHTML = html;
|
||||
|
||||
let el_selector = $("__switch-port-edid-selector");
|
||||
add_edid_option(el_selector, edids.all["default"], "default");
|
||||
for (let kv of Object.entries(edids.all)) {
|
||||
if (kv[0] !== "default") {
|
||||
tools.selector.addSeparator(el_selector, 20);
|
||||
add_edid_option(el_selector, kv[1], kv[0]);
|
||||
}
|
||||
}
|
||||
|
||||
for (let action of Object.keys(atx_actions)) {
|
||||
let limits = model.limits.atx.click_delays[action];
|
||||
let el_slider = $(`__switch-port-atx-click-${action}-delay-slider`);
|
||||
let display_value = tools.partial(function(action, value) {
|
||||
$(`__switch-port-atx-click-${action}-delay-value`).innerText = `${value.toFixed(1)}`;
|
||||
}, action);
|
||||
let reset_default = tools.partial(function(el_slider, limits) {
|
||||
tools.slider.setValue(el_slider, limits["default"]);
|
||||
}, el_slider, limits);
|
||||
tools.slider.setParams(el_slider, limits.min, limits.max, 0.5, model.ports[port].atx.click_delays[action], display_value);
|
||||
tools.el.setOnClick($(`__switch-port-atx-click-${action}-delay-default-button`), reset_default);
|
||||
}
|
||||
};
|
||||
|
||||
wm.modal(`Port ${__formatPort(__state.model, port)} settings`, create_content, true, true).then(function(ok) {
|
||||
if (ok) {
|
||||
let params = {
|
||||
"port": port,
|
||||
"edid_id": $("__switch-port-edid-selector").value,
|
||||
"name": $("__switch-port-name-input").value,
|
||||
};
|
||||
for (let action of Object.keys(atx_actions)) {
|
||||
params[`atx_click_${action}_delay`] = tools.slider.getValue($(`__switch-port-atx-click-${action}-delay-slider`));
|
||||
};
|
||||
__sendPost("/api/switch/set_port_params", params);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var __formatPort = function(model, port) {
|
||||
if (model.units.length > 1) {
|
||||
return `${model.ports[port].unit + 1}.${model.ports[port].channel + 1}`;
|
||||
} else {
|
||||
return `${port + 1}`;
|
||||
}
|
||||
};
|
||||
|
||||
var __setLedState = function(el, color, on) {
|
||||
el.classList.toggle(`led-${color}`, on);
|
||||
el.classList.toggle("led-gray", !on);
|
||||
};
|
||||
|
||||
var __setPowerLedState = function(el, power, busy) {
|
||||
el.classList.toggle("led-green", (power && !busy));
|
||||
el.classList.toggle("led-yellow", busy);
|
||||
el.classList.toggle("led-gray", !(power || busy));
|
||||
};
|
||||
|
||||
var __switchActivePort = function(port) {
|
||||
if (__msd_connected) {
|
||||
wm.error(`
|
||||
Oops! Before port switching, please disconnect an active Mass Storage Drive image first.
|
||||
Otherwise, it will break a current USB operation (OS installation, Live CD, or whatever).
|
||||
`);
|
||||
} else {
|
||||
__sendPost("/api/switch/set_active", {"port": port});
|
||||
}
|
||||
};
|
||||
|
||||
var __switchUplinkBeacon = function(unit) {
|
||||
let state = false;
|
||||
try { state = !__state.beacons.uplinks[unit]; } catch {}; // eslint-disable-line no-empty
|
||||
__sendPost("/api/switch/set_beacon", {"uplink": unit, "state": state});
|
||||
};
|
||||
|
||||
var __switchDownlinkBeacon = function(unit) {
|
||||
let state = false;
|
||||
try { state = !__state.beacons.downlinks[unit]; } catch {}; // eslint-disable-line no-empty
|
||||
__sendPost("/api/switch/set_beacon", {"downlink": unit, "state": state});
|
||||
};
|
||||
|
||||
var __switchPortBeacon = function(port) {
|
||||
let state = false;
|
||||
try { state = !__state.beacons.ports[port]; } catch {}; // eslint-disable-line no-empty
|
||||
__sendPost("/api/switch/set_beacon", {"port": port, "state": state});
|
||||
};
|
||||
|
||||
var __atxClick = function(port, button) {
|
||||
let click_button = function() {
|
||||
__sendPost("/api/switch/atx/click", {"port": port, "button": button});
|
||||
};
|
||||
if ($("switch-atx-ask-switch").checked) {
|
||||
wm.confirm(`
|
||||
Are you sure you want to press the <b>${button}</b> button?<br>
|
||||
Warning! This could cause data loss on the server.
|
||||
`).then(function(ok) {
|
||||
if (ok) {
|
||||
click_button();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
click_button();
|
||||
}
|
||||
};
|
||||
|
||||
var __sendPost = function(url, params, error_callback=null) {
|
||||
tools.httpPost(url, params, function(http) {
|
||||
if (http.status !== 200) {
|
||||
if (error_callback) {
|
||||
error_callback();
|
||||
}
|
||||
wm.error("Switch error", http.responseText);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
__init__();
|
||||
}
|
||||
@@ -78,7 +78,7 @@ export var tools = new function() {
|
||||
};
|
||||
|
||||
self.partial = function(func, ...args) {
|
||||
return () => func(...args);
|
||||
return (...rest) => func(...args, ...rest);
|
||||
};
|
||||
|
||||
self.upperFirst = function(text) {
|
||||
@@ -104,10 +104,6 @@ export var tools = new function() {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
};
|
||||
|
||||
self.formatHex = function(value) {
|
||||
return `0x${value.toString(16).toUpperCase()}`;
|
||||
};
|
||||
|
||||
self.formatSize = function(size) {
|
||||
if (size > 0) {
|
||||
let index = Math.floor( Math.log(size) / Math.log(1024) );
|
||||
|
||||
Reference in New Issue
Block a user