mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-12 01:00:29 +08:00
pikvm/pikvm#1440: Websocket-based transport and decoding for H.264
This commit is contained in:
parent
eda7ab3a49
commit
ab08d823c4
@ -1,6 +1,6 @@
|
|||||||
location /media/ws {
|
location /api/media/ws {
|
||||||
rewrite ^/media/ws$ /ws break;
|
rewrite ^/api/media/ws$ /ws break;
|
||||||
rewrite ^/media/ws\?(.*)$ /ws?$1 break;
|
rewrite ^/api/media/ws\?(.*)$ /ws?$1 break;
|
||||||
proxy_pass http://media;
|
proxy_pass http://media;
|
||||||
include /etc/kvmd/nginx/loc-proxy.conf;
|
include /etc/kvmd/nginx/loc-proxy.conf;
|
||||||
include /etc/kvmd/nginx/loc-websocket.conf;
|
include /etc/kvmd/nginx/loc-websocket.conf;
|
||||||
|
|||||||
@ -102,6 +102,12 @@ EOF
|
|||||||
touch -t 200701011000 /etc/fstab
|
touch -t 200701011000 /etc/fstab
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "$(vercmp "$2" 4.29)" -lt 0 ]]; then
|
||||||
|
if [ "$(systemctl is-enabled kvmd-janus || true)" = enabled ]; then
|
||||||
|
systemctl enable kvmd-media || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Some update deletes /etc/motd, WTF
|
# Some update deletes /etc/motd, WTF
|
||||||
# shellcheck disable=SC2015,SC2166
|
# shellcheck disable=SC2015,SC2166
|
||||||
[ ! -f /etc/motd -a -f /etc/motd.pacsave ] && mv /etc/motd.pacsave /etc/motd || true
|
[ ! -f /etc/motd -a -f /etc/motd.pacsave ] && mv /etc/motd.pacsave /etc/motd || true
|
||||||
|
|||||||
@ -170,6 +170,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="hidden" id="stream-message-no-vd">
|
||||||
|
<div class="text">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2"><img class="sign " src="/share/svg/warning.svg"></td>
|
||||||
|
<td style="line-height:1.5"><b>Direct HTTP H.264 streaming is not supported</b></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
<div class="hidden" id="stream-message-no-h264">
|
<div class="hidden" id="stream-message-no-h264">
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<table>
|
<table>
|
||||||
@ -220,10 +231,12 @@
|
|||||||
<td>Video <a target="_blank" href="https://docs.pikvm.org/webrtc">mode</a>:</td>
|
<td>Video <a target="_blank" href="https://docs.pikvm.org/webrtc">mode</a>:</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="radio-box">
|
<div class="radio-box">
|
||||||
<input checked type="radio" id="stream-mode-radio-mjpeg" name="stream-mode-radio" value="mjpeg">
|
|
||||||
<label for="stream-mode-radio-mjpeg">MJPEG / HTTP</label>
|
|
||||||
<input type="radio" id="stream-mode-radio-janus" name="stream-mode-radio" value="janus">
|
<input type="radio" id="stream-mode-radio-janus" name="stream-mode-radio" value="janus">
|
||||||
<label for="stream-mode-radio-janus">H.264 / WebRTC</label>
|
<label for="stream-mode-radio-janus">WebRTC</label>
|
||||||
|
<input type="radio" id="stream-mode-radio-media" name="stream-mode-radio" value="media">
|
||||||
|
<label for="stream-mode-radio-media">H.264</label>
|
||||||
|
<input checked type="radio" id="stream-mode-radio-mjpeg" name="stream-mode-radio" value="mjpeg">
|
||||||
|
<label for="stream-mode-radio-mjpeg">MJPEG</label>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -914,6 +927,7 @@
|
|||||||
<button class="window-button-exit-full-tab">▼</button>
|
<button class="window-button-exit-full-tab">▼</button>
|
||||||
<div class="stream-box-offline" id="stream-box"><img id="stream-image" src="/share/png/blank-stream.png">
|
<div class="stream-box-offline" id="stream-box"><img id="stream-image" src="/share/png/blank-stream.png">
|
||||||
<video class="hidden" id="stream-video" disablePictureInPicture="true" autoplay playsinline muted></video>
|
<video class="hidden" id="stream-video" disablePictureInPicture="true" autoplay playsinline muted></video>
|
||||||
|
<canvas class="hidden" id="stream-canvas"></canvas>
|
||||||
<div id="stream-fullscreen-active"></div>
|
<div id="stream-fullscreen-active"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="keypad" id="stream-mouse-buttons" align="center">
|
<div class="keypad" id="stream-mouse-buttons" align="center">
|
||||||
|
|||||||
@ -19,6 +19,9 @@ li(id="system-dropdown" class="right")
|
|||||||
div(id="stream-message-no-webrtc" class="hidden")
|
div(id="stream-message-no-webrtc" class="hidden")
|
||||||
+menu_message("warning", "WebRTC is not supported by this browser")
|
+menu_message("warning", "WebRTC is not supported by this browser")
|
||||||
hr
|
hr
|
||||||
|
div(id="stream-message-no-vd" class="hidden")
|
||||||
|
+menu_message("warning", "Direct HTTP H.264 streaming is not supported")
|
||||||
|
hr
|
||||||
div(id="stream-message-no-h264" class="hidden")
|
div(id="stream-message-no-h264" class="hidden")
|
||||||
+menu_message("warning", "H.264 is not supported by this browser")
|
+menu_message("warning", "H.264 is not supported by this browser")
|
||||||
hr
|
hr
|
||||||
@ -46,10 +49,12 @@ li(id="system-dropdown" class="right")
|
|||||||
td Video #[a(target="_blank" href="https://docs.pikvm.org/webrtc") mode]:
|
td Video #[a(target="_blank" href="https://docs.pikvm.org/webrtc") mode]:
|
||||||
td
|
td
|
||||||
div(class="radio-box")
|
div(class="radio-box")
|
||||||
input(checked type="radio" id="stream-mode-radio-mjpeg" name="stream-mode-radio" value="mjpeg")
|
|
||||||
label(for="stream-mode-radio-mjpeg") MJPEG / HTTP
|
|
||||||
input(type="radio" id="stream-mode-radio-janus" name="stream-mode-radio" value="janus")
|
input(type="radio" id="stream-mode-radio-janus" name="stream-mode-radio" value="janus")
|
||||||
label(for="stream-mode-radio-janus") H.264 / WebRTC
|
label(for="stream-mode-radio-janus") WebRTC
|
||||||
|
input(type="radio" id="stream-mode-radio-media" name="stream-mode-radio" value="media")
|
||||||
|
label(for="stream-mode-radio-media") H.264
|
||||||
|
input(checked type="radio" id="stream-mode-radio-mjpeg" name="stream-mode-radio" value="mjpeg")
|
||||||
|
label(for="stream-mode-radio-mjpeg") MJPEG
|
||||||
tr(id="stream-orient" class="feature-disabled")
|
tr(id="stream-orient" class="feature-disabled")
|
||||||
td Orientation:
|
td Orientation:
|
||||||
td
|
td
|
||||||
|
|||||||
@ -16,6 +16,7 @@ div(id="stream-window" class="window window-resizable")
|
|||||||
div(id="stream-box" class="stream-box-offline")
|
div(id="stream-box" class="stream-box-offline")
|
||||||
img(id="stream-image" src=`${png_dir}/blank-stream.png`)
|
img(id="stream-image" src=`${png_dir}/blank-stream.png`)
|
||||||
video(id="stream-video" class="hidden" disablePictureInPicture="true" autoplay playsinline muted)
|
video(id="stream-video" class="hidden" disablePictureInPicture="true" autoplay playsinline muted)
|
||||||
|
canvas(id="stream-canvas" class="hidden")
|
||||||
div(id="stream-fullscreen-active")
|
div(id="stream-fullscreen-active")
|
||||||
|
|
||||||
div(id="stream-mouse-buttons" class="keypad" align="center")
|
div(id="stream-mouse-buttons" class="keypad" align="center")
|
||||||
|
|||||||
@ -85,7 +85,8 @@ div.stream-box-mouse-none {
|
|||||||
}
|
}
|
||||||
|
|
||||||
img#stream-image,
|
img#stream-image,
|
||||||
video#stream-video {
|
video#stream-video,
|
||||||
|
canvas#stream-canvas {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import {tools, $} from "../tools.js";
|
|||||||
import {wm} from "../wm.js";
|
import {wm} from "../wm.js";
|
||||||
|
|
||||||
import {JanusStreamer} from "./stream_janus.js";
|
import {JanusStreamer} from "./stream_janus.js";
|
||||||
|
import {MediaStreamer} from "./stream_media.js";
|
||||||
import {MjpegStreamer} from "./stream_mjpeg.js";
|
import {MjpegStreamer} from "./stream_mjpeg.js";
|
||||||
|
|
||||||
|
|
||||||
@ -168,17 +169,20 @@ export function Streamer() {
|
|||||||
if (state.features) {
|
if (state.features) {
|
||||||
let f = state.features;
|
let f = state.features;
|
||||||
let l = state.limits;
|
let l = state.limits;
|
||||||
let has_webrtc = JanusStreamer.is_webrtc_available();
|
let sup_h264 = $("stream-video").canPlayType("video/mp4; codecs=\"avc1.42E01F\"");
|
||||||
let has_h264 = JanusStreamer.is_h264_available();
|
let sup_vd = MediaStreamer.is_videodecoder_available();
|
||||||
let has_janus = (__janus_imported && f.h264 && has_webrtc); // Don't check has_h264 for sure
|
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(
|
tools.info(
|
||||||
`Stream: Janus WebRTC state: features.h264=${f.h264},`
|
`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-webrtc"), __janus_imported && f.h264 && !sup_webrtc);
|
||||||
tools.hidden.setVisible($("stream-message-no-h264"), __janus_imported && f.h264 && !has_h264);
|
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);
|
tools.slider.setRange($("stream-desired-fps-slider"), l.desired_fps.min, l.desired_fps.max);
|
||||||
if (f.resolution) {
|
if (f.resolution) {
|
||||||
@ -190,21 +194,27 @@ export function Streamer() {
|
|||||||
} else {
|
} else {
|
||||||
$("stream-resolution-selector").options.length = 0;
|
$("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-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.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-quality"), f.quality); // Only on s.encoder.quality
|
||||||
tools.feature.setEnabled($("stream-resolution"), f.resolution);
|
tools.feature.setEnabled($("stream-resolution"), f.resolution);
|
||||||
tools.feature.setEnabled($("stream-h264-bitrate"), has_janus);
|
tools.feature.setEnabled($("stream-h264-bitrate"), f.h264);
|
||||||
tools.feature.setEnabled($("stream-h264-gop"), has_janus);
|
tools.feature.setEnabled($("stream-h264-gop"), f.h264);
|
||||||
tools.feature.setEnabled($("stream-mode"), has_janus);
|
tools.feature.setEnabled($("stream-mode"), f.h264);
|
||||||
if (!has_janus) {
|
if (!f.h264) {
|
||||||
tools.feature.setEnabled($("stream-audio"), false);
|
tools.feature.setEnabled($("stream-audio"), 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);
|
tools.radio.clickValue("stream-mode-radio", mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,8 +295,12 @@ export function Streamer() {
|
|||||||
// Firefox doesn't support RTP orientation:
|
// Firefox doesn't support RTP orientation:
|
||||||
// - https://bugzilla.mozilla.org/show_bug.cgi?id=1316448
|
// - https://bugzilla.mozilla.org/show_bug.cgi?id=1316448
|
||||||
tools.feature.setEnabled($("stream-orient"), !tools.browser.is_firefox);
|
tools.feature.setEnabled($("stream-orient"), !tools.browser.is_firefox);
|
||||||
} else { // mjpeg
|
} else {
|
||||||
__streamer = new MjpegStreamer(__setActive, __setInactive, __setInfo);
|
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-orient"), false);
|
||||||
tools.feature.setEnabled($("stream-audio"), false); // Enabling in stream_janus.js
|
tools.feature.setEnabled($("stream-audio"), false); // Enabling in stream_janus.js
|
||||||
}
|
}
|
||||||
@ -299,7 +313,8 @@ export function Streamer() {
|
|||||||
let mode = tools.radio.getValue("stream-mode-radio");
|
let mode = tools.radio.getValue("stream-mode-radio");
|
||||||
tools.storage.set("stream.mode", mode);
|
tools.storage.set("stream.mode", mode);
|
||||||
if (mode !== __streamer.getMode()) {
|
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"));
|
tools.hidden.setVisible($("stream-video"), (mode === "janus"));
|
||||||
__resetStream(mode);
|
__resetStream(mode);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,8 @@ var _Janus = null;
|
|||||||
export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, __allow_audio) {
|
export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, __allow_audio) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
/************************************************************************/
|
||||||
|
|
||||||
var __stop = false;
|
var __stop = false;
|
||||||
var __ensuring = false;
|
var __ensuring = false;
|
||||||
|
|
||||||
@ -45,10 +47,12 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
|||||||
var __state = null;
|
var __state = null;
|
||||||
var __frames = 0;
|
var __frames = 0;
|
||||||
|
|
||||||
|
/************************************************************************/
|
||||||
|
|
||||||
self.getOrientation = () => __orient;
|
self.getOrientation = () => __orient;
|
||||||
self.isAudioAllowed = () => __allow_audio;
|
self.isAudioAllowed = () => __allow_audio;
|
||||||
|
|
||||||
self.getName = () => (__allow_audio ? "H.264 + Audio" : "H.264");
|
self.getName = () => (__allow_audio ? "WebRTC H.264 + Audio" : "WebRTC H.264");
|
||||||
self.getMode = () => "janus";
|
self.getMode = () => "janus";
|
||||||
|
|
||||||
self.getResolution = function() {
|
self.getResolution = function() {
|
||||||
@ -75,9 +79,9 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
|||||||
|
|
||||||
var __ensureJanus = function(internal) {
|
var __ensureJanus = function(internal) {
|
||||||
if (__janus === null && !__stop && (!__ensuring || internal)) {
|
if (__janus === null && !__stop && (!__ensuring || internal)) {
|
||||||
|
__ensuring = true;
|
||||||
__setInactive();
|
__setInactive();
|
||||||
__setInfo(false, false, "");
|
__setInfo(false, false, "");
|
||||||
__ensuring = true;
|
|
||||||
__logInfo("Starting Janus ...");
|
__logInfo("Starting Janus ...");
|
||||||
__janus = new _Janus({
|
__janus = new _Janus({
|
||||||
"server": `${tools.is_https ? "wss" : "ws"}://${location.host}/janus/ws`,
|
"server": `${tools.is_https ? "wss" : "ws"}://${location.host}/janus/ws`,
|
||||||
@ -447,11 +451,3 @@ JanusStreamer.ensure_janus = function(callback) {
|
|||||||
JanusStreamer.is_webrtc_available = function() {
|
JanusStreamer.is_webrtc_available = function() {
|
||||||
return !!window.RTCPeerConnection;
|
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;
|
|
||||||
};
|
|
||||||
|
|||||||
240
web/share/js/kvm/stream_media.js
Normal file
240
web/share/js/kvm/stream_media.js
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
# #
|
||||||
|
# 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 __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;
|
||||||
|
};
|
||||||
@ -41,7 +41,7 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) {
|
|||||||
|
|
||||||
/************************************************************************/
|
/************************************************************************/
|
||||||
|
|
||||||
self.getName = () => "MJPEG";
|
self.getName = () => "HTTP MJPEG";
|
||||||
self.getMode = () => "mjpeg";
|
self.getMode = () => "mjpeg";
|
||||||
|
|
||||||
self.getResolution = function() {
|
self.getResolution = function() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user