作者:韓振方
https://juejin.cn/post/7289662055183597603
前言
最近接到的一個需求十分有意思,設(shè)計整體實現(xiàn)了前端仿 微信掃一掃 的功能。整理了一下思路,做一個分享。
tips:
如果想要實現(xiàn)完整掃一掃的功能,你需要掌握一些前置知識,這次我們先講如何實現(xiàn)拍照并且保存的功能。
你想調(diào)取手機的攝像頭,首先你得先檢驗當(dāng)前設(shè)備是否有攝像設(shè)備,window 身上自帶了一個 navigator 屬性,這個對象有一個叫做 mediaDevices 的屬性是我們即將用到的。
于是我們就可以先設(shè)計一個叫做 checkCamera
的函數(shù),用來在頁面剛開始加載的時候執(zhí)行。。
我們先看一下這個對象有哪些方法,你也許會看到下面的場景,會發(fā)現(xiàn)這個屬性身上只有一個值為 null 的 ondevicechange 屬性,不要怕,真正要用的方法其實在它的原型身上。
讓我們點開它的原型屬性,注意下面這兩個方法,這是我們本章節(jié)的主角。
我們到這一步只是需要判斷當(dāng)前設(shè)備是否有攝像頭,我們先調(diào)取 enumerateDevices 函數(shù)來查看當(dāng)前媒體設(shè)備是否存在。它的返回值是一個 promise 類型,我們直接用 async 和 await 來簡化一下代碼。
從上圖可以看出,我的電腦有兩個音頻設(shè)備和一個視頻設(shè)備,那么我們就可以放下進(jìn)行下一步了。接下來就需要用到上面提到的第二個函數(shù),navigator.getUserMedia。這個函數(shù)接收一個對象作為參數(shù),這個對象可以預(yù)設(shè)一些值,來作為我們請求攝像頭的一些參數(shù)。
這里我們的重點是 facingMode 這個屬性,因為我們掃一掃一般都是后置攝像頭
當(dāng)你執(zhí)行了這個函數(shù)以后,你會看到瀏覽器有如下提示:于是你高興的點擊了允許,卻發(fā)現(xiàn)頁面沒有任何變化。
這里你需要知道,這個函數(shù)只是返回了一個媒體流信息給你,你可以這樣簡單理解剛剛我們干了什么,首先瀏覽器向手機申請我想用一下攝像頭可以嗎?在得到了你本人的確認(rèn)以后,手機將攝像頭的數(shù)據(jù)線遞給了瀏覽器,:“諾,給你?!?/span>
但瀏覽器現(xiàn)在僅僅拿到了一根數(shù)據(jù)線,然而瀏覽器不知道需要將這個攝像頭渲染到哪里,它不可能自動幫你接上這根線,你需要自己找地方接上這根數(shù)據(jù)線。
這里不賣關(guān)子,我們需要請到我們的 Video 標(biāo)簽。我沒聽錯吧?那個播放視頻的 video
標(biāo)簽?沒錯,就是原生的 video
標(biāo)簽。
這里創(chuàng)建一個 video 標(biāo)簽,然后打上 ref 來獲取這個元素。
這里的關(guān)鍵點在于將流數(shù)據(jù)賦值給 video 標(biāo)簽的 srcObject 屬性。就好像你拿到了數(shù)據(jù)線,插到了顯示器上。
(tips: 這里需要特別注意,不是 video.src 而是 video.srcObject 請務(wù)必注意)
現(xiàn)在你應(yīng)該會看到攝像頭已經(jīng)在屏幕上展示了,這里是我用電腦前置攝像頭錄制的一段視頻做成了gif。(脈動請給我打錢,哼)
這里我隨手寫了一個按鈕當(dāng)作拍攝鍵,接下來我們將實現(xiàn)點擊這個按鈕截取當(dāng)前畫面。
這里你需要知道一個前提,雖然我們現(xiàn)在看到的視頻是連貫的,但其實在瀏覽器渲染的時候,它其實是一幀一幀渲染的。就像宮崎駿有些動漫一樣,是一張一張手寫畫。
讓我們打開 Performance 標(biāo)簽卡,記錄一下打開掘金首頁的過程,可以看到瀏覽器的整個渲染過程其實也是一幀一幀拼接到一起,才完成了整個頁面的渲染。
知道了這個前提,那么舉一反三,我們就可以明白,雖然我們現(xiàn)在已經(jīng)打開了攝像頭,看到的視頻好像是在連貫展示,但其實它也是一幀一幀拼到一起的。那現(xiàn)在我們要做的事情就非常明了,當(dāng)我按下按鈕的時候,想辦法將 video 標(biāo)簽當(dāng)前的畫面保存下來。
這里不是特別容易想到,我就直接說答案了,在這個場景,我們需要用到 canvas 的一些能力。不要害怕,我目前對 canvas 的使用也不是特別熟練,今天也不會用到特別復(fù)雜的功能。
首先創(chuàng)建一個空白的 canvas 元素,元素的寬高設(shè)置為和 video 標(biāo)簽一致。
接下來是重點: 我們需要用到 canvas 的 getContext 方法,先別著急頭暈,這里你只需要知道,它接受一個字符串 '2d'
作為參數(shù)就行了,它會把這個畫布的上下文返回給你。
( tips 如果這里還不清楚上下文的概念,也不用擔(dān)心,這里你就簡單理解為把這個 canvas 這個元素加工了一下,幫你在它身上添加了一些新的方法而已。)
在這個 ctx 對象身上,我們只需要用到一個 drawImage
方法即可,不需要關(guān)心其它屬性。
感覺參數(shù)有點多?沒關(guān)系,我們再精簡一下,我們只需要考慮第二個用法,也就是5參數(shù)的寫法。(sx,sy 是做裁切用到的,本文用不到,感興趣可以自行了解。)
這里先簡單解釋一下 dx 和 dy 是什么意思。在 canvas 里也存在一個看不見的坐標(biāo)系,起點也是左上角。設(shè)想你想在一個 HTML 的 body 元素里寫一個距離左邊距離 100px
距離頂部 100px
的畫面,是不是得寫 margin-left:100px margin-top:100px
這樣的代碼?沒錯,這里的 dy 和 dx 也是同樣的道理。
我們再看 dwidth
,和 dheight
,從這個名字你就能才出來,肯定和我們將要在畫筆里畫畫的元素的寬度和高度有關(guān),是的,你猜的沒錯,它就好像你設(shè)置一個 div 元素的高度和寬度一樣,代表著你將在畫布上畫的截圖的寬高屬性。
現(xiàn)在只剩下第一個參數(shù)還沒解釋,這里直接說答案,我們可以直接將 video
標(biāo)簽填進(jìn)去,ctx 會自動將當(dāng)前 video
標(biāo)簽的這一幀畫面填寫進(jìn)去。現(xiàn)在按鈕的代碼應(yīng)該是這個樣子。
vue
復(fù)制代碼
function shoot() {
if (!videoEl.value || !wrapper.value) return;
const canvas = document.createElement('canvas');
canvas.width = videoEl.value.videoWidth;
canvas.height = videoEl.value.videoHeight;
//拿到 canvas 上下文對象
const ctx = canvas.getContext('2d');
ctx?.drawImage(videoEl.value, 0, 0, canvas.width, canvas.height);
wrapper.value.appendChild(canvas);//將 canvas 投到頁面上
}
測試一下效果。
<script lang='ts' setup>
import { ref, onMounted } from 'vue';
const wrapper = ref<HTMLDivElement>();
const videoEl = ref<HTMLVideoElement>();
async function checkCamera() {
const navigator = window.navigator.mediaDevices;
const devices = await navigator.enumerateDevices();
if (devices) {
const stream = await navigator.getUserMedia({
audio: false,
video: {
width: 300,
height: 300,
// facingMode: { exact: 'environment' }, //強制后置攝像頭
facingMode: 'user', //前置攝像頭
},
});
if (!videoEl.value) return;
videoEl.value.srcObject = stream;
videoEl.value.play();
}
}
function shoot() {
if (!videoEl.value || !wrapper.value) return;
const canvas = document.createElement('canvas');
canvas.width = videoEl.value.videoWidth;
canvas.height = videoEl.value.videoHeight;
//拿到 canvas 上下文對象
const ctx = canvas.getContext('2d');
ctx?.drawImage(videoEl.value, 0, 0, canvas.width, canvas.height);
wrapper.value.appendChild(canvas);
}
onMounted(() => {
checkCamera();
});
</script>
<template>
<div ref='wrapper' class='w-full h-full bg-red flex flex-col items-center'>
<video ref='videoEl' />
<div
@click='shoot'
class='w-100px leading-100px text-center bg-black text-30px'
>
拍攝
</div>
</div>
</template>
實現(xiàn)拍照的整體思路其實很簡單,僅僅需要了解到視頻其實也是一幀一幀畫面構(gòu)成的,而 canvas 恰好有捕捉當(dāng)前幀的能力。
聯(lián)系客服