參考文章:
- 實現的demo:https://www.freesion.com/article/67641324321/
- getUserMedia:https://developer.mozilla.org/zh-CN/docs/Web/API/MediaDevices/getUserMedia
- 一些問題合集:https://www.jb51.net/html5/722394.html
- 無法使用getUserMedia的原因:https://stackoverflow.com/questions/58808091/how-can-i-replace-the-getusermedia-function-in-mozilla
效果展示
html部分
主要分為四部分
- 啟動按鈕
- 原始相機
- 自定義相機(重點)
- 上傳提示
<div class="uploadFacePic">
<!-- 頭部 省略-->
<!-- 啟動相機按鈕-->
<main style="marginTop:20px">
<div class="btn" @click="openCamera">開啟人臉采集</div>
</main>
<!-- 1 原始相機 不兼容時的回退方案-->
<input id="file" type="file" accept="image/*" capture="camera" style="display:none">
<!-- 2 自定義相機 -->
<div style="width: 100%; position: fixed; left: 0; bottom: 0; top: 0; right: 0;" v-if="cameraShow">
<!-- 頂部樣式 -->
<div style="width: 100%; position: fixed; left: 0; bottom: 90vh; top: 0; right: 0;background:black">
</div>
<!-- 中間部分 -->
<video style="height: 65vh;width: 100vw;position: fixed;top: 10vh;left: 0;"></video>
<div style="width: 100%; position: fixed; left: 0; bottom: 25vh; top: 10vh; right: 0;">
<!-- a.拍攝時展示取景框。把取景框圖片換下就可以了 -->
<img src="../../assets/qujing.png" alt="" v-if="status==1" style="width: 100%;height: 100%;opacity: 0.8">
<!-- b.拍攝完后展示抓拍圖片 -->
<img :src="imageUrl" alt="" v-if="status==2" style="width:100%;height:100%">
</div>
<!-- 底下控制部分-->
<div class="control">
<!-- 拍照前 -->
<div class="control_before" v-if="status==1">
<div class="control_before_top">照片</div>
<div class="control_before_bottom">
<div class="smaller" @click="cameraShow=false">取消</div>
<i class="iconfont icon-xiangji bigger" @click="snapPhoto"></i>
<i class="iconfont icon-zhongxin small" @click="front = !front"></i>
</div>
</div>
<!-- 拍照后 -->
<div class="control_after" v-if="status==2">
<div class="smaller" @click="status=1">重拍</div>
<div class="smaller" @click="submitPhoto('custom')">使用照片</div>
</div>
</div>
<!-- 抓拍 -->
<canvas id="mycanvas"></canvas>
</div>
<!-- 提示部分 -->
<div class="tipinfo" v-if="tipVisible">
<div class="successContent" v-if="tipInfo.result=='ok'">
<van-icon name="passed"/>
<div class="title">采集成功</div>
<div class="info">恭喜您,完成人臉照片采集</div>
<div class="btn" @click="tipVisible=false">{{'返回'+btntext}}</div>
</div>
<div class="failContent" v-else>
<van-icon name="warning-o" />
<div class="title">采集失敗</div>
<div class="info">{{tipInfo.msg+',請重拍'}}</div>
<div class="btn" @click="tipVisible=false">{{'返回'+btntext}}</div>
</div>
</div>
</div>
js部分
變量部分:
data() {
return {
type:'',//上傳類型 update|upload
cameraShow:false,//啟動自定義相機
status:0,//自定義相機-拍攝進度:0|未開啟 1|開啟但未拍攝 2|開啟且已拍攝
imageUrl:'',//自定義相機-抓拍url
front:true,// 自定義相機-前置與后置轉換(未驗證)
//提示部分
tipVisible:false,
tipInfo:{
result:'fail',
msg:'采集人臉失敗'
},//上傳結果
btntext:'',//倒計時文本
time:null,//計時器
imageFile:'',//圖片對象
};
},
啟動相機
openCamera:主要做了一些兼容與回退。
openCamera() {
// 1. 先展示,因為要從這里獲取video標簽
this.cameraShow=true
// 2. constraints:指定請求的媒體類型和相對應的參數
var constraints={
audio: false,
video: {
facingMode: (this.front? "user" : "environment")
}
}
// 3. 兼容部分:
// 老的瀏覽器可能根本沒有實現 mediaDevices,所以我們可以先設置一個空的對象
if (navigator.mediaDevices === undefined) {
navigator.mediaDevices = {};
}
// 一些瀏覽器部分支持 mediaDevices。我們不能直接給對象設置 getUserMedia
// 因為這樣可能會覆蓋已有的屬性。這里我們只會在沒有getUserMedia屬性的時候添加它。
if (navigator.mediaDevices.getUserMedia === undefined) {
navigator.mediaDevices.getUserMedia = function(constraints) {
// 首先,如果有getUserMedia的話,就獲得它
var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia || navigator.oGetUserMedia;
// 一些瀏覽器根本沒實現它 - 那么就返回一個error到promise的reject來保持一個統一的接口
if (!getUserMedia) {
return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
}
// 否則,為老的navigator.getUserMedia方法包裹一個Promise
return new Promise(function(resolve, reject) {
getUserMedia.call(navigator, constraints, resolve, reject);
});
}
}
// 4. 獲取視頻流
let that=this
navigator.mediaDevices.getUserMedia(constraints)
.then(function(stream) {
// 進來這里表示能夠兼容
let video=document.querySelector('video');
video.srcObject = stream;
video.onloadedmetadata = function(e) {
video.play();
};
// 進入自定義拍攝模式
that.status=1
})
.catch(function(err) {
// 進來這里表示不能兼容
console.log('nonono',err)
// 調用原始攝像頭
that.originCamera()
});
},
方案一:兼容
snapPhoto:抓拍
snapPhoto(){
var canvas = document.querySelector('#mycanvas');
var video = document.querySelector('video');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video,0,0);
// 保存為文件,用於后續的上傳至服務器(尚未實踐)——>后續提交
this.imageFile=this.canvasToFile(canvas)
// blob轉url:用於展示
let p=new Promise((resolve,reject)=>{
canvas.toBlob(blob=>{
let url=URL.createObjectURL(blob)
resolve(url)
});
})
let that=this
p.then(value=>{
that.imageUrl=value
that.status=2//表示拍攝完成
})
},
canvasToFile:canvas轉為文件格式,用於上傳服務器
canvasToFile(canvas){
var dataurl = canvas.toDataURL("image/png");
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
var file = new File([u8arr], "phone.png", {type: mime});
return file
},
方案二:不兼容
originCamera:調用原始攝像機
originCamera(){
let that=this
//關閉自定義相機
that.cameraShow=false
let promise= new Promise(function (resolve, reject) {
let file=document.getElementById('file')
file.click()
file.onchange = function (event) {
if (!event) {
reject('empty')
}
//當選中或者拍攝后確定圖片后,保存圖片文件——>后續提交
let file=event.target.files[0]
resolve(file)
}
})
promise.then((value)=> {
that.submitPhoto('origin',value)
}
)
},
提交與上傳提示
submitPhoto(type,file) {
if(type=='origin'){
this.imageFile=file
}
console.log("提交",this.imageFile);
//在這里進行上傳操作
// let fd=new FormData()
// fd.append("face_image",this.imageFile)
//...
//上傳成功時:
this.tipInfo.result='ok'
// this.tipInfo.result='fail'
this.cameraShow=false
//開始提示
this.countdown()
},
//倒計時與提示
countdown(){
clearInterval(this.time);
this.tipVisible=true
let coden = 3;
this.btntext = '('+coden+'s)';
this.time = setInterval(() => {
coden--
this.btntext = '('+coden+'s)';
if (coden == 0) {
clearInterval(this.time);
this.tipVisible = false;
this.btntext = "";
}
}, 1000);
},
css部分
.uploadFacePic{
.img{
width:100%;
height:100%
}
.bigger{
font-weight: 600;
font-size: 3em;
}
.small{
font-size: 2em;
}
.smaller{
font-size: 1.2em;
}
// 控制台
.control{
width: 100%;
position: fixed;
left: 0;
bottom: 0;
top: 75vh;
right: 0;
background:black;
.control_before{
position:relative;
width:100%;
height:100%;
display: flex;
flex-direction: column;
.control_before_top{
z-index: 1;
flex: 1;
color: orange;
display: flex;
justify-content: center;
align-items: center;
}
.control_before_bottom{
flex: 2;
display: flex;
justify-content: space-around;
color: white;
align-items: center;
margin-bottom: 1.5em;
}
}
.control_after{
position:relative;
width:100%;
height:100%;
display: flex;
color: white;
align-items: center;
justify-content: space-around;
}
}
main{
text-align: center;
}
.tipinfo{
z-index: 2;
position: fixed;
top:46px;
left: 0;
right: 0;
bottom: 0;
background: white;
display: flex;
justify-content: center;
align-items: center;
.successContent{
.van-icon{
color: #f68618;
font-size: 5em;
}
.title{
color: #f68618;
font-size: 1.8em;
}
}
.failContent{
.van-icon{
color: red;
font-size: 5em;
}
.title{
color: red;
font-size: 1.8em;
}
}
.info{
margin: 1em 0 3em;
}
}
.btn{
height: 34px;
width: 80vw;
background: #f68618;
color: white;
display: flex;
justify-content: center;
align-items: center;
border-radius: .3em;
margin: 0 auto;
}
}
后記
- 部分icon是從阿里導入的,這里沒有寫出來。
- 目前在微信開發者工具能夠看到自定義相機的效果,在真機上(http)還啟動不了,查閱資料說navigator.mediaDevices.getUserMedia服務器需要用https(尚未驗證)
- 自定義相機自拍時會出現鏡像效果,暫時不知道怎么處理。
- 中間上傳時估計要做一個加載ui進行過渡
優化:video自適應大小
在html中,屏幕中顯示的可見視圖大小其實我們一開始就規定好了,但是不同設備有它的大小,那怎么統一且自適應呢?
-
video標簽原始的object-fit的屬性值為contain,這就造成了問題:沒有辦法填充我們定義的可見視圖大小——解決:
object-fit:cover
<video style="object-fit: cover"></video>
-
然額還沒結束。了解
object-fit:cover
的原理后,canvas也應該對應變化。cover的情況下,當寬高不一致時,超出部分會被剪切。此外,cover會自適應,即它不會只從一邊裁剪,而是端水從兩邊等份裁剪。var canvas = document.querySelector('#mycanvas'); var video = document.querySelector('video'); // 基於object-fit:cover // 原始寬高 let width=video.videoWidth let height=video.videoHeight //判斷寬高是否一致 const WIDTH_UNEQUAL_HEIGHT=width!=height //被裁減掉的一邊的大小 const SPAN=Math.abs(width-height)/2 //根據差值定義裁剪起點x,y。 //例:如果高比寬高出10,那么上面會被裁減掉5,那么起點y就等於5,這樣就保證了裁剪到的是可視圖片 let cut_x=WIDTH_UNEQUAL_HEIGHT?SPAN:0 //裁剪起點x let cut_y=WIDTH_UNEQUAL_HEIGHT?0:SPAN //裁剪起點y //如果裁剪過,寬度要把裁剪部分去掉。 let cut_after_width=cut_x!=0?width-2*SPAN:width let cut_after_height=cut_y!=0?height-2*SPAN:height //繪制畫布(裁剪后的) canvas.width = cut_after_width; canvas.height = cut_after_height; canvas.getContext('2d').drawImage(video,cut_x,cut_y,cut_after_width,cut_after_height,0,0,cut_after_width,cut_after_height)
ok完成。
優化:video鏡像翻轉
受前面的啟發。
<video style="transform: rotateY(180deg);"></video>
稍微改寫一下畫布
let context=canvas.getContext('2d')
context.drawImage(video,cut_x,cut_y,cut_after_width,cut_after_height,0,0,cut_after_width,cut_after_height)
// 如果是前置 需要鏡像翻轉
if(this.front){
context.scale(-1, 1);
context.drawImage(video,cut_x,cut_y, width*-1,height);
}
但是查到資料說ios的scale無法接收負數。。