欢迎来到feenan的藏宝洞 ! 这里是我的第二个宝贝哦,有眼光的宝子们看到就是赚到喽。

hhh,第二篇虽迟但到。

不管是老手还是新手们都常常会在学习过程中发现知识点真是太多啦!!!脑容量不够啦!!!!

今天又来喽,还有哪双大眼睛没有看老师?

要读懂本篇文章需要掌握的技能: js、 html。

一. 你还记得 Navigator 吗?

  我们在详细学习BOM的核心:window 的时候,应该都有了解过这个构造函数,它的实例就是 window 上的 navigator 对象。当初我在学习的过程中只是了解到它包含了客户端浏览器的信息,有一些 userAgent,online 等属性便于我们获取浏览器版本、内核、以及联网状态等等讯息的。呵呵呵最近进一步了解之后才发现,我还在 js 学前班。
  干货来了: navigator 上存在一个对象 mediaDevices(该对象可提供对相机和麦克风等媒体输入设备的连接访问,也包括屏幕共享),该对象也是我们本文的主角,它提供了一个API:getUserMedia, 能够直接获取到我们需要的媒体流。绝绝子呀,有这玩意儿,干点啥不香。

二. 媒体流 + video + canvas实现拍照

展示一下我同桌的靓包给大家浅看一下。
原理:navigator.mediaDevices提供的API获取媒体流 + h5标签video + canvas的drawImage方法绘制图像。
代码:html准备

1
2
3
4
5
6
<!-- autoplay:自动播放 -->
<!-- playsinline:禁止拖动进度条 -->
<!-- muted:视频的音频输出为静音 -->
<video id="myVideo" autoplay playsinline muted />
<button type="button">点击拍照</button>
<div class="imageList"></div>

js部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const owrapper = document.getElementsByClassName('imageList')[0];
const btn = document.getElementsByTagName('button')[0];

// 给button添加点击事件
btn.onclick = takePhoto;

// 拍照
function takePhoto() {
//实时拿才有效果
const myVideo = document.getElementById('myVideo');
const canvas = document.createElement('canvas')
canvas.width = myVideo.videoWidth
canvas.height = myVideo.videoHeight
const ctx = canvas.getContext('2d');
ctx.drawImage(myVideo, 0, 0, canvas.width, canvas.height);
// 生成一张直拍照片(无美颜的哦,嘿嘿)
addImage(canvas.toDataURL());
}

// 封装一个图片生成器
function addImage(value){
const image = document.createElement('img');
image.src = value;
image.style.width='100px';
image.style.height='100px';
owrapper.appendChild(image);
}

// 获取本地音视频流
async function getLocalStream(constraints) {
// 获取媒体流
const stream = await navigator.mediaDevices.getUserMedia(constraints)
// video拿到数据
const myVideo = document.getElementById('myVideo');
myVideo.srcObject = stream
}

// video开始播放(参数配置只获取视频不获取音频)
getLocalStream({
audio: false,
video: true,
})

如果想要加上美颜,就要上我们的filter神器啦:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 // 添加滤镜
const filterList = [
'blur(2px)', //模糊
'brightness(0.8)', //亮度
'contrast(200%)', //对比度
'grayscale(100%)', //灰度
'drop-shadow(2px 6px 9px blue)', //阴影
'hue-rotate(90deg)', //色相旋转
'invert(100%)', //反色
'opacity(0%)', //透明度
'saturate(200%)', //饱和度
'sepia(100%)', // 褐色
]
// 生成不同滤镜下的照片
for (let i = 0; i < filterList.length; i++) {
ctx.filter = filterList[i]
ctx.drawImage(myVideo, 0, 0, canvas.width, canvas.height)
addImage(canvas.toDataURL());
}

当然,如果你真想美颜,按这个数值设置的话拍出来的可能是女鬼,还是自己调调试试看吧。[比心]
我们往往有前置摄像头和后置摄像头之分,那么可以通过mediaDevices提供的enumerateDevices方法获取所有设备的信息,并在调用getUserMedia时通过配置deviceId来进行摄像头的切换。

三. 屏幕共享

navigator.mediaDevices提供了一个API:getDisplayMedia,能够获取我们自己想要共享的本地页面或者屏幕的媒体流;我们拿到后跟二可以一样处理,在页面进行展示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 获取屏幕共享的媒体流
async function shareScreen() {
let localStream = await navigator.mediaDevices.getDisplayMedia({
audio: true,
video: true,
})
// 页面播放
playStream(localStream)
}

// video播放视频
function playStream(stream) {
const video = document.getElementById('myVideo');
video.srcObject = stream
}
shareScreen();

代码执行后页面会自动出现提示弹窗,问询你要分享的内容:

选择后会单独共享该页面或屏幕,不必担心其他页面乱入等等;我们也可以结合canvas进行截屏(见二的拍照方法)

四. 录屏

  接下来向我们走来的是高级的应用–录屏;它的实现利用了MediaRecorder(一个可用于录制媒体流的好东西)这一构造函数,它还提供了静态方法 isTypeSupported 来帮助我们判断当前浏览器是否支持我们想要的媒体类型。
上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 获取当前浏览器支持的媒体类型
function getMimeTypes() {
const types = ['mp4', 'ogg', 'webm', 'flv', 'mkv', 'mov', 'avi', 'wmv', 'x-matroska', 'ts']
const codecs = ['vp9', 'vp9.0', 'vp8', 'vp8.0', 'avc1', 'av1', 'h265', 'h264']
// 支持的媒体类型
const support = []
// 遍历判断支持的媒体类型列表(媒体+types+codecs构成了媒体类型)
types.forEach((type) => {
const mimeType = `video/${type}`

codecs.forEach((codec) =>
[`${mimeType};codecs=${codec}`, `${mimeType};codecs=${codec.toUpperCase()}`,].forEach((variation) => {
if (MediaRecorder.isTypeSupported(variation)) support.push(variation)
}),
)

if (MediaRecorder.isTypeSupported(mimeType)) support.push(mimeType)
})
return support
}
// 录制媒体流
function startRecord(localStream) {
// 查询相关属性进行配置
const options = {
audioBitsPerSecond: 128000,
videoBitsPerSecond: 2500000,
mimeType: 'video/webm; codecs="vp8,opus"',
}
const recorder = new MediaRecorder(localStream, options)
recorder.start()
recorder.ondataavailable = (e) => {
// 直接使用e.data.type作为 blob 的 type,如果希望得到mp4格式就写 'video/mp4'
const blob = new Blob([e.data], { type: e.data.type })
downloadBlob(blob)
}
}

// 我们的老朋友--下载
function downloadBlob(blob) {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `录屏_${new Date().getTime()}.${blob.type.split('/')[1]}`
a.click()
URL.revokeObjectURL(url)
}

我们只要在合适的时机调用startRecord即可开始录屏,默认当我们点击停止共享的时候就会生成该视频文件:

五. 上期遗留问题揭晓

问:对于浏览器的工作原理的进一步了解对于某些特殊的bug能够提前规避,大家来猜一猜剩余雷数(this.mineNumDom.innerHTML)的变更与弹框的出现究竟哪一个先哪一个后呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if(obj.className=='flag'){
this.mineNumDom.innerHTML=--this.surplusMine;
}else{
this.mineNumDom.innerHTML=++this.surplusMine;
}
if(this.surplusMine==0){
//剩余的雷的数量为0,表示用户已经标完小红旗了,这时候要判断游戏是成功还是结束
if(this.allRight){
//这个条件成立说明用户全部标对了
alert('恭喜你,游戏通过');
this.removeEvent();
}else{
alert('游戏失败');
this.gameOver();
}
}

答:最终弹窗会先于dom的变化出现,页面的渲染线程与js代码的执行线程是互斥的,这是因为JS也能够操作DOM,所以不会同时进行。 在一个js代码的线程中对页面元素的更改,要等到这个js代码的线程执行结束,页面才会重新渲染。
解决方式:异步;使用延时函数解决,能够解决的原因就是因为setTimeout开了一个定时器线程,此时主线程就空了,此时浏览器觉得可以先渲染UI不耽搁事,所以才去先渲染,再去继续js线程的事情。

一个大佬说过:技术不进则死。共勉!