#44 添加视频录制支持

使用浏览器前端 API
支持 mjpeg 和 h.264 模式下的视频录制
录制格式为wbem(vp8)
This commit is contained in:
mofeng-git 2024-11-04 13:25:18 +00:00
parent 6fbfc2b343
commit 1a13760df0
8 changed files with 100 additions and 2 deletions

View File

@ -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 \

View File

@ -18,7 +18,7 @@ kvmd:
streamer:
cmd:
- "/usr/bin/ustreamer"
- "--device=/dev/kvmd-video"
- "--device=/dev/video0"
- "--persistent"
- "--format=mjpeg"
- "--resolution={resolution}"

View File

@ -259,6 +259,11 @@
<button class="row33" data-force-hide-menu id="stream-screenshot-button" i18n="kvm_text21">&bull; Screenshot</button>
<button class="row33" id="stream-reset-button" i18n="kvm_text22">Reset stream</button>
</div>
<div class="text"><b i18n="kvm_text79">Video Record<br></b><sub i18n="kvm_text80">Record video using the browser API, and will be downloaded automatically</sub></div>
<div class="buttons buttons-row">
<button class="row50" data-force-hide-menu id="stream-record-start-button" i18n="kvm_text81">&bull; Start recording</button>
<button class="row50" data-force-hide-menu id="stream-record-stop-button" i18n="kvm_text82">&bull; End recording</button>
</div>
<div class="feature-disabled" id="hid-outputs">
<hr>
<table class="kv">
@ -897,6 +902,7 @@
</div>
</div>
</div>
<canvas class="hidden" id="stream-mjpeg-canvas"></canvas>
</div>
<div class="window" id="keyboard-window">
<div class="window-header" id="keyboard-window-header">

View File

@ -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") &bull; Show stream
button(data-force-hide-menu id="stream-screenshot-button" class="row33" i18n="kvm_text21") &bull; 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") &bull; Start recording
button(data-force-hide-menu id="stream-record-stop-button" class="row50" i18n="kvm_text82") &bull; End recording
div(id="hid-outputs" class="feature-disabled")
hr
table(class="kv")

View File

@ -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")

View File

@ -109,6 +109,10 @@
"kvm_text76":"Connect drive to Server",
"kvm_text77":"Disconnect",
"kvm_text78":"Reset",
"kvm_text79":"Video Record<br>",
"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",

View File

@ -109,6 +109,10 @@
"kvm_text76":"连接 MSD 到主机",
"kvm_text77":"断开连接",
"kvm_text78":"重置",
"kvm_text79":"视频录制<br>",
"kvm_text80":"使用浏览器 API 录制视频(无音频),结束录制后视频文件会自动下载",
"kvm_text81":"开始录制",
"kvm_text82":"结束录制",
"atx-ask-switch":"点击二次确认",
"hid-recorder-loop-switch":"无限循环重放",

View File

@ -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) {