副標題:iREC 一款基于瀏覽器JavaScript的螢屏錄制工具
背景
周末,公司里的測驗小妹給我發訊息說,她昨晚又加班到很晚,原因是研發要求提復雜bug時需要附上具體的操作流程以便詳細了解操作程序和復現,最好能提供一個錄制視頻,這不是難為我們測驗小妹嘛?隨后她問我有沒有好用,免費的錄制螢屏的軟體,我答應幫她找找,
看到這里你可能以為這是一篇軟體推薦文章,但其實這是一篇造輪子的文章,經過一番搜索,我發現大多數的錄屏軟體,不是比較笨重,就是有些需要付費,或者無法跨平臺使用,于是我想能不能自己開發一個錄屏工具,這個想法一旦產生就無法停止,在造輪子之前我需要簡單整理一下需求范圍,以便挑選合適的工具來實作,
需求如下
實作一個錄屏工具或軟體,能夠錄制整個螢屏,最低要求是能夠錄制瀏覽器的操作,該軟體有一個開始錄制的按鈕,點擊后開始錄制,按鈕變成停止按鈕,再次點擊按鈕,錄制完成,并將錄制的檔案下載下來,
這是一個最小的需求,如果要擴張的話,需要增減暫停錄制,繼續錄制,輸入自定義的檔案名,定制視頻格式,清晰度,是否錄制聲音,這些要求都是核心需求之外的,可以后續考慮,
?
需求了解清楚了,接下來就是尋找合適的工具或編程語言來實作,
技術調研
首先我想到的是JavaScript,因為JavaScript 是世界上最好的編程語言 😂,馬克斯的火箭操作面板就是使用JavaScript寫的, Lens–Kubernets IDE 也是使用JavaScript寫的,于是我決定先在JavaScript方向上嘗試實作這個工具,
在經過幾番的搜索與請教一些做專業人士后,最終我在JavaScript 的BOM編程中找到了這個物件Navigator.mediaDevices,
mediaDevices 是 Navigator 只讀屬性,回傳一個 MediaDevices 物件,該物件可提供對相機和麥克風等媒體輸入設備的連接訪問,也包括螢屏共享,
經過一番的嘗試與搜索我得出:在瀏覽器上使用JavaScript做錄屏功能使用的主要API是navigator.mediaDevices.getDisplayMedia() 與 MediaRecorder 物件,下面進行一一拆分講解,
navigator 下的MediaDevices有以下幾個主要介面:
- ?
navigator.mediaDevices.enumerateDevices()該方法回傳 一個promise,包含了系統中可用的媒體輸入和輸出設備的一系列資訊, - ?
navigator.mediaDevices.getDisplayMedia()該方法回傳一個promise,并提示用戶選擇顯示幕或顯示幕的一部分(例如視窗)以捕獲為MediaStream 以便共享或記錄,直接在瀏覽器控制臺輸入該方法即可調取授權視窗, - ?
navigator.mediaDevices.getUserMedia()回傳一個promise,在用戶通過提示允許的情況下,打開系統上的相機或螢屏共享和/或麥克風,并提供 MediaStream 包含視頻軌道和/或音頻軌道的輸入,
本次使用的API就是 navigator.mediaDevices.getDisplayMedia(),該方法可以配置一個引數,可以省略,我們可以直接將這段代碼復制到瀏覽器控制臺中運行,運行效果如下圖

圖1:運行navigator.mediaDevices.getDisplayMedia() 效果圖
選中對應的表單或螢屏,點擊分享就可以了,在這個頁面,你滑鼠的移動,頁簽的切換都會實時地反應在上面,由于是目前查看的是當前螢屏,所有會有一個無限嵌套的效果,
點擊分享后,在螢屏的下方會有一個如下的標識

圖2:螢屏分享tab資訊
?
并且在啟動分享的tab上有一個紅色的標識

圖3:螢屏分享tab標識
?
點擊了分享之后,我們的系統就發起了一個分享,當這個分享活動創建后,就會生成一個 MediaStream 翻譯成中文就是媒體流,它是一個媒體內容的流.,一個流包含幾個_軌道_,比如視頻和音頻軌道,這個MediaStream可以直接使用 html中的video 標簽顯示出具體的內容,
?
于是一個清晰的思路就出現了,首先呼叫API navigator.mediaDevices.getUserMedia() 回去一個媒體流,然后使用一個video來顯示這個媒體流,
偽代碼如下
mediaStream = await navigator.mediaDevices.getDisplayMedia()
document.querySelector("video").srcObject = mediaStream
這里需要注意一個細節,要顯示媒體流的內容我們必須將媒體流設定在video的srcObject 屬性上,
video的src與 srcObject二個屬性的區別在與, src是靜態的地址,srcObject是一個實時資料,媒體流,
思路很清晰,接下來我們進行詳細的編碼,
編碼
開始分享螢屏
首先創建一個html,加入一個按鈕,點擊按鈕進行分享螢屏,并在該頁面上顯示分享的內容,
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>測驗web螢屏分享</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/@bootcss/v3.bootcss.com@1.0.17/dist/css/bootstrap.min.css" rel="stylesheet">
<style type="text/css">
html,
body {
height: 100%;
background-color: #fff;
}
.container {
height: 100%;
border: 1px solid #ddd;
padding: 20px;
}
.pre-start-view {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.living-view {
display: none;
}
.video-container {
display: flex;
justify-content: center;
align-content: center;
}
#j_video {
width: 800px;
height: 500px;
}
.video-btns {
margin-top: 16px;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<div class="pre-start-view">
<button type="button" class="btn btn-primary" id="j_start">Start</button>
</div>
<div class="living-view">
<div class="video-container">
<video autoplay playsinline id="j_video"></video>
</div>
</div>
</div>
</body>
<script>
const videoplay = document.querySelector("#j_video")
async function getMediaStream(stream) {
videoplay.srcObject = stream
window.$stream = stream
}
function handleError() {
console.error('getUserMedia error : ', err)
}
async function startCapture(displayMediaOptions) {
let captureStream = null;
try {
captureStream = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);
} catch (err) {
console.error("Error: " + err);
}
return captureStream;
}
async function start() {
if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) {
console.log('getUserMedia is not supported')
} else {
const displayMediaOptions = {
video: true,
audio: false,
}
let captureStream = await startCapture(displayMediaOptions)
await getMediaStream(captureStream)
document.querySelector(".pre-start-view").style.display = "none"
document.querySelector(".living-view").style.display = "block"
}
}
function init() {
document.querySelector("#j_start").addEventListener('click', start)
document.querySelector("#j_record").addEventListener("click", record)
}
init()
</script>
</html>
分享當前螢屏并顯示分享內容 效果如下

圖4:分享內容預覽
到這里我們就已經完成了創建分享,查看分享,但要實作一個完整的錄制功能,還缺少關鍵性的兩步,就是錄制,下載,
?
錄制下載
于是我們在視頻下面添加一個Record 按鈕,點擊開始錄制,然后按鈕變成Stop,點擊后,停止錄制,然后下載一個以當前時間命名的視頻檔案,
?
這里的錄制應該是開始截取媒體流中的一部分,最后做成視頻檔案下載,
查閱檔案后得知,要截取媒體流需要使用MediaRecorder 物件,
MediaRecorder() 建構式會創建一個對指定的 MediaStream 進行錄制的 MediaRecorder 物件,
該建構式接受二個引數,一是媒體流MediaStream,第二個引數是配置引數,指定將媒體流轉化為什么格式的內容,如mp4,音頻的位元率,視頻的位元率,
創建的MediaRecorder 物件可以對錄制程序,進行管理,開始,暫停,停止,
此外MediaRecorder 物件 還有一些事件處理方法,
MediaRecorder.ondataavailable呼叫它用來處理 dataavailable 事件, 該事件可用于獲取錄制的媒體資源 (在事件的 data 屬性中會提供一個可用的 Blob 物件)MediaRecorder.onstart用來處理 start 事件, 該事件在媒體開始錄制時觸發MediaRecorder.onpause用來處理 pause (en-US) 事件, 該事件在媒體暫停錄制時觸發MediaRecorder.onstop用來處理 stop 事件, 該事件會在媒體錄制結束時、媒體流(MediaStream)結束時、或者呼叫MediaRecorder.stop()方法后觸發.
?
我們在創建MediaRecorder物件后,需要監聽它的ondataavailable事件,并將事件中的Blob資料存盤起來,最終將存盤起來的資料轉化為一個視頻檔案,然后下載,
?
最終完整的html代碼
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>測驗web螢屏分享</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/@bootcss/v3.bootcss.com@1.0.17/dist/css/bootstrap.min.css" rel="stylesheet">
<style type="text/css">
html,
body {
height: 100%;
background-color: #fff;
}
.container {
height: 100%;
border: 1px solid #ddd;
padding: 20px;
}
.pre-start-view {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.living-view {
display: none;
}
.video-container {
display: flex;
justify-content: center;
align-content: center;
}
#j_video {
width: 800px;
height: 500px;
}
.video-btns {
margin-top: 16px;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<div class="pre-start-view">
<button type="button" class="btn btn-primary" id="j_start">Start</button>
</div>
<div class="living-view">
<div class="video-container">
<video autoplay playsinline id="j_video"></video>
</div>
<p class="video-btns">
<button type="button" class="btn btn-primary" id="j_record">Record</button>
</p>
</div>
</div>
</body>
<script>
const videoplay = document.querySelector("#j_video")
let videoBuffer = []
let mediaRecorder
let recording = false
async function getMediaStream(stream) {
videoplay.srcObject = stream
window.$stream = stream
}
function handleError() {
console.error('getUserMedia error : ', err)
}
async function startCapture(displayMediaOptions) {
let captureStream = null;
try {
captureStream = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);
} catch (err) {
console.error("Error: " + err);
}
return captureStream;
}
async function start() {
if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) {
console.log('getUserMedia is not supported')
} else {
const displayMediaOptions = {
video: true,
audio: false,
}
let captureStream = await startCapture(displayMediaOptions)
await getMediaStream(captureStream)
document.querySelector(".pre-start-view").style.display = "none"
document.querySelector(".living-view").style.display = "block"
}
}
function handleDataAvailable(e) {
if (e && e.data && e.data.size > 0) {
videoBuffer.push(e.data)
}
}
async function record(even) {
let $target = even.target
if (recording) {
stopRecord()
$target.innerText = 'Record'
} else {
startRecord()
$target.innerText = 'Stop'
}
}
function startRecord() {
videoBuffer = []
const options = {
mimeType: 'video/webm; codecs = vp8',
}
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
console.error('${options.mimeType} is not supported!')
return
}
try {
mediaRecorder = new MediaRecorder(window.$stream, options)
} catch (e) {
console.error('Failed to create MediaRecorder:', e)
return
}
mediaRecorder.ondataavailable = handleDataAvailable
mediaRecorder.start(10)
recording = true
}
function stopRecord() {
mediaRecorder.stop()
recording = false
downRecord()
}
// 下載錄制
function downRecord() {
const blob = new Blob(videoBuffer, { type: 'video/webm' })
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
const fileName = new Date().toLocaleString()
a.href = url
a.style.display = 'none'
a.download = `${fileName}.webm`
a.click()
}
function init() {
document.querySelector("#j_start").addEventListener('click', start)
document.querySelector("#j_record").addEventListener("click", record)
}
init()
</script>
</html>
經過加工改造,撰寫了一個js腳本,使用tampermonkey管理,直接將錄制按鈕注入到頁面上,不會使用tampermonkey的,也可以直接在控制臺執行腳本,最后我將該工具的名字命名為 iREC,
完整工具腳本私信我獲取,
后續
周一我把做好的錄制腳本發給了測驗小妹,
在使用過一段時間后,有人在內部群里給我發了一條這樣的訊息,

哈哈,本故事純屬虛構,如有雷同純屬巧合,希望大家都能把學到的技術轉化為生產力,提升生活品質,
猿力與你同在,
相關鏈接
媒體流解釋: https://developer.mozilla.org/zh-CN/docs/Web/API/MediaStream
媒體錄制解釋: https://developer.mozilla.org/zh-CN/docs/Web/API/MediaRecorder
srcObject解釋: https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLMediaElement/srcObject
WebRTC(五) Web端實作螢屏錄制 https://blog.csdn.net/SImple_a/article/details/102523658
JavaScript 螢屏錄制 API 學習 https://segmentfault.com/a/1190000020267689
MediaRecorder 支持的mimeType
https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/isTypeSupported
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/323456.html
標籤:其他
上一篇:如何從包含“AZ-APP-office365”的組中檢索整個用戶的詳細資訊
下一篇:vue3 + typescript + axios封裝(附帶loading效果,...并攜帶跨域處理,...element-plus按需引入)
