背景
不久前我做了關於獲取瀏覽器攝像頭並掃碼識別的功能,本文中梳理了涉及到知識點及具體程式碼實現,整理成此篇文章內容。
本文主要介紹,通過使用基於 vue
技術棧的前端開發技術,在瀏覽器端調起攝像頭 ?
,並進行掃碼識別功能,對識別到的二維碼進行跳轉或其他操作處理。本文內容分為背景介紹、實現效果、技術簡介、程式碼實現、總結等部分組成。
實現效果
本例項中主要有兩個頁面首頁和掃碼頁,具體實現效果如下圖所示。
- 首頁:點選
SCAN QRCODE
按鈕,進入到掃碼頁。 - 掃碼頁:首次進入時,或彈出
獲取攝像頭訪問許可權的系統提示框
,點選允許訪問,頁面開始載入攝像頭資料並開始進行二維碼捕獲拾取,若捕獲到二維碼,開始進行二維碼解析,解析成功後載入識別成功彈窗。
?
提示:需要在有攝像頭裝置的瀏覽器中豎屏訪問。手機橫豎屏檢測小知識可前往我的另一篇文章《五十音小遊戲中的前端知識》 中進行了解。
技術簡介
WebRTC API
WebRTC (Web Real-Time Communications) 是一項實時通訊技術,它允許網路應用或者站點,在不借助中間媒介的情況下,建立瀏覽器之間 點對點(Peer-to-Peer)
的連線,實現視訊流和(或)音訊流或者其他任意資料的傳輸。WebRTC
包含的這些標準使使用者在無需安裝任何外掛或者第三方的軟體的情況下,建立 點對點(Peer-to-Peer)
的資料分享和電話會議成為可能。
三個主要介面:
MediaStream
:能夠通過裝置的攝像頭及話筒獲得視訊、音訊的同步流。RTCPeerConnection
:是WebRTC
用於構建點對點之間穩定、高效的流傳輸的元件。RTCDataChannel
:使得瀏覽器之間建立一個高吞吐量、低延時的通道,用於傳輸任意資料。
?
前往MDN
深入學習:WebRTC_API
WebRTC adapter
雖然 WebRTC
規範已經相對健全穩固了,但是並不是所有的瀏覽器都實現了它所有的功能,有些瀏覽器需要在一些或者所有的 WebRTC API
上新增字首才能正常使用。
WebRTC
組織在 github
上提供了一個 WebRTC介面卡(WebRTC adapter)
來解決在不同瀏覽器上實現 WebRTC
的相容性問題。這個介面卡是一個 JavaScript墊片
,它可以讓你根據 WebRTC
規範描述的那樣去寫程式碼,在所有支援 WebRTC
的瀏覽器中不用去寫字首或者其他相容性解決方法。
?
前往MDN
深入學習:WebRTC adapter
核心的API navigator.mediaDevices.getUserMedia
網頁呼叫攝像頭需要呼叫 getUserMedia API
,MediaDevices.getUserMedia()
會提示使用者給予使用媒體輸入的許可,媒體輸入會產生一個 MediaStream
,裡面包含了請求的媒體型別的軌道。此流可以包含一個視訊軌道(來自硬體或者虛擬視訊源,比如相機、視訊採集裝置和螢幕共享服務等等)、一個音訊軌道(同樣來自硬體或虛擬音訊源,比如麥克風、A/D轉換器
等等),也可能是其它軌道型別。
它返回一個 Promise
物件,成功後會 resolve
回撥一個 MediaStream物件
;若使用者拒絕了使用許可權,或者需要的媒體源不可用,promise
會 reject
回撥一個 PermissionDeniedError
或者 NotFoundError
。(返回的 promise物件
可能既不會 resolve
也不會 reject
,因為使用者不是必須選擇允許或拒絕。)
通常可以使用 navigator.mediaDevices
來獲取 MediaDevices
,例如:
navigator.mediaDevices.getUserMedia(constraints)
.then(function(stream) {
// 使用這個stream
})
.catch(function(err) {
// 處理error
})
?
前往MDN
深入學習:navigator.mediaDevices.getUserMedia
二維碼解析庫 JSQR
jsQR
是一個純 JavaScript
二維碼解析庫,該庫讀取原始影像或者是攝像頭,並將定位,提取和解析其中的任何 QR碼
。
如果要使用 jsQR
掃描網路攝像頭流,則需要 ImageData
從視訊流中提取,然後可以將其傳遞給 jsQR
。
jsQR
匯出一個方法,該方法接受 4
個引數,分別是解碼的 影像資料
,寬
、高
以及 可選的物件
進一步配置掃描行為。
imageData
:格式為 [r0, g0, b0, a0, r1, g1, b1, a1, ...]
的 Uint8ClampedArray( 8位無符號整型固定陣列)
的 rgba
畫素值。
const code = jsQR(imageData, width, height, options);
if (code) {
console.log('找到二維碼!', code);
}
?
前往github
深入瞭解:jsQR
程式碼實現
流程
整個掃碼流程如下圖所示:頁面初始化,先檢查瀏覽器是否支援 mediaDevices
相關API
,瀏覽器進行調去攝像頭,呼叫失敗,就執行失敗回撥;呼叫成功,進行捕獲視訊流,然後進行掃碼識別,沒有掃瞄到可識別的二維碼就繼續掃描,掃碼成功後繪製掃描成功圖案並進行成功回撥。
下文內容對流程進行拆分,分別實現對應的功能。
掃碼元件 Scaner
頁面結構
我們先看下頁面結構,主要由 4
部分組成:
- 提示框。
- 掃碼框。
video
:展示攝像頭捕獲視訊流。canvas
: 繪製視訊幀,用於二維碼識別。
<template>
<div class="scaner" ref="scaner">
<!-- 提示框:用於在不相容的瀏覽器中顯示提示語 -->
<div class="banner" v-if="showBanner">
<i class="close_icon" @click="() => showBanner = false"></i>
<p class="text">若當前瀏覽器無法掃碼,請切換其他瀏覽器嘗試</p>
</div>
<!-- 掃碼框:顯示掃碼動畫 -->
<div class="cover">
<p class="line"></p>
<span class="square top left"></span>
<span class="square top right"></span>
<span class="square bottom right"></span>
<span class="square bottom left"></span>
<p class="tips">將二維碼放入框內,即可自動掃描</p>
</div>
<!-- 視訊流顯示 -->
<video
v-show="showPlay"
class="source"
ref="video"
:width="videoWH.width"
:height="videoWH.height"
controls
></video>
<canvas v-show="!showPlay" ref="canvas" />
<button v-show="showPlay" @click="run">開始</button>
</div>
</template>
方法:繪製
- 畫線。
- 畫框(用於掃碼成功後繪製矩形圖形)。
// 畫線
drawLine (begin, end) {
this.canvas.beginPath();
this.canvas.moveTo(begin.x, begin.y);
this.canvas.lineTo(end.x, end.y);
this.canvas.lineWidth = this.lineWidth;
this.canvas.strokeStyle = this.lineColor;
this.canvas.stroke();
},
// 畫框
drawBox (location) {
if (this.drawOnfound) {
this.drawLine(location.topLeftCorner, location.topRightCorner);
this.drawLine(location.topRightCorner, location.bottomRightCorner);
this.drawLine(location.bottomRightCorner, location.bottomLeftCorner);
this.drawLine(location.bottomLeftCorner, location.topLeftCorner);
}
},
方法:初始化
- 檢查是否支援。
- 調起攝像頭。
- 成功失敗處理。
// 初始化
setup () {
// 判斷了瀏覽器是否支援掛載在MediaDevices.getUserMedia()的方法
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
this.previousCode = null;
this.parity = 0;
this.active = true;
this.canvas = this.$refs.canvas.getContext("2d");
// 獲取攝像頭模式,預設設定是後置攝像頭
const facingMode = this.useBackCamera ? { exact: 'environment' } : 'user';
// 攝像頭視訊處理
const handleSuccess = stream => {
if (this.$refs.video.srcObject !== undefined) {
this.$refs.video.srcObject = stream;
} else if (window.videoEl.mozSrcObject !== undefined) {
this.$refs.video.mozSrcObject = stream;
} else if (window.URL.createObjectURL) {
this.$refs.video.src = window.URL.createObjectURL(stream);
} else if (window.webkitURL) {
this.$refs.video.src = window.webkitURL.createObjectURL(stream);
} else {
this.$refs.video.src = stream;
}
// 不希望使用者來拖動進度條的話,可以直接使用playsinline屬性,webkit-playsinline屬性
this.$refs.video.playsInline = true;
const playPromise = this.$refs.video.play();
playPromise.catch(() => (this.showPlay = true));
// 視訊開始播放時進行週期性掃碼識別
playPromise.then(this.run);
};
// 捕獲視訊流
navigator.mediaDevices
.getUserMedia({ video: { facingMode } })
.then(handleSuccess)
.catch(() => {
navigator.mediaDevices
.getUserMedia({ video: true })
.then(handleSuccess)
.catch(error => {
this.$emit("error-captured", error);
});
});
}
},
方法:週期性掃描
run () {
if (this.active) {
// 瀏覽器在下次重繪前迴圈呼叫掃碼方法
requestAnimationFrame(this.tick);
}
},
方法:成功回撥
// 二維碼識別成功事件處理
found (code) {
if (this.previousCode !== code) {
this.previousCode = code;
} else if (this.previousCode === code) {
this.parity += 1;
}
if (this.parity > 2) {
this.active = this.stopOnScanned ? false : true;
this.parity = 0;
this.$emit("code-scanned", code);
}
},
方法:停止
// 完全停止
fullStop () {
if (this.$refs.video && this.$refs.video.srcObject) {
// 停止視訊流序列軌道
this.$refs.video.srcObject.getTracks().forEach(t => t.stop());
}
}
方法:掃描
- 繪製視訊幀。
- 掃碼識別。
// 週期性掃碼識別
tick () {
// 視訊處於準備階段,並且已經載入足夠的資料
if (this.$refs.video && this.$refs.video.readyState === this.$refs.video.HAVE_ENOUGH_DATA) {
// 開始在畫布上繪製視訊
this.$refs.canvas.height = this.videoWH.height;
this.$refs.canvas.width = this.videoWH.width;
this.canvas.drawImage(this.$refs.video, 0, 0, this.$refs.canvas.width, this.$refs.canvas.height);
// getImageData() 複製畫布上制定矩形的畫素資料
const imageData = this.canvas.getImageData(0, 0, this.$refs.canvas.width, this.$refs.canvas.height);
let code = false;
try {
// 識別二維碼
code = jsQR(imageData.data, imageData.width, imageData.height);
} catch (e) {
console.error(e);
}
// 如果識別出二維碼,繪製矩形框
if (code) {
this.drawBox(code.location);
// 識別成功事件處理
this.found(code.data);
}
}
this.run();
},
父元件
Scaner
的父元件主要載入頁面,並展示 Scaner
掃碼結果的回撥。
頁面結構
<template>
<div class="scan">
<!-- 頁面導航欄 -->
<div class="nav">
<a class="close" @click="() => $router.go(-1)"></a>
<p class="title">Scan QRcode</p>
</div>
<div class="scroll-container">
<!-- 掃碼子元件 -->
<Scaner
v-on:code-scanned="codeScanned"
v-on:error-captured="errorCaptured"
:stop-on-scanned="true"
:draw-on-found="true"
:responsive="false"
/>
</div>
</div>
</template>
父元件方法
import Scaner from '../components/Scaner';
export default {
name: 'Scan',
components: {
Scaner
},
data () {
return {
errorMessage: "",
scanned: ""
}
},
methods: {
codeScanned(code) {
this.scanned = code;
setTimeout(() => {
alert(`掃碼解析成功: ${code}`);
}, 200)
},
errorCaptured(error) {
switch (error.name) {
case "NotAllowedError":
this.errorMessage = "Camera permission denied.";
break;
case "NotFoundError":
this.errorMessage = "There is no connected camera.";
break;
case "NotSupportedError":
this.errorMessage =
"Seems like this page is served in non-secure context.";
break;
case "NotReadableError":
this.errorMessage =
"Couldn't access your camera. Is it already in use?";
break;
case "OverconstrainedError":
this.errorMessage = "Constraints don't match any installed camera.";
break;
default:
this.errorMessage = "UNKNOWN ERROR: " + error.message;
}
console.error(this.errorMessage);
alert('相機呼叫失敗');
}
},
mounted () {
var str = navigator.userAgent.toLowerCase();
var ver = str.match(/cpu iphone os (.*?) like mac os/);
// 經測試 iOS 10.3.3以下系統無法成功呼叫相機攝像頭
if (ver && ver[1].replace(/_/g,".") < '10.3.3') {
alert('相機呼叫失敗');
}
}
完整程式碼
總結
應用擴充套件
我覺得以下幾個功能都是可以通過瀏覽器呼叫攝像頭並掃描識別來實現的,大家覺得還有哪些 很哇塞?
的功能應用可以通過瀏覽器端掃碼實現 ?
?
?
連結跳轉。?
價格查詢。?
登入認證。?
檔案下載。
相容性
❗
即使使用了adapter
,getUserMedia API
在部分瀏覽器中也存在不支援的。❗
低版本瀏覽器(如iOS 10.3
以下)、Android
小眾瀏覽器(如IQOO
自帶瀏覽器)不相容。❗
QQ
、微信
內建瀏覽器無法呼叫。
參考資料
- [1]. Taking still photos with WebRTC
- [2]. Choosing cameras in JavaScript with the mediaDevices API
- [3]. 如何使用JavaScript訪問裝置前後攝像頭
作者:dragonir 本文地址:https://www.cnblogs.com/dragonir/p/15405141.html