今日任務: 使用getUserMedia API 取得視訊鏡頭,繪製到畫面上,並做出不同的特效
今天這篇比較複雜,因為想寫得清楚一些,所以版面稍長,在這邊先寫下大綱:
- 前置作業
- 視訊鏡頭
- Browsersync
- 請求取得視訊鏡頭
- 把MediaStream放進裡面播放
- 把影像投到canvas上
- 拍照
- 取得canvas畫布的像素色彩
- 紅色濾鏡效果
- 顏色分離效果
- 綠幕效果
因為使用桌電沒有視訊鏡頭,所以這邊是使用ivcam,將手機轉為電腦鏡頭使用
ivcam官網:將手機轉變成電腦視訊鏡頭的免費軟體 App
MediaDevices.getUserMedia()需在安全連線(像是https、localhost)下使用
作者有提供一份package.json,裡面內的套件:Browsersync
BrowserSync 能在開發時幫我們建立一個網頁伺服器,並隨檔案變更自動重新整理頁面
先下載node.js,
如果不確定有沒有裝過可以在終端機輸入node -v
查看版本
下載後在終端機執行
npm install //安裝package.json內的套件(Browsersync)
npm start //啟動localserver(預設port3000)
navigator
物件包含有關使用者的瀏覽器資訊。MediaDevices.getUserMedia()
: 詢問使用者是否同意瀏覽器存取多媒體數據。
使用者按下許可後會產生MediaStream
物件,裡面包含了請求存取的多媒體數據,例如視頻軌道或音頻軌道。
function getVideo() {
navigator.mediaDevices.getUserMedia({ video: true, audio: false })
.then((localMediaStream) => {
console.log(localMediaStream);
});
}
getVideo();
<video>
裡面播放localMediaStream目前是一個obj,為了可以讓<video>
看得懂
把MediaStream轉為url
作者影片寫法:window.URL.createObjectURL()
: 建立一個帶有 URL 的 DOMString 以代表參數中所傳入的物件
video.src = window.URL.createObjectURL(localMediaStream);
video.play();
出現錯誤
作者有在影片標題寫下:
createObjectURL
no longer works withmediaStream
objects. Instead, we need to setvideo.srcObject = localMediaSteam
MDN:createObjectURL()也有寫下:
In older versions of the Media Source specification, attaching a stream to a
<video>
element required creating an object URL for the MediaStream. This is no longer necessary, and browsers are removing support for doing this.
Warning: If you still have code that relies oncreateObjectURL()
to attach streams to media elements, you need to update your code to setsrcObject
to theMediaStream
directly.
瀏覽器目前正在取消支援createObjectURL(MediaStream),所以我們改用srcObject
const video = document.querySelector('.player');
const canvas = document.querySelector('.photo');
const ctx = canvas.getContext('2d');
const strip = document.querySelector('.strip');
const snap = document.querySelector('.snap');
function getVideo() {
navigator.mediaDevices
.getUserMedia({ video: true, audio: false })
.then((localMediaStream) => {
console.log(localMediaStream);
video.srcObject = localMediaStream;
video.play();
})
}
如果使用者拒絕或發生錯誤,加上catch
function getVideo() {
navigator.mediaDevices
.getUserMedia({ video: true, audio: false })
.then((localMediaStream) => {
console.log(localMediaStream);
video.srcObject = localMediaStream;
video.play();
})
.catch((err) => {
console.log('發生錯誤:', err);
});
}
HTMLVideoElement.videoWidth
: 鏡頭的寬度HTMLVideoElement.videoHeight
: 鏡頭的高度
function paintToCanavas() {
const width = video.videoWidth;
const height = video.videoHeight;
console.log(width, height);
canvas.width = width;
canvas.height = height;
}
setInterval(function, milliseconds)
: 每一段時間(毫秒)執行一次functiondrawImage(image, dx, dy, dWidth, dHeight)
: 在畫布上繪製圖像,詳細參數可看這裡ctx.drawImage(video, 0, 0, width, height)
:將鏡頭影像從畫布的(0,0)繪製寬width高height的圖
function paintToCanavas() {
const width = video.videoWidth;
const height = video.videoHeight;
console.log(width, height);
canvas.width = width;
canvas.height = height;
return setInterval(() => {
ctx.drawImage(video, 0, 0, width, height);
}, 16);
}
HTMLMediaElement: canplay event
:可以播放媒體時觸發該canplay事件
video.addEventListener('canplay', paintToCanavas);
toDataURL()
: 將canvas轉為base64位編碼,預設為 image/png。base64位編碼
: Base64編碼是一種圖片處理格式,通過特定的算法將圖片編碼成一長串字符串,在頁面上顯示的時候,可以用該字符串來代替圖片的url屬性。
function takePhoto() {
//拍照音效
snap.currentTime = 0;
snap.play();
//將canvas轉為base64位編碼
const data = canvas.toDataURL('image/jpeg');
console.log(data);
}
base64位編碼長這樣:
createElement(tagName)
:可以建立一個新的HTML元素,直到透過 appendChild()
、insertBefore()
或 replaceChild()
等方法將新元素加入至指定的位置之後才會顯示。insertBefore(newNode, referenceNode)
: 將新節點 newNode 插入至指定的 refNode 節點的前面,詳細可以看這裡
setAttribute(name, value)
:設置指定元素的屬性值。如果屬性已經存在,則更新值<a>
有一個屬性為download,詳細可以看這裡
function takePhoto() {
...
//將canvas轉為base64位編碼
const data = canvas.toDataURL('image/jpeg');
//建立一個新的<a>元素
const link = document.createElement('a');
//超連結為base64位編碼的canvas圖
link.href = data;
//連結點下後會下載base64位編碼的canvas圖,文件名為penguin
link.setAttribute('download', 'penguin');
link.textContent = '下載圖片';
//strip為div,新增的超連結會放到第一個位置,也就是新增的元素會把舊的往後推
strip.insertBefore(link, strip.firstChild);
}
function takePhoto() {
...
link.innerHTML = ` <img src="${data}" alt="penguin">`;
strip.insertBefore(link, strip.firstChild);
}
這樣直接點哪張圖就可以下載哪張圖
認識一下ImageData相關定義與用法:ImageData
: 儲存canvas的像素數據。ImageData.data
: 一維數組,其中包含 RGBA 順序的數據,整數值介於0和255(包括)之間。getImageData()
: 取得ImageData數據。putImageData
: 將ImageData數據繪製到畫布上 。
const pixels = ctx.getImageData(0, 0, width, height);
console.log(pixels);
console.log來看一下
將data展開
每4個數字代表圖中每一個像素的顏色,順序為 R(紅色)、G(綠色)、B(藍色)和 A(alpha不透明度),再由這些像素組成一張圖。
下圖來源:這裡,裡面有對canvas ImageData的詳細介紹
因為每個像素由data數組中的四個值組成,因此for循環以4的倍數進行迭代。
function redEffect(pixels) {
for (let i = 0; i < pixels.data.length; i += 4) {
pixels.data[i + 0] = pixels.data[i + 0] + 100; //紅色增強
pixels.data[i + 1] = pixels.data[i + 1] - 50; //綠色減弱
pixels.data[i + 2] = pixels.data[i + 2] * 0.5; //藍色減弱
}
return pixels;
}
function paintToCanavas() {
...
return setInterval(() => {
//繪製畫布
ctx.drawImage(video, 0, 0, width, height);
//取出canvas像素資料
pixels = ctx.getImageData(0, 0, width, height);
//紅色濾鏡效果
pixels = redEffect(pixels);
//將ImageData數據繪製到畫布上
ctx.putImageData(pixels,0,0)
}, 16);
}
紅色效果
以此類推,可以將綠色數值增高就變綠色
pixels.data[i + 1] = pixels.data[i + 1] + 150;
這個效果看影片時不太理解,後來自己實作嘗試後有自己的理解想法,記錄下來,希望有幫助到一樣疑惑的人,如有錯誤也歡迎提出!
function rgbSplit(pixels) {
for (let i = 0; i < pixels.data.length; i += 4) {
pixels.data[i + 0 - 360] = pixels.data[i + 0];
pixels.data[i + 1 - 360] = pixels.data[i + 1];
pixels.data[i + 2 - 360] = pixels.data[i + 2];
}
return pixels;
}
可以看到畫面被移過去
pixels.data[i - 150]
= pixels.data[i + 0];
事實上,pixels.data[i + 0]
只是一個數值,例如:255、100
而pixels.data[i - 150]
跑迴圈後,為:pixels.data[-150]
,pixels.data[-146]
,pixels.data[-142]
...pixels.data[-2]
,pixels.data[+2]
,pixels.data[+6]
...
可以從pixels.data[+2]
發現是負責處理每個像素藍色部分的地方,
所以pixels.data[i - 150]
= pixels.data[i + 0];
跑迴圈等於:
像素1控制藍色部分=255,像素2控制藍色部分=100,像素3控制藍色部分=150......
所以可以看到是所有藍色往左邊移動,紅+綠留在原地
因此要看要看前面的數值為處理什麼顏色,數值越大,移動部分越多
以此類推:
//像素控制藍色部分=數值;pixels.data[i - 150] = pixels.data[i + 0];
//像素控制綠色部分=數值;pixels.data[i - 151] = pixels.data[i + 0];
//像素控制紅色部分=數值;pixels.data[i - 152] = pixels.data[i + 0];
//像素控制alpha部分=數值;pixels.data[i - 153] = pixels.data[i + 0];
//又回到像素控制藍色部分=數值;pixels.data[i - 154] = pixels.data[i + 0];
紅色和藍色往左移動640像素
function rgbSplit(pixels) {
for (let i = 0; i < pixels.data.length; i += 4) {
pixels.data[i + 0 - 640] = pixels.data[i + 0]; //控制紅色(會被下一個控制紅色覆蓋)
pixels.data[i - 640] = pixels.data[i + 1];//控制紅色
pixels.data[i + 2 - 640] = pixels.data[i + 2]; //控制藍色
}
return pixels;
}
紅色和綠色往左移動640像素
function rgbSplit(pixels) {
for (let i = 0; i < pixels.data.length; i += 4) {
pixels.data[i + 0 - 640] = pixels.data[i + 0]; //控制紅色(會被下一個控制紅色覆蓋)
pixels.data[i + 1 - 640] = pixels.data[i + 1]; //控制綠色
pixels.data[i - 640] = pixels.data[i + 2]; //控制紅色
}
return pixels;
}
紅色往左移動640像素
function rgbSplit(pixels) {
for (let i = 0; i < pixels.data.length; i += 4) {
pixels.data[i - 640] = pixels.data[i + 0];//控制紅色(會被下一個控制紅色覆蓋)
pixels.data[i - 640] = pixels.data[i + 1];//控制紅色(會被下一個控制紅色覆蓋)
pixels.data[i - 640] = pixels.data[i + 2]; //控制紅色
}
return pixels;
}
讓我們回到作者的數據:
pixels.data[i - 150] = pixels.data[i + 0]; // 控制藍色
pixels.data[i + 500] = pixels.data[i + 1]; // 控制紅色
pixels.data[i - 550] = pixels.data[i + 2]; // 控制藍色
發現有兩個控制藍色的部分,代表其實第一個控制藍色會被覆蓋
當我把第一個控制藍色(pixels.data[i - 150] = pixels.data[i + 0];
)註解掉:
可以發現兩張圖長得一模一樣
function paintToCanavas() {
...
return setInterval(() => {
...
//加入顏色分離效果
pixels = rgbSplit(pixels);
ctx.putImageData(pixels, 0, 0);
}, 16);
}
function rgbSplit(pixels) {
for (let i = 0; i < pixels.data.length; i += 4) {
pixels.data[i - 150] = pixels.data[i + 0]; // 控制藍色
pixels.data[i + 500] = pixels.data[i + 1]; // 控制紅色
pixels.data[i - 550] = pixels.data[i + 2]; // 控制藍色
}
return pixels;
}
ctx.globalAlpha
:在將形狀和圖像繪製到畫布上之前應用於這個屬性的 alpha(不透明度)值。
function paintToCanavas() {
...
return setInterval(() => {
...
pixels = rgbSplit(pixels);
ctx.globalAlpha = 0.1
ctx.putImageData(pixels, 0, 0);
}, 16);
}
<div class="rgb">
<label for="rmin">Red Min:</label>
<input type="range" min="0" max="255" name="rmin" />
<label for="rmax">Red Max:</label>
<input type="range" min="0" max="255" name="rmax" />
<br />
<label for="gmin">Green Min:</label>
<input type="range" min="0" max="255" name="gmin" />
<label for="gmax">Green Max:</label>
<input type="range" min="0" max="255" name="gmax" />
<br />
<label for="bmin">Blue Min:</label>
<input type="range" min="0" max="255" name="bmin" />
<label for="bmax">Blue Max:</label>
<input type="range" min="0" max="255" name="bmax" />
</div>
const controllers = document.querySelectorAll('.rgb input');
function greenScreen() {
const levels = {};
controllers.forEach((controller) => {
levels[controller.name] = controller.value;
console.log(levels);
});
}
如果最小值 <= 顏色 <= 最大值,將它設為透明
function paintToCanavas() {
...
return setInterval(() => {
...
//加入綠幕效果
pixels = greenScreen(pixels);
//輸出
ctx.putImageData(pixels, 0, 0);
}, 16);
}
function greenScreen(pixels) {
const levels = {};
controllers.forEach((controller) => {
levels[controller.name] = controller.value;
});
for (i = 0; i < pixels.data.length; i += 4) {
red = pixels.data[i + 0];
green = pixels.data[i + 1];
blue = pixels.data[i + 2];
alpha = pixels.data[i + 3];
if (
red >= levels.rmin &&
green >= levels.gmin &&
blue >= levels.bmin &&
red <= levels.rmax &&
green <= levels.gmax &&
blue <= levels.bmax
) {
//透明
pixels.data[i + 3] = 0;
}
}
return pixels;
}
今天的比較複雜一點,以上皆可以再改數值玩出不同效果,
避免板面太長偏離主題,所以有些知識點只有稍微介紹過去,有興趣的話可以再點進相關連結詳細了解。
今日學習到的:
navigator物件
: 包含有關使用者的瀏覽器資訊。MediaDevices.getUserMedia()
: 詢問使用者是否同意瀏覽器存取多媒體數據。MediaStream物件
: 裡面包含了請求存取的多媒體數據,例如視頻軌道或音頻軌道。<video>
裡面播放:
createObjectURL(MediaStream)
,改用video.srcObject = localMediaStream
;HTMLVideoElement.videoWidth
: 鏡頭的寬度HTMLVideoElement.videoHeight
: 鏡頭的高度setInterval(function, milliseconds)
: 每一段時間(毫秒)執行一次functiondrawImage()
: 在畫布上繪製圖像canplay event
:可以播放媒體時觸發該canplay事件toDataURL()
: 將canvas轉為base64位編碼,預設為 image/png。createElement(tagName)
:可以建立一個新的HTML元素,將新元素加入至指定的位置之後才會顯示。insertBefore(newNode, referenceNode)
: 將新節點 newNode 插入至指定的 refNode 節點的前面。setAttribute(name, value)
:設置指定元素的屬性值。如果屬性已經存在,則更新值<a>
有一個屬性為downloadImageData
: 儲存canvas的像素數據。ImageData.data
: 一維數組,每4個數字代表圖中每一個像素的顏色,順序為 R(紅色)、G(綠色)、B(藍色)和 A(alpha不透明度),整數值介於0和255(包括)之間,再由這些像素組成一張圖。getImageData()
: 取得ImageData數據。putImageData()
: 將ImageData數據繪製到畫布上 。ctx.globalAlpha
:在將形狀和圖像繪製到畫布上之前應用於這個屬性的 alpha(不透明度)值。效果連結:連結(有加上效果按鈕可切換)
參考連結:
MDN: MediaDevices.getUserMedia()
安全連線
MDN: URL.createObjectURL()
MDN: HTMLMediaElement.srcObject
MDN: Canvas.drawImage()
Base64編碼
MDN: a屬性
重新認識 JavaScript: Day 13 DOM Node 的建立、刪除與修改
MDN: ImageData.data
Canvas ImageData 對象