前言
長期以來音視頻捕獲一直是web開發的珍寶。多年來,我們不得不依賴瀏覽器插件( flash 或者 silverlight )來完成這個工作.
HTML5完成了救贖。可能並不明顯,但是HTML5的興起帶來了大量對這杯硬件的訪問。Geolocation
(GPS)、Orientation API
(加速度計)、WebGL
(GPU)和Web Audio
(音頻硬件)都是完美的例子。這些功能非常強大,暴露了位於系統底層硬件功能之上的高級JS api。
本篇教程介紹 navigator.mediaDevices.getUserMedia()
,這使得web應用能夠訪問用戶的攝像頭和麥克風.
探尋getUserMedia()
之路
如果你不清楚這段歷史,那我們這段探尋之旅將是一個非常有趣的故事。
在過去的幾年中, "Media Capture APIs" 的幾個變種都得到了發展。許多人意識到在web上需要訪問本設備,而這出事了每一個人共同制定一套新規范。事情變得很混亂(每個瀏覽器都有自己的規范),W3C最終決定成立一個工作組。他們唯一的目的就是去掉混亂。Device APIs Policy (DAP) 工作組已經把整合化和標准化大量提案作為他們的任務。
我將會嘗試總結下2011年發生的事情。。。
第一輪 HTML Media Capture
HTML Media Capture 是DAP首次在web上標准化如何訪問多媒體。它通過重載 <input type="file">
標簽並新增accept
參數的值來實現.
如果你想讓用戶使用網絡攝像頭來獲取一個快照,可以使用 capture=camera
<input type="file" accept="image/*;capture=camera">
錄制視頻和視頻也是類似的:
<input type="file" accept="video/*;capture=camcorder">
<input type="file" accept="audio/*;capture=microphone">
看起來不錯吧?我特別喜歡它重用文件輸入。從語義上來說很好理解。這個api的不足之處是實時效果(比如:渲染實時網絡攝像頭數據到 <canvas>
並應用到 WebGL過濾器).HTML Media Capture僅允許你錄制和獲取快照(無法進行實時渲染).
第二輪 device 元素標簽
許多人認為 HTML Media Capture api過於局限,因此出現了支持任何類型(未來)設備的新規范。該設計不意外的使用了新標簽 getUserMedia()
的前身。
Opera是最早一批基於 <device>
標簽實現視頻捕獲的瀏覽器廠商之一。不久之后(准確來說是同一天),WhatWG (網頁超文本技術工作小組)決定取消 device
標簽轉向另一個方案:navigator.getUserMedia()
。一個星期后,Opera發布了支持getUserMedia()提案的新版本(這迭代速度剛剛的。。。)從那年以后,微軟也加入了新提案的大家庭:IE9 實驗室版支持新特性.
<device>
就長這樣:
<device type="media" onchange="update(this.data)"></device>
<video autoplay></video>
<script>
function update(stream) {
document.querySelector('video').src = stream.url;
}
</script>
支持
不幸的是,瀏覽器正式版中一直沒有支持過 <device>
。 <device>
有兩個好處:1. 語義化良好 2. 更容易去擴展而不僅僅支持音頻和視頻設備。
第三輪 WebRTC
<device>
標簽最終被放棄了。。。
由於WebRTC(web實時通信)的努力使得找到合適api的速度加快了。這個規范由 W3C WebRTC小組監督。谷歌,Opera,火狐和一些公司已經實現。
getUserMedia()自從Chrome 21,Opera 18和Firefox 17開始支持。最初由Navigator.getUserMedia()提供支持的方法現在已經被廢除。你現在應該使用navigator.MediaDevices.getUserMedia(),這個方法普遍都支持。
navigator.MediaDevices.getUserMedia()入門
有了getUserMedia
,我們無需使用任何插件就可使用網絡攝像頭和麥克風輸入。訪問攝像頭只需要一個調用,而不是安裝插件。它直接被內置在瀏覽器中。有沒有點小激動!
功能檢測
對 navigator.mediaDevices.getUserMedia 是否支持進行簡單檢查:
function hasGetUserMedia() {
return !!(navigator.mediaDevices &&
navigator.mediaDevices.getUserMedia);
}
if (hasGetUserMedia()) {
// Good to go!
} else {
alert('getUserMedia() is not supported by your browser');
}
獲取一個輸入設備的訪問權限
為了使用攝像頭和麥克風,我們需要申請權限。getUserMedia()的參數是一個對象:對每一種你想訪問的每一種多媒體說明了具體和要求。比如,如果你想訪問攝像頭,參數應該為 {video: true}
。同時使用攝像頭和麥克風,傳入 {video: true, audio: true}
:
<video autoplay></video>
<script>
const constraints = {
video: true
};
const video = document.querySelector('video');
navigator.mediaDevices.getUserMedia(constraints).
then((stream) => {video.srcObject = stream});
</script>
ok.現在感覺怎么樣。。。媒體捕獲是HTML5協同工作的完美示例。它可以和其他HTML5小伙伴一起工作:
我們告訴
設置媒體限制(解析度,寬,高)
傳給getUserMedia()的參數可以用來對返回的媒體流做更多要求(或者限制)。比如,不僅僅指明你想訪問vedio(比如{vedio: true),你還可以添加額外的配置要求返回HD的流:
const hdConstraints = {
video: {width: {min: 1280}, height: {min: 720}}
};
navigator.mediaDevices.getUserMedia(hdConstraints).
then((stream) => {video.srcObject = stream});
...
const vgaConstraints = {
video: {width: {exact: 640}, height: {exact: 480}}
};
navigator.mediaDevices.getUserMedia(vgaConstraints).
then((stream) => {video.srcObject = stream});
如果當前被選擇的攝像頭不滿足解析度的要求,getUserMedia()將會被rejected一個OverconstrainedError
,並且不會向用戶申請訪問攝像頭的提示。
選擇媒體源
navigator.mediaDevices.enumerateDevices() 方法提供了可用的輸入、輸出設備的信息,並可以選擇相機和麥克風(注:MediaStreamTrack.getSources()
api已經被棄用)
這個例子允許用戶選擇音視頻源:
const videoElement = document.querySelector('video');
const audioSelect = document.querySelector('select#audioSource');
const videoSelect = document.querySelector('select#videoSource');
navigator.mediaDevices.enumerateDevices()
.then(gotDevices).then(getStream).catch(handleError);
audioSelect.onchange = getStream;
videoSelect.onchange = getStream;
function gotDevices(deviceInfos) {
for (let i = 0; i !== deviceInfos.length; ++i) {
const deviceInfo = deviceInfos[i];
const option = document.createElement('option');
option.value = deviceInfo.deviceId;
if (deviceInfo.kind === 'audioinput') {
option.text = deviceInfo.label ||
'microphone ' + (audioSelect.length + 1);
audioSelect.appendChild(option);
} else if (deviceInfo.kind === 'videoinput') {
option.text = deviceInfo.label || 'camera ' +
(videoSelect.length + 1);
videoSelect.appendChild(option);
} else {
console.log('Found another kind of device: ', deviceInfo);
}
}
}
function getStream() {
if (window.stream) {
window.stream.getTracks().forEach(function(track) {
track.stop();
});
}
const constraints = {
audio: {
deviceId: {exact: audioSelect.value}
},
video: {
deviceId: {exact: videoSelect.value}
}
};
navigator.mediaDevices.getUserMedia(constraints).
then(gotStream).catch(handleError);
}
function gotStream(stream) {
window.stream = stream; // make stream available to console
videoElement.srcObject = stream;
}
function handleError(error) {
console.error('Error: ', error);
訪問Sam Dutton's great demo,這個例子顯示了如何讓用戶選擇媒體源。
安全
getUserMedia() 只能在HTTPS URL,localhost 或者 file://URL下使用,其他情況會被rejected掉。getUserMedia()也不能在跨域的iframe中使用。
所有的瀏覽器在調用getUserMedia()是都會彈出一個信息框,讓用戶可以選擇授權或者拒絕其攝像頭或者麥克風的使用權限。下圖是chrome的權限彈框:
這個授權是永久的。也就是說,用戶不用每次去授權(決絕/允許)。如果用戶后來改變了主意,他們可以瀏覽器設置中心針對每一個域進行設置。
貼士:攝像頭在使用的情況下,MediaStreamTrack處於激活, 這會占用資源,並且打開攝像頭(保持攝像頭燈打開),當你不再使用track的時候,確保調用 track.stop() 來使攝像頭關閉。
截屏
<video autoplay></video>
<img src="">
<canvas style="display:none;"></canvas>
<script>
const captureVideoButton =
document.querySelector('#screenshot .capture-button');
const screenshotButton = document.querySelector('#screenshot-button');
const img = document.querySelector('#screenshot img');
const video = document.querySelector('#screenshot video');
const canvas = document.createElement('canvas');
captureVideoButton.onclick = function() {
navigator.mediaDevices.getUserMedia(constraints).
then(handleSuccess).catch(handleError);
};
screenshotButton.onclick = video.onclick = function() {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0);
// Other browsers will fall back to image/png
img.src = canvas.toDataURL('image/webp');
};
function handleSuccess(stream) {
screenshotButton.disabled = false;
video.srcObject = stream;
}
</script>
應用效果
css濾鏡
使用css濾鏡,我們可以在
<video autoplay></video>
<p><button class="capture-button">Capture video</button>
<p><button id="cssfilters-apply">Apply CSS filter</button></p>
<script>
const captureVideoButton =
document.querySelector('#cssfilters .capture-button');
const cssFiltersButton =
document.querySelector('#cssfilters-apply');
const video =
document.querySelector('#cssfilters video');
let filterIndex = 0;
const filters = [
'grayscale',
'sepia',
'blur',
'brightness',
'contrast',
'hue-rotate',
'hue-rotate2',
'hue-rotate3',
'saturate',
'invert',
''
];
captureVideoButton.onclick = function() {
navigator.mediaDevices.getUserMedia(constraints).
then(handleSuccess).catch(handleError);
};
cssFiltersButton.onclick = video.onclick = function() {
video.className = filters[filterIndex++ % filters.length];
};
function handleSuccess(stream) {
video.srcObject = stream;
}
</script>
WebGL紋理
視頻捕獲一個神奇的應用就是作為一個WegGL紋理實時渲染。因為我對WebGL一無所知(除了它很牛逼),所以我建議你們看一個教程和示例。它講了如何在WebGL里使用getUserMedia() 和Three.js去渲染實時vedio。
對web audio api使用getUserMedia
我的夢想之一就是在瀏覽器中構建autotune(不知道啥意思),而不是開放的web技術。
chrome支持從getUserMedia()到web audio api的實時麥克風輸入,以實現實時效果。
window.AudioContext = window.AudioContext ||
window.webkitAudioContext;
const context = new AudioContext();
navigator.mediaDevices.getUserMedia({audio: true}).
then((stream) => {
const microphone = context.createMediaStreamSource(stream);
const filter = context.createBiquadFilter();
// microphone -> filter -> destination
microphone.connect(filter);
filter.connect(context.destination);
});
總結
從發展歷程上來看,在web上訪問設備一直是個棘手的難題。做了許多嘗試,只有少部分獲得了成功。大多數早起的想法從未得到廣泛的采用,也沒有在特定環境之外被接受。主要的問題可能是web的安全模型與原生系統有很大的不同。特別是,您可能並不希望每個網站都可以隨機訪問您的攝像頭和麥克風。總之很難找到權衡之際。
在移動設備日益普及的功能驅動下,web開始提供更豐富的功能。我們現在可以利用很多api去拍照,控制攝像頭設置,錄制音頻和視頻,並訪問其他類型的傳感器數據,比如位置,運動和設備方向。通過傳感器框架將他們串在一起,並與api一起配合使用,使得we應用能訪問usb並與藍牙進行交互。
getUserMedia()只是與硬件交互的第一波熱潮。