前言
- 在写 JS30 19 - Webcam Fun 时发现是个拍照并保存的需求,之前写项目时也遇到了这方面内容,并且当时还需要生成和扫描二维码(扫描是 App 带有的),这部分当时因为一些原因未自己动手,现在遇到了正好了解并学习拓展一下。
BTW
,这作者当时咋想的做了个这样示例,挺抽象的,昨天刚觉得他设计天赋🐂呢
正文
摄像头
获取摄像头权限
通过 Navigator.mediaDevices
只读属性返回一个 MediaDevices
对象,该对象可提供对相机和麦克风等媒体输入设备以及屏幕共享的连接访问。
- 本文中主要用到
enumerateDevices
查询是否有媒体设备以及getUserMedia
方法实现对 audio 以及 video 权限的获取 - 至于屏幕共享为getDisplayMedia 方法,暂未尝试
查询媒体设备
- 打印出来是个数组,里面有各个媒体设备的信息
js
async function checkHasDevices(mediaDevices) {
try {
const devices = await mediaDevices.enumerateDevices();
return devices
} catch (err) {
console.log(err.name + ": " + err.message);
return []
}
}
申请权限
- 有了媒体设备,使用
getUserMedia
函数即可,这个函数的参数是一个对象
,里面包括对 audio 以及 video 详细的配置 - 再就是注意下是
video.srcObject
,因为 stream 是二进制流
的形式
js
const video = document.querySelector('.player');
async function getVideoPermission() {
const mediaDevices = navigator.mediaDevices;
const devices = await checkHasDevices(mediaDevices);
console.log(devices);
if (devices.length > 0) {
const constraints = {
audio: false,
video: {
width: 1920, //分辨率,ideal 意为理想情况
// { min: 1024, ideal: 1280, max: 1920 },
height: 1080, //分辨率,ideal 意为理想情况
// { min: 776, ideal: 720, max: 1080 },
facingMode: "user" // 前置
// facingMode: {exact:"environment" } //后置,exact 为强制
}
}
try {
const stream = await mediaDevices.getUserMedia(constraints);
video.srcObject = stream;
video.play();
} catch (err) {
console.log(err);
}
} else {
console.log("No devices found.");
}
}
- 发现笔记本设置 facingMode 无论为哪个值都是调前置,如果使用强制后置摄像头的话就会抛出 OverconstrainedError了🤨
截屏
好!现在已经踏出了第一步!接下来进行截屏操作!
- 截屏其实很简单,因为视频是一帧一帧的嘛,截屏就是拿到某一帧然后展示,在此之前需要先了解下 Canvas 的基本用法
js
function screenshot() {
const width = video.videoWidth;
const height = video.videoHeight;
canvas.width = width;
canvas.height = height;
ctx.drawImage(video, 0, 0, width, height);
}
- 要下载的话就加个 a 标签自动触发下就行
js
function screenshot() {
const width = video.videoWidth;
const height = video.videoHeight;
canvas.width = width;
canvas.height = height;
ctx.drawImage(video, 0, 0, width, height);
const data = canvas.toDataURL();// 参数可指定图片类型
const link = document.createElement('a');
link.href = data;
link.setAttribute('download', 'handsome');
link.click()
}
扫一扫
简单认识
- 二维码其实就是一个 Url 地址以一种形式存放到我们常见的那种图像里,通过识别可以得到其中的地址
- 可以在
Antd
的组件 QRCode试试 - 使用的扫一扫为社区找到的一个项目 qr-sanner-wechat
识别
- 因为浏览器不支持模块化,用原生 JS 写的话还得再用打包工具打包,得配置一堆东西,因此使用 vite 创了个简单的 Vanilla + TS项目来展示
ts
// main.ts
import { scanner } from "./counter"
document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
<input type="file" accept="image/png" />
<img src=" " alt="a" width='200' height='200' />
`
scanner(
document.querySelector<HTMLInputElement>('input')!,
document.querySelector<HTMLImageElement>('img')!
)
ts
//counter.ts
import { scan } from 'qr-scanner-wechat'
export function scanner(
input:HTMLInputElement,
img:HTMLImageElement,
container:HTMLDivElement
) {
input!.addEventListener('change',async (e:Event)=>{
const fileElement = (e.target as HTMLInputElement)
if(!fileElement.files?.length) return
const file = fileElement.files[0]
const value = URL.createObjectURL(file)
img!.src = value
img.addEventListener('load', async () => {
await coverQRCode(img, container);
})
})
}
- 我们可以发现打印出来的信息是这样
rect
: 即rectangle
,他告诉了你这个图片的一个相对位置以及宽高text
: 即这张图片对应的Url
地址
有了
text
,我们就可以轻松跳转到对应地址,完成对二维码的识别
拓宽思路
问题所在
-
很多扫一扫都是在有多个二维码时识别多个,然后让用户选择
-
试试这个图像如果直接丢给第三方库会如何
妈耶,🐓了,只有一个
那怎么办?
只要思想不滑坡,方法总比困难多
- 使用其它库
- 不能一起识别,就一个一个识别,识别一个遮住一个
注意到该库的的
scan
参数也可以是HTMLCanvasElement
类型,因此可以使用canvas
结合result 中的 rect 属性
来生成遮挡图形遮住已经扫描的二维码
ts
async function coverQRCode(img: HTMLImageElement, container: HTMLDivElement) {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx?.drawImage(img, 0, 0, img.width, img.height);
let continueScanning = true;
while (continueScanning) {
const res = await scan(canvas) as any
if (!res.text) {
continueScanning = false;
break;
}
scannedQRCodeInfo.push(res);
const { x, y, width, height } = res.rect;
ctx!.fillStyle = 'black';
ctx?.fillRect(x, y, width, height);
const codeCanvas = document.createElement('canvas');
codeCanvas.width = width;
codeCanvas.height = height;
const codeCtx = codeCanvas.getContext('2d');
codeCtx?.drawImage(canvas, x, y, width, height, 0, 0, width, height);
container.appendChild(codeCanvas);
}
}
芜湖!成了!
供用户选择跳转
都做到这一步了,那后面的岂不是🤲🏻🤲🏻🤲🏻🤲
易如反掌
易如反掌
嘛
- 可以加一个数组存放所有扫描得到的信息,方便在每一个二维码上显示选项
- 刚才是为了演示用黑块遮住了二维码,现在根据坐标信息在每个二维码上绘制一个小绿圈就行
ts
function draw(container:HTMLDivElement) {
scannedQRCodeInfo.forEach(({rect, text}:any) => {
const dom = document.createElement("div");
const { x, y, width, height } = rect;
const _x = (x || 0) + width / 2 - 20;
const _y = (y || 0) + height / 2 - 20;
dom.style.width = "40px";
dom.style.height = "40px";
dom.style.background = "green";
dom.style.position = "absolute";
dom.style.zIndex = "9";
dom.style.top = _y + "px";
dom.style.left = _x + "px";
dom.style.color = "#fff";
dom.style.textAlign = "center";
dom.style.borderRadius = "100px";
dom.style.borderBlockColor = "#fff";
dom.style.borderColor = "unset";
dom.style.borderRightStyle = "solid";
dom.style.borderWidth = "3px";
dom.addEventListener("click", () => {
console.log(text);
});
container.appendChild(dom);
});
}
最终效果
源码
放在了 Github 里,有兴趣可以拉下来玩玩 scanCode
结语
钻研这些东西还挺好玩的,这篇文章也是花费了几天的时间才写出来,中间甚至经历了28年难得一遇
的什么?当然是疯狂星期四
啦