什么是取景器
取景器是什么?取景器是相机的一个专业术语,在前端就是扫描拍照
取景器的实现原理
请求手机的一个媒体类型的视频轨道,利用一个div或者图片作为上层蒙层,然后在利用canvas绘制视频中某一帧的画面绘制为图片。
前期知识准备
- # MediaDevices.getUserMedia()
MediaDevices.getUserMedia() - Web API 接口参考 | MDN 在mdn中介绍了,这个api会调取摄像头,获取一个媒体轨道,这里我们只需要重点关注视频轨道,mdn对api的讲解也很清楚,下面是mdn的做好兼容的代码只需要直接使用
值得注意的点
- 该api必须https
- 兼容老版本浏览器
// 老的浏览器可能根本没有实现 mediaDevices,所以我们可以先设置一个空的对象
if (navigator.mediaDevices === undefined) {
navigator.mediaDevices = {};
}
// 一些浏览器部分支持 mediaDevices。我们不能直接给对象设置 getUserMedia
// 因为这样可能会覆盖已有的属性。这里我们只会在没有 getUserMedia 属性的时候添加它。
if (navigator.mediaDevices.getUserMedia === undefined) {
navigator.mediaDevices.getUserMedia = function(constraints) {
// 首先,如果有 getUserMedia 的话,就获得它
var getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
// 一些浏览器根本没实现它 - 那么就返回一个 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);
});
}
}
navigator.mediaDevices.getUserMedia(
{
audio: false, //不需要获取声音
video: {
//后置摄像头
facingMode: 'environment',
//分辨率,理想是2040,如果没有,最小1280,没有设置会比较模糊
width: { min: 1280, ideal: 2040 },
height: { min: 720, ideal: 1080 },
},
}
)
.then(function(stream) {
var video = document.querySelector('video');
// 旧的浏览器可能没有 srcObject
if ("srcObject" in video) {
video.srcObject = stream;
} else {
// 防止在新的浏览器里使用它,应为它已经不再支持了
video.src = window.URL.createObjectURL(stream);
}
video.onloadedmetadata = function(e) {
video.play();
};
})
.catch(function(err) {
console.log(err.name + ": " + err.message);
});
-
canvas绘图-drawImage
HTML5 canvas drawImage() 方法 canvas绘制,主要是要注意绘制过程中的参数,w3cschool也写的非常清楚,暂不做重复说明
案例源码
html部分
<div class="viewfinder">
<!-- 用于兼容,若不能使用,调用手机拍照功能 -->
<input id="file" type="file" accept="image/*" capture="camera" style="display:none"/>
<div class="wrap-box">
<!-- 视频整个页面 -->
<video style="height: 100vh;width: 100vw;position: fixed;top: 0;left: 0;object-fit: cover"/>
<div class="imgshow">
<div v-if="status==1" class="box"></div>
<!-- b.拍摄完后展示抓拍图片 -->
<img v-if="status==2" :src="imageUrl" alt="" class="wrapImg">
</div>
<button class="snap" @click="snapPhoto">拍照</button>
<!-- 抓拍 -->
<canvas id="mycanvas" style="visibility: hidden;"/>
</div>
</div>
css部分
<style lang="less" scoped>
.wrap-box {
width: 100%;
position: fixed;
left: 0;
height: 100%;
.imgshow {
width: 100%;
position: absolute;
left: 0;
bottom: 25vh;
top: 25vh;
right: 0;
}
.box {
width: 100%;
height: 100%;
border: 10px solid rgba(0,0,0,0.5);box-sizing: border-box;
}
.wrapImg{
width:100%;
height: 100%;
}
.snap {
position: fixed;
left: 0;
top: 2vh;
z-index: 1000;
}
}
</style>
js逻辑部分
<script>
export default {
data() {
return {
status: 0, // 自定义相机-拍摄进度:0|未开启 1|开启但未拍摄 2|开启且已拍摄
imageUrl: '',
};
},
mounted() {
//打开视频轨道
this.openCamera();
},
methods: {
openCamera() {
const constraints = {
audio: false,
video: {
facingMode: 'environment',
width: { min: 1280, ideal: 2040 },
height: { min: 720, ideal: 1080 }
},
};
// 兼容
if (navigator.mediaDevices === undefined) navigator.mediaDevices = {};
if (navigator.mediaDevices.getUserMedia === undefined) {
navigator.mediaDevices.getUserMedia = function(constraints) {
const getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia || navigator.oGetUserMedia;
if (!getUserMedia) {
return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
}
return new Promise(function(resolve, reject) {
getUserMedia.call(navigator, constraints, resolve, reject);
});
};
}
// 获取视频流
const that = this;
navigator.mediaDevices.getUserMedia(constraints)
.then(function(stream) {
const video = document.querySelector('video');
video.srcObject = stream;
video.onloadedmetadata = function() {
video.play();
};
that.status = 1;
})
.catch(function(err) {
// 不兼容调用原始相机拍照;
that.originCamera();
});
},
originCamera() {
const promise = new Promise(function(resolve, reject) {
const file = document.getElementById('file');
file.click();
file.onchange = function(event) {
if (!event) {
reject('empty');
}
// 当选中或者拍摄后确定图片后
const file = event.target.files[0];
resolve(file);
};
});
promise.then(value => {
//提交照片
that.submitPhoto('origin', value);
}
);
},
snapPhoto() {
const canvas = document.querySelector('#mycanvas');
const video = document.querySelector('video');
let width = canvas.width = video.videoWidth;
let height = canvas.height = video.videoHeight;
let cut_y = height/4
//只画取景框内的图片
canvas.getContext('2d').drawImage(video, 0, cut_y, width, cut_y*2, 0, 0, width, height);
// 将canvas保存为图片
this.imageFile = this.canvasToFile(canvas);
// blob转url:用于展示
const p = new Promise((resolve) => {
canvas.toBlob(blob => {
const url = URL.createObjectURL(blob);
resolve(url);
});
});
const that = this;
p.then(value => {
that.imageUrl = value;
that.status = 2;// 表示拍摄完成
});
},
canvasToFile(canvas) {
const dataurl = canvas.toDataURL('image/png');
let 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);
}
const file = new File([ u8arr ], 'phone.png', { type: mime });
return file;
},
// 提交
submitPhoto(type, file) {
if (type == 'origin') {
this.imageFile = file;
}
},
},
};
</script>
整个功能就完结啦 参考以下文章
- Html5调用手机摄像头后添加取景框并使用WebUploader上传 - 灰信网(软件开发博客聚合)
- 兼容性相关:h5调用摄像头拍照兼容性及原生实现拍照取景框 - 简书