最近忽發奇想,在下每次使用電子顯微鏡觀察電子晶片時都需要使用一些能夠存取鏡頭裝置的軟件,
雖然不算不方便,亦有支援 Linux 的軟件,但就是覺得有點麻煩,
在下曾經在網上見過有網頁能夠存取鏡頭裝置,並在網頁上顯示鏡頭影像,因此在下打算實作這個操作。
存取媒體串流
媒體裝置
const constraints = {
"audio": false, // boolean
"video": true, // boolean
};
let mediaStreamObject = await navigator.mediaDevices.getUserMedia(constraints);
執行 navigator.mediaDevices.getUserMedia 後,網頁瀏覽器 會先嘗試列出系統上可用的 USB鏡頭裝置,並請求使用者授權存取。
完成裝置存取權限請求後,使用者可以在下拉式選單中選擇目標鏡頭。
htmlVideoElement.srcObject = mediaStreamObject;
選定鏡頭後,按下 允許(Allow) 即可開始存取影像。
navigator.mediaDevices.getUserMedia 會回傳鏡頭的 媒體串流物件(MediaStream Object) ,
只要將 媒體串流物件 指派給 <video>元素 的 srcObject屬性 ,就能在網頁上即時顯示鏡頭畫面。
留意:<video>元素 的 srcObject屬性 不能以 htmlVideoElement.setAttribute("srcObject", mediaStreamObject) 指派。
測試拍攝功能,在下嘗試將鏡頭影像拍攝成圖片,
使用 <canvas>元素 繪製鏡頭影像,再將鏡頭影像儲存為圖像檔案。
測試錄影功能,在下嘗試使用 MediaRecorder API 錄製鏡頭串流,
同樣使用 <video>元素 將影像片段節錄,並影像片段節錄儲存為影片檔案。
基於 Chromium 的 網頁瀏覽器 都 內建 投放(Cast) 功能,
點擊後,網頁瀏覽器 會自動掃描相同網域內的 Chromecast 或 支援投放的電視裝置 。
選定後,就能將鏡頭的 即時鏡頭影像 投放到螢幕上,不需要額外硬體或軟體。
顯示裝置
在下想到,疫情後經常使用的網上會議軟件,
普遍都能夠讓參與者不需要額外安裝軟件,只需要使用網頁瀏覽器便能夠加入至會議,
而且能夠在網頁瀏覽器上 分享熒幕,因此在下同樣嘗試了解原理,
發現這個功能同樣來自 Web Media Devices API 的另一個功能。
執行 navigator.mediaDevices.getUserDisplay 後,網頁瀏覽器 會先嘗試列出系統上可用的 熒幕裝置 及 活動視窗 。
使用者可以在下拉式選單中選擇目標 熒幕裝置(Display) 及 活動視窗(Active Window) 。
獲得授權後,同樣回傳 媒體串流物件 , 因此往後的操作與 navigator.mediaDevices.getUserMedia 獲取 USB鏡頭裝置 完全相同。
基於 Chromium 的網頁瀏覽器同樣完整支援 Web Media Devices API,
會按照媒體類型清楚分類,讓使用者更容易找到並選擇要分。
另外,基於 Chromium 的網頁瀏覽器 還額外提供專屬的 分頁(Tab) 選項,
即使目標分頁不是目前正在檢視的活動分頁,也能直接從選單中選取並分享該分頁的內容,而不需要強制切回該分頁。
測試錄影視窗的效果。
// WebMediaDevicesAPI.js
class WebMediaDevicesAPI {
#mediaStreamContainer = null;
#mediaRecorder = null;
#recordedChunks = [];
constructor() {
}
async openDevice(mediaStreamContainer) {
try {
if (this.#mediaStreamContainer != null) {
throw new Error("Media Stream is already opened");
}
this.#openStream(await navigator.mediaDevices.getUserMedia({"video": true, "audio": false}), mediaStreamContainer);
} catch (error) {
throw error;
}
}
async openDisplay(mediaStreamContainer) {
try {
if (this.#mediaStreamContainer != null) {
throw new Error("Media Stream is already opened");
}
this.#openStream(await navigator.mediaDevices.getDisplayMedia({"video": true, "audio": false}), mediaStreamContainer);
} catch (error) {
throw error;
}
}
#openStream(mediaStream, mediaStreamContainer) {
try {
if (this.#mediaStreamContainer != null) {
throw new Error("Media Stream Container is already opened");
}
this.#mediaStreamContainer = mediaStreamContainer;
this.#mediaStreamContainer.srcObject = mediaStream;
const TRACKS = this.#mediaStreamContainer.srcObject.getTracks();
for (const I in TRACKS) {
const SETTINGS = TRACKS[I].getSettings();
this.#mediaStreamContainer.setAttribute("width", SETTINGS.width);
this.#mediaStreamContainer.setAttribute("height", SETTINGS.height);
}
} catch (error) {
throw error;
}
}
closeStream() {
try {
if (this.#mediaStreamContainer == null) {
throw new Error("No Media Stream");
}
const TRACKS = this.#mediaStreamContainer.srcObject.getTracks();
for (const I in TRACKS) {
TRACKS[I].stop();
}
this.#mediaStreamContainer.removeAttribute("width");
this.#mediaStreamContainer.removeAttribute("height");
this.#mediaStreamContainer.srcObject = null;
this.#mediaStreamContainer = null;
} catch (error) {
throw error;
}
}
captureFrame(captureContainer) {
try {
if (this.#mediaStreamContainer == null) {
throw new Error("No Media Stream Container");
}
let width = this.#mediaStreamContainer.getAttribute("width");
let height = this.#mediaStreamContainer.getAttribute("height");
captureContainer.setAttribute("width", width);
captureContainer.setAttribute("height", height);
const CONTEXT = captureContainer.getContext("2d");
CONTEXT.drawImage(this.#mediaStreamContainer, 0, 0, width, height);
} catch (error) {
throw error;
}
}
startClip() {
try {
if (this.#mediaStreamContainer == null) {
throw new Error("No Media Stream");
}
if (this.#mediaRecorder != null) {
throw new Error("Media Recorder is already recorded");
}
this.#mediaRecorder = new MediaRecorder(this.#mediaStreamContainer.srcObject);
this.#mediaRecorder.addEventListener("dataavailable", function(dataavailableEvent) {
this.#recordedChunks.push(dataavailableEvent.data);
}.bind(this));
this.#mediaRecorder.start(1000);
} catch (error) {
throw error;
}
}
stopClip(clipContainer) {
try {
if (this.#mediaRecorder == null) {
throw new Error("No Media Recorder");
}
this.#mediaRecorder.addEventListener("stop", function(stopEvent) {
clipContainer.src = URL.createObjectURL(new Blob(this.#recordedChunks));
clipContainer.pause();
this.#mediaRecorder = null;
this.#recordedChunks = [];
}.bind(this));
this.#mediaRecorder.stop();
} catch (error) {
throw error;
}
}
}
<!DOCTYPE html>
<!-- index.html -->
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title></title>
<style>
html, body {
margin: 0;
padding: 0;
}
video, canvas {
width: 320px;
height: 180px;
border: 1px solid #000000;
}
</style>
<script src="WebMediaDevicesAPI.js"></script>
<script>
// <!--
const WEB_MEDIA_DEVICES_API = new WebMediaDevicesAPI();
window.addEventListener("load", async function(loadEvent) {
const STREAM_CONTAINER = document.getElementById("stream-container");
const CAPTURE_CONTAINER = document.getElementById("capture-container");
const CLIP_CONTAINER = document.getElementById("clip-container");
document.getElementById("open-camera").addEventListener("click", async function(clickEvent) {
WEB_MEDIA_DEVICES_API.openDevice(STREAM_CONTAINER);
});
document.getElementById("open-display").addEventListener("click", async function(clickEvent) {
WEB_MEDIA_DEVICES_API.openDisplay(STREAM_CONTAINER);
});
document.getElementById("close-stream").addEventListener("click", async function(clickEvent) {
WEB_MEDIA_DEVICES_API.closeStream();
});
document.getElementById("capture-frame").addEventListener("click", async function(clickEvent) {
WEB_MEDIA_DEVICES_API.captureFrame(CAPTURE_CONTAINER);
});
document.getElementById("start-clip").addEventListener("click", async function(clickEvent) {
WEB_MEDIA_DEVICES_API.startClip();
});
document.getElementById("stop-clip").addEventListener("click", async function(clickEvent) {
WEB_MEDIA_DEVICES_API.stopClip(CLIP_CONTAINER);
});
});
// -->
</script>
</head>
<body>
<div>
<button id="open-camera">Open Camera</button>
<button id="open-display">Open Display</button>
<button id="close-stream">Close Stream</button>
<button id="capture-frame">Capture Frame</button>
<button id="start-clip">Start Clip</button>
<button id="stop-clip">Stop Clip</button>
</div>
<div>
<video id="stream-container" controls="true" mutes="false" autoplay="true" width="560" height="315"></video>
</div>
<div>
<canvas id="capture-container" width="560" height="315"></canvas>
</div>
<div>
<video id="clip-container" controls="true" mutes="false" autoplay="false" width="560" height="315"></video>
</div>
</body>
</html>
基於兩種存取方法,在下編寫 WebMediaDevicesAPI 類別,方便存取 鏡頭裝置 及 熒幕影像。










