diff --git a/Makefile b/Makefile index 81191e1b..80e1eaaa 100644 --- a/Makefile +++ b/Makefile @@ -331,7 +331,7 @@ run-nogpio: testenv && cp /usr/share/kvmd/configs.default/kvmd/*.yaml /etc/kvmd \ && cp /usr/share/kvmd/configs.default/kvmd/*passwd /etc/kvmd \ && cp /usr/share/kvmd/configs.default/kvmd/*.secret /etc/kvmd \ - && cp /usr/share/kvmd/configs.default/kvmd/main/$(if $(P),$(P),$(DEFAULT_PLATFORM)).yaml /etc/kvmd/main.yaml \ + && cp /usr/share/kvmd/configs.default/kvmd/main.yaml /etc/kvmd/main.yaml \ && ln -s /testenv/web.css /etc/kvmd/web.css \ && mkdir -p /etc/kvmd/override.d \ && cp /testenv/$(if $(P),$(P),$(DEFAULT_PLATFORM)).override.yaml /etc/kvmd/override.yaml \ diff --git a/testenv/v2-hdmiusb-rpi4.override.yaml b/testenv/v2-hdmiusb-rpi4.override.yaml index d46597bd..fe75b420 100644 --- a/testenv/v2-hdmiusb-rpi4.override.yaml +++ b/testenv/v2-hdmiusb-rpi4.override.yaml @@ -18,7 +18,7 @@ kvmd: streamer: cmd: - "/usr/bin/ustreamer" - - "--device=/dev/kvmd-video" + - "--device=/dev/video0" - "--persistent" - "--format=mjpeg" - "--resolution={resolution}" diff --git a/web/kvm/index.html b/web/kvm/index.html index 597671ad..a5722260 100644 --- a/web/kvm/index.html +++ b/web/kvm/index.html @@ -259,6 +259,11 @@ +
Video Record
Record video using the browser API, and will be downloaded automatically
+
+ + +

@@ -897,6 +902,7 @@ +
diff --git a/web/kvm/navbar-system.pug b/web/kvm/navbar-system.pug index b6bfa96d..bdfd58c2 100644 --- a/web/kvm/navbar-system.pug +++ b/web/kvm/navbar-system.pug @@ -71,6 +71,12 @@ li(id="system-dropdown" class="right") button(data-force-hide-menu data-show-window="stream-window" class="row33" i18n="kvm_text20") • Show stream button(data-force-hide-menu id="stream-screenshot-button" class="row33" i18n="kvm_text21") • Screenshot button(id="stream-reset-button" class="row33" i18n="kvm_text22") Reset stream + div(class="text") + b(i18n="kvm_text79") Video Record#[br] + sub(i18n="kvm_text80") Record video using the browser API, and will be downloaded automatically + div(class="buttons buttons-row") + button(data-force-hide-menu id="stream-record-start-button" class="row50" i18n="kvm_text81") • Start recording + button(data-force-hide-menu id="stream-record-stop-button" class="row50" i18n="kvm_text82") • End recording div(id="hid-outputs" class="feature-disabled") hr table(class="kv") diff --git a/web/kvm/window-stream.pug b/web/kvm/window-stream.pug index 7f48c26c..0bf317c5 100644 --- a/web/kvm/window-stream.pug +++ b/web/kvm/window-stream.pug @@ -46,3 +46,4 @@ div(id="stream-window" class="window window-resizable") div(class="label") Up div(data-code="down" class="key small rounded-right") div(class="label") Down + canvas(id="stream-mjpeg-canvas" class="hidden") diff --git a/web/share/i18n/i18n_en.json b/web/share/i18n/i18n_en.json index 5e239deb..aa1d01a7 100644 --- a/web/share/i18n/i18n_en.json +++ b/web/share/i18n/i18n_en.json @@ -109,6 +109,10 @@ "kvm_text76":"Connect drive to Server", "kvm_text77":"Disconnect", "kvm_text78":"Reset", + "kvm_text79":"Video Record
", + "kvm_text80":"Record video using the browser API, and will be downloaded automatically", + "kvm_text81":"Start recording", + "kvm_text82":"End recording", "atx-ask-switch":"Ask click confirmation", "hid-recorder-loop-switch":"Infinite loop playback", diff --git a/web/share/i18n/i18n_zh.json b/web/share/i18n/i18n_zh.json index 811eb4d1..99c67421 100644 --- a/web/share/i18n/i18n_zh.json +++ b/web/share/i18n/i18n_zh.json @@ -109,6 +109,10 @@ "kvm_text76":"连接 MSD 到主机", "kvm_text77":"断开连接", "kvm_text78":"重置", + "kvm_text79":"视频录制
", + "kvm_text80":"使用浏览器 API 录制视频(无音频),结束录制后视频文件会自动下载", + "kvm_text81":"开始录制", + "kvm_text82":"结束录制", "atx-ask-switch":"点击二次确认", "hid-recorder-loop-switch":"无限循环重放", diff --git a/web/share/js/kvm/stream.js b/web/share/js/kvm/stream.js index b811a657..cfb85627 100644 --- a/web/share/js/kvm/stream.js +++ b/web/share/js/kvm/stream.js @@ -97,9 +97,15 @@ export function Streamer() { tools.el.setOnClick($("stream-screenshot-button"), __clickScreenshotButton); tools.el.setOnClick($("stream-reset-button"), __clickResetButton); + tools.el.setOnClick($("stream-record-start-button"), __clickRecordStartButton); + tools.el.setOnClick($("stream-record-stop-button"), __clickRecordStopButton); + $("stream-window").show_hook = () => __applyState(__state); $("stream-window").close_hook = () => __applyState(null); + + //hidden stream-record-stop-button + document.getElementById('stream-record-stop-button').disabled = true; }; /************************************************************************/ @@ -304,6 +310,77 @@ export function Streamer() { }); }; + + var stream_mjpeg_refresh_img; + var stream_now_fps + let mediaRecorder; + var __clickRecordStartButton = function() { + wm.confirm("Are you sure you want to record stream?").then(function (ok) { + if (ok) { + stream_now_fps = tools.slider.getValue($("stream-desired-fps-slider")); + let recordedBlobs = []; + //"mjpeg" or "janus" + let stream_type = document.querySelector('input[name="stream-mode-radio"]:checked').value; + if ( stream_type == "mjpeg"){ + + var stream_mjpeg_img = document.getElementById('stream-image'); + var stream_mjpeg_canvas = document.getElementById('stream-mjpeg-canvas'); + var ctx = stream_mjpeg_canvas.getContext('2d'); + stream_mjpeg_canvas.width = stream_mjpeg_img.width; + stream_mjpeg_canvas.height = stream_mjpeg_img.height; + const stream = stream_mjpeg_canvas.captureStream(stream_now_fps); // Capture FPS + mediaRecorder = new MediaRecorder(stream); + }else{ + const stream = document.getElementById("stream-video") + stream.captureStream = stream.captureStream || stream.mozCaptureStream; + mediaRecorder = new MediaRecorder(stream.captureStream()); + } + + + mediaRecorder.ondataavailable = function(event) { + if (event.data && event.data.size > 0) { + recordedBlobs.push(event.data); + } + }; + + mediaRecorder.onstop = function() { + const blob = new Blob(recordedBlobs, {type: 'video/webm'}); + var url = URL.createObjectURL(blob); + const a = document.createElement('a'); + document.body.appendChild(a); + const now = new Date(); + const year = now.getFullYear(); + const month = ('0' + (now.getMonth() + 1)).slice(-2); + const day = ('0' + now.getDate()).slice(-2); + const hours = ('0' + now.getHours()).slice(-2); + const minutes = ('0' + now.getMinutes()).slice(-2); + const seconds = ('0' + now.getSeconds()).slice(-2);//Get now time + a.style = "display: none"; + a.href = url; + a.download = stream_type +"_"+ year + month + day + hours + minutes + seconds + ".webm"; + a.click(); + window.URL.revokeObjectURL(url); + }; + + mediaRecorder.start(); + document.getElementById('stream-record-start-button').disabled = true; + document.getElementById('stream-record-stop-button').disabled = false; + if (stream_type == "mjpeg"){ + stream_mjpeg_refresh_img = setInterval(function() { + ctx.drawImage(stream_mjpeg_img, 0, 0, stream_mjpeg_img.width, stream_mjpeg_img.height); + }, 1000 / stream_now_fps); + } + } + }); + }; + + var __clickRecordStopButton = function() { + mediaRecorder.stop(); + clearInterval(stream_mjpeg_refresh_img); + document.getElementById('stream-record-start-button').disabled = false; + document.getElementById('stream-record-stop-button').disabled = true; + }; + var __sendParam = function(name, value) { tools.httpPost(`/api/streamer/set_params?${name}=${value}`, function(http) { if (http.status !== 200) {