|
|
|
|
# 音视频技术
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 0. 目录
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 1. 背景
|
|
|
|
|
- 一方面,**视频压缩技术从 H261 到 H264,再到现在的 H265及未来不久将出现的 AV1**,视频压缩率越来越高;**音频压缩技术也从电话使用的 G.711、
|
|
|
|
|
G.722 等窄带音频压缩技术,发展到现代的 AAC、OPUS 等宽带音频压缩技术**。
|
|
|
|
|
- 标准: 所开发的直播系统既可以支持上万人同时在线,又可以进行多人实时音视频互动,此外还可以与固话、MCU等硬件设备互联互通
|
|
|
|
|
|
|
|
|
|
- 音频技术的现在与未来
|
|
|
|
|
- H264/H265、VP8/VP9 以及后面的 AV1 编解码器,解决了视频压缩率的问题;而 5G 的商用,解决了带宽的问题。
|
|
|
|
|
- 尤其是 2011 年 Google 推出 WebRTC 技术后,大大降低了音视频技术的门槛。有了WebRTC,你就**不必自己去实现回音消除算法了**;有了 WebRTC ,你也**不必自己去实现各
|
|
|
|
|
种音视频的编解码器了**;有了 WebRTC,你更**不必去考虑跨平台的问题了**。因此,可以说WebRTC 的出现大大加速了音视频技术的应用与推广。
|
|
|
|
|
- WebRTC 有个特别有诱惑力的愿景:可以在浏览器上快速开发出各种音视频应用。
|
|
|
|
|
|
|
|
|
|
- 行业及学习痛点
|
|
|
|
|
- 如果你想开发一款音视频产品,**不仅需要有最基础的音视频知识(如音视频的编码、解码),往往还需要多层级的技术栈,涉及移动端开发、PC 端开发、各种协议规范、网络
|
|
|
|
|
协议、socket 开发**等。所以,要想成为一员合格的音视频开发工程师,你需要对各领域的知识都有一些掌握才行。
|
|
|
|
|
|
|
|
|
|
- 如何学习音视频技术
|
|
|
|
|
- 首先,让你学会如何**使用浏览器相关 API 调用 WebRTC 实现 1 对 1 通话;然后,再逐步深入学习其他音视频知识**。
|
|
|
|
|
|
|
|
|
|
- 主题一:WebRTC 1 对 1 通话
|
|
|
|
|
- 实现WebRTC 1 对 1 通话
|
|
|
|
|
- 主题二:WebRTC 多人音视频实时通话
|
|
|
|
|
- 介绍几种多人音视频实时互动的架构,以及这几种架构的优劣
|
|
|
|
|
- 重点讲解如何使用 SFU 架构实现多人音视频实时通话(SFU 是现在最流行的多人实时互动架构)
|
|
|
|
|
- 主题三:支持上万人同时在线的直播系统
|
|
|
|
|
- 支持上万人同时在线的直播系统主要使用 CDN 技术,它是一种比较老的直播架构,使用的底层传输协议是 RTMP 和 HLS。以及如何使用各种播放器从
|
|
|
|
|
CDN 拉取媒体流
|
|
|
|
|
|
|
|
|
|
## 2. WebRTC1对1通话
|
|
|
|
|
|
|
|
|
|
### 2.1 通过浏览器访问摄像头
|
|
|
|
|
|
|
|
|
|
- 随着 WebRTC 1.0 规范的推出,现在主流浏览器 **Chrome、Firefox、Safari 以及 Edge** 都已经支持了 WebRTC 库。换句话说,在这些浏览器之间进行实时音视频通信已经很成熟了。
|
|
|
|
|
|
|
|
|
|
- WebRTC 处理过程
|
|
|
|
|
- 在正式讲解如何通过浏览器采集音视频数据之前,我先向你介绍一下 WebRTC 实现一对一音视频实时通话的整个处理过程
|
|
|
|
|
- ![WebRTC 1 对 1 音视频实时通话过程示意图](pic/WebRTC1对1音视频实时通话过程示意图.png)
|
|
|
|
|
- 这幅图从大的方面可以分为 4 部分,即**两个 WebRTC 终端**(上图中的两个大方框)、**一个 Signal(信令)服务器**和**一个 STUN/TURN 服务器**。
|
|
|
|
|
- WebRTC 终端,负责音视频采集、编解码、NAT 穿越、音视频数据传输。
|
|
|
|
|
- Signal 服务器,负责信令处理,如加入房间、离开房间、媒体协商消息的传递等。
|
|
|
|
|
- STUN/TURN 服务器,负责获取 WebRTC 终端在公网的 IP 地址,以及 NAT 穿越失败后的数据中转。
|
|
|
|
|
- 描述一下WebRTC 进行音视频通话的大体过程。
|
|
|
|
|
- 当一端(WebRTC 终端)进入房间之前,它首先会检测自己的设备是否可用。如果此时设备可用,则进行**音视频数据采集**。
|
|
|
|
|
- 采集到的数据一方面可以做预览,也就是让自己可以看到自己的视频;另一方面,可以将其录制下来保存成文件,等到视频通话结束后,上传到服务器让用户回看之前的内容。
|
|
|
|
|
- 在获取音视频数据就绪后,WebRTC 终端要发送 “加入” 信令到 Signal 服务器。Signal服务器收到该消息后会创建房间。在另外一端,也要做同样的事情,只不过它不是创建房
|
|
|
|
|
间,而是加入房间了。待第二个终端成功加入房间后,第一个用户会收到 “另一个用户已经加入成功” 的消息。
|
|
|
|
|
- 在获取音视频数据就绪后,WebRTC 终端要发送 “**加入**” 信令到 Signal 服务器。Signal 服务器收到该消息后会创建房间。在另外一端,也要做同样的事情,只不过它不是创建房
|
|
|
|
|
间,而是加入房间了。待第二个终端成功加入房间后,第一个用户会收到 “**另一个用户已经加入成功**” 的消息。
|
|
|
|
|
- 此时,第一个终端将创建 “**媒体连接**” 对象,即 **RTCPeerConnection**(该对象会在后面的文章中做详细介绍),并将采集到的音视频数据通过 RTCPeerConnection 对象进行编
|
|
|
|
|
码,最终通过 P2P 传送给对端。
|
|
|
|
|
- 当然,在进行 P2P 穿越时很有可能失败。所以,当 P2P 穿越失败时,为了保障音视频数据仍然可以互通,则需要通过 TURN 服务器(TURN 服务会在后面文章中专门介绍)进行音
|
|
|
|
|
视频数据中转。
|
|
|
|
|
- 这样,当音视频数据 “历尽千辛万苦” 来到对端后,对端首先将收到的音视频数据进行解码,最后再将其展示出来,这样就完成了一端到另一端的单通。如果双方要互通,那么,两
|
|
|
|
|
方都要通过 RTCPeerConnection 对象传输自己一端的数据,并从另一端接收数据。
|
|
|
|
|
|
|
|
|
|
- 音视频采集基本概念
|
|
|
|
|
- 在正式介绍 JavaScript 采集音视频数据的 API 之前,你还需要了解一些基本概念。
|
|
|
|
|
- **摄像头**。用于捕捉(采集)图像和视频。
|
|
|
|
|
- **帧率**。现在的摄像头功能已非常强大,一般情况下,一秒钟可以采集 30 张以上的图像,一些好的摄像头甚至可以采集 100 张以上。我们把**摄像头一秒钟采集图像的次数称为帧**
|
|
|
|
|
率。帧率越高,视频就越平滑流畅。然而,在直播系统中一般不会设置太高的帧率,因为帧率越高,占的网络带宽就越多。
|
|
|
|
|
- **分辨率**。摄像头除了可以设置帧率之外,还可以调整分辨率。我们常见的分辨率有 2K、1080P、720P、420P 等。分辨率越高图像就越清晰,但同时也带来一个问题,即占用的
|
|
|
|
|
带宽也就越多。所以,在直播系统中,分辨率的高低与网络带宽有紧密的联系。也就是说,分辨率会跟据你的网络带宽进行动态调整。
|
|
|
|
|
- **宽高比**。分辨率一般分为两种宽高比,即 16:9 或 4:3。4:3 的宽高比是从黑白电视而来,而 16:9 的宽高比是从显示器而来。现在一般情况下都采用 16:9 的比例。
|
|
|
|
|
- **麦克风**。用于采集音频数据。它与视频一样,可以指定一秒内采样的次数,称为采样率。每个采样用几个 bit 表示,称为采样位深或采样大小。
|
|
|
|
|
- **轨(Track)**。WebRTC 中的“轨”借鉴了多媒体的概念。火车轨道的特性你应该非常清楚,两条轨永远不会相交。“轨”在多媒体中表达的就是每条轨数据都是独立的,不会
|
|
|
|
|
与其他轨相交,如 MP4 中的音频轨、视频轨,它们在 MP4 文件中是被分别存储的。
|
|
|
|
|
- **流(Stream)**。可以理解为容器。在 WebRTC 中,“流”可以分为媒体流(MediaStream)和数据流(DataStream)。其中,**媒体流**可以存放 0 个或多个音频
|
|
|
|
|
轨或视频轨;数据流可以存 0 个或多个数据轨。
|
|
|
|
|
- **媒体流**: 参考 https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
|
|
|
|
|
|
|
|
|
- 音视频采集
|
|
|
|
|
- 有了上面这些基本概念,你就可以很容易理解后面所要讲的内容了。接下来,就让我们来具体看看在浏览器下采集音视频的 API 格式以及如何控制音视频的采集吧。
|
|
|
|
|
|
|
|
|
|
- **getUserMedia 方法**
|
|
|
|
|
- 在浏览器中访问音视频设备非常简单,只要调用 **getUserMedia** 这个 API 即可。该 API 的基本格式如下:
|
|
|
|
|
```js
|
|
|
|
|
var promise = navigator.mediaDevices.getUserMedia(constraints);
|
|
|
|
|
```
|
|
|
|
|
- 它返回一个 **Promise** 对象。
|
|
|
|
|
- 如果 **getUserMedia** 调用成功,则可以通过 Promise 获得 **MediaStream** 对象,也就是说现在我们已经从音视频设备中获取到音视频数据了。
|
|
|
|
|
- 如果调用失败,比如用户拒绝该 API 访问媒体设备(音频设备、视频设备),或者要访问的媒体设备不可用,则返回的 Promise 会得到 PermissionDeniedError 或
|
|
|
|
|
NotFoundError 等错误对象。
|
|
|
|
|
|
|
|
|
|
- **MediaStreamConstraints 参数**
|
|
|
|
|
- 从上面的调用格式中可以看到,**getUserMedia** 方法有一个输入参数 **constraints**,其类型为 **MediaStreamConstraints**。它可以指定 **MediaStream** 中包含哪些类型的媒体轨(音
|
|
|
|
|
频轨、视频轨),并且可为这些媒体轨设置一些限制。
|
|
|
|
|
- 下面我们就来详细看一下它包括哪些限制,这里我引用一下 WebRTC 1.0 规范对 MediaStreamConstraints 的定义,其格式如下:
|
|
|
|
|
- PS: MediaStreamConstraints 详解 https://w3c.github.io/mediacapture-main/getusermedia.html#mediastreamconstraints
|
|
|
|
|
```js
|
|
|
|
|
dictionary MediaStreamConstraints {
|
|
|
|
|
(boolean or MediaTrackConstraints) video = false,
|
|
|
|
|
(boolean or MediaTrackConstraints) audio = false
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
- 从上面的代码中可以看出,**该结构可以指定采集音频还是视频,或是同时对两者进行采集**。
|
|
|
|
|
|
|
|
|
|
- 举个例子,比如你**只想采集视频**,则可以像下面这样定义 constraints:
|
|
|
|
|
```js
|
|
|
|
|
const mediaStreamContrains = {
|
|
|
|
|
video: true
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
- 或者,同时采集音视和视频:
|
|
|
|
|
```js
|
|
|
|
|
const mediaStreamContrains = {
|
|
|
|
|
video: true,
|
|
|
|
|
audio: true
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
- 其实,你还可以通过 MediaTrackConstraints 进一步对**每一条媒体轨进行限制**,比如下面的代码示例:
|
|
|
|
|
```js
|
|
|
|
|
const mediaStreamContrains = {
|
|
|
|
|
video: {
|
|
|
|
|
frameRate: {min: 20},
|
|
|
|
|
width: {min: 640, ideal: 1280},
|
|
|
|
|
height: {min: 360, ideal: 720},
|
|
|
|
|
aspectRatio: 16/9
|
|
|
|
|
},
|
|
|
|
|
audio: {
|
|
|
|
|
echoCancellation: true,
|
|
|
|
|
noiseSuppression: true,
|
|
|
|
|
autoGainControl: true
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
```
|
|
|
|
|
- 上面这个例子表示:视频的帧率最小 20 帧每秒;宽度最小是 640,理想的宽度是 1280;同样的,高度最小是 360,最理想高度是 720;此外宽高比是 16:9;对于音频则是开启回
|
|
|
|
|
音消除、降噪以及自动增益功能。
|
|
|
|
|
- 除了上面介绍的这些参数来控制摄像头和麦克风外,当然还有其他一些参数可以设置,更详细的参数信息,可以跳到下面的参考部分。
|
|
|
|
|
|
|
|
|
|
- 如何使用 getUserMedia API
|
|
|
|
|
- 如何使用上面介绍的 API 来采集视频数据吧
|
|
|
|
|
- 下面的 HTML 代码非常简单,它引入一段 JavaScript 代码用于捕获音视频数据,然后将采集到的音视频数据通过 video 标签播放出来。
|
|
|
|
|
```html
|
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html>
|
|
|
|
|
<head>
|
|
|
|
|
<title>Realtime communication with WebRTC</title>
|
|
|
|
|
<link rel="stylesheet", href="css/client.css" />
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<h1>Realtime communication with WebRTC </h1>
|
|
|
|
|
<video autoplay playsinline></video>
|
|
|
|
|
<script src="js/client.js"></script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|
|
|
|
|
```
|
|
|
|
|
- 为便于你更好地理解该部分的知识,上面这段代码中有两条代码我需要解释一下,一句是:
|
|
|
|
|
```html
|
|
|
|
|
<video autoplay playsinline></video>
|
|
|
|
|
```
|
|
|
|
|
- 它是 HTML5 的视频标签,不仅可以播放多媒体文件,还可以用于播放采集到的数据。其参数含义如下:
|
|
|
|
|
- **autoplay**,表示当页面加载时可以自动播放视频;
|
|
|
|
|
- **playsinline**,表示在 HTML5 页面内播放视频,而不是使用系统播放器播放视频。
|
|
|
|
|
- 另一句是:
|
|
|
|
|
```html
|
|
|
|
|
<script src="js/client.js"></script>
|
|
|
|
|
```
|
|
|
|
|
- 它引入了外部的 JavaScript 代码,起到的作用就是获取视频数据。具体代码如下:
|
|
|
|
|
```js
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
const mediaStreamContrains = {
|
|
|
|
|
video: true
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const localVideo = document.querySelector('video');
|
|
|
|
|
|
|
|
|
|
function gotLocalMediaStream(mediaStream){
|
|
|
|
|
localVideo.srcObject = mediaStream;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleLocalMediaStreamError(error){
|
|
|
|
|
console.log('navigator.getUserMedia error: ', error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
navigator.mediaDevices.getUserMedia(mediaStreamContrains).then(
|
|
|
|
|
gotLocalMediaStream
|
|
|
|
|
).catch(
|
|
|
|
|
handleLocalMediaStreamError
|
|
|
|
|
);
|
|
|
|
|
```
|
|
|
|
|
- JavaScript 代码中首先执行 **getUserMedia()** 方法,该方法会请求访问 Camera。如果是第一次请求 Camera,浏览器会向用户弹出提示窗口,让用户决定是否可以访问摄像头。如果
|
|
|
|
|
用户允许访问,且设备可用,则调用 gotLocalMediaStream 方法。
|
|
|
|
|
- 在 gotLocalMediaStream 方法中,其输入参数为 **MediaStream** 对象,**该对象中存放着getUserMedia方法采集到的音视频轨**。我们将它作为视频源赋值给 HTML5 的 video 标
|
|
|
|
|
签的 srcObject 属性。这样在 HTML 页面加载之后,就可以在该页面中看到摄像头采集到的视频数据了。
|
|
|
|
|
- 在这个例子中,**getUserMedia** 方法的输入参数 **mediaStreamContraints** 限定了只采集视频数据。同样的,你也可以采集音频数据或同时采集音频和视频数据。
|
|
|
|
|
|
|
|
|
|
- 总结
|
|
|
|
|
- **MediaTrack和MediaStream**这两个概念特别重要,后续学习 WebRTC的过程中,我们会反复用到,所以在这最开始你就要理解透这两个概念。举个例子,如果你
|
|
|
|
|
想在一个房间里,同时共享视频、共享音频、共享桌面,该怎么做呢?如果你对 MediaTrack 和 MediaStream 真正理解了,就会觉得 WebRTC 处理这种情况太简单了。
|
|
|
|
|
|
|
|
|
|
- getUserMedia API 控制设备的参数及其含义如下:
|
|
|
|
|
- ![getUserMedia-API](pic/getUserMedia-API.png)
|
|
|
|
|
- 直接在chrome测试成功,声音不正常(回声很想),加了参数消除回音,还是不行,为什么啊?
|
|
|
|
|
- 自己的音频没有mute 吧?把video 标签里加个muted 试试
|
|
|
|
|
- NAT穿越是啥?
|
|
|
|
|
- P2P,端与端直接进行连接,不需要服务器中转数据,这样可以节省服务器带宽,但并不意味着不需要服务器,服务器作为辅助功能
|
|
|
|
|
- STUN/TURN 服务器是否有开源的,我之前看到都是使用谷歌的。
|
|
|
|
|
- coturn就可以,大家都用它!
|
|
|
|
|
- 如果使用freeswitch在其中作为一个什么角色?
|
|
|
|
|
- freeswitch可以做混音服务器,或mcu。不过在直播中,一般用它做服务器混音。
|
|
|
|
|
- 现有的 rtmp 的直播解决方案和 webRTC 之间优劣势在什么地方?
|
|
|
|
|
- Rtmp 底层用的tcp,wenrtc底层主要使用udp,使用tcp 就注定他在极端网络情况下没法实时通信
|
|
|
|
|
- 在使用nuxt创建的vue项目中,
|
|
|
|
|
使用ip访问:navigator.mediaDevices.是 undefined
|
|
|
|
|
使用localhost访问:navigator.mediaDevices. 存在
|
|
|
|
|
- 出于安全的原因,你只能用localhost 访问或https 访问时才能检测到mediaDevice
|
|
|
|
|
|
|
|
|
|
### 2.2 通过WebRTC进行音视频设备检测
|
|
|
|
|
- 在打开摄像头(Camera)或麦克风(Micphone)的时候,首先要对其进行检测,检测的内容包括:
|
|
|
|
|
- 电脑 / 手机上都有那些音视频设备?
|
|
|
|
|
- 我们选中的音视频设备是否可用?
|
|
|
|
|
- WebRTC 处理过程
|
|
|
|
|
- WebRTC 的整体处理过程图:
|
|
|
|
|
- ![WebRTC的整体处理过程图](pic/WebRTC的整体处理过程图.png)
|
|
|
|
|
- 图中两个音视频设备检测模块置红了
|
|
|
|
|
|
|
|
|
|
- 音视频设备的基本原理
|
|
|
|
|
|
|
|
|
|
- 音频设备
|
|
|
|
|
- 音频有**采样率和采样大小**的概念,实际上这两个概念就与音频设备密不可分。
|
|
|
|
|
- 音频输入设备的主要工作是采集音频数据,而采集音频数据的本质就是模数转换(A/D),即将模似信号转换成数字信号。
|
|
|
|
|
- 模数转换使用的采集定理称为**奈奎斯特定理**,其内容如下:
|
|
|
|
|
- 在进行模拟 / 数字信号的转换过程中,当采样率大于信号中最高频率的 2 倍时,采样之后的数字信号就完整地保留了原始信号中的信息。
|
|
|
|
|
- 你也知道,人类听觉范围的频率是 20Hz~20kHz 之间。对于日常语音交流(像电话),8kHz 采样率就可以满足人们的需求。
|
|
|
|
|
但为了追求高品质、高保真,你需要将音频输入设备的采样率设置在 40kHz 以上,这样才能完整地将原始信号保留下来。
|
|
|
|
|
例如我们平时听的数字音乐,一般其采样率都是 44.1k、48k 等,以确保其音质的无损。
|
|
|
|
|
- 采集到的数据再经过量化、编码,最终形成数字信号,这就是音频设备所要完成的工作。在量化和编码的过程中,采样大小(保存每个采样的二进制位个数)决定了每个采样最大可以表示的范围。
|
|
|
|
|
如果采样大小是 8 位,则它表示的最大值是就是 28 -1,即 255;如果是 16 位,则其表示的最大数值是 65535。
|
|
|
|
|
|
|
|
|
|
- 视频设备
|
|
|
|
|
- 至于视频设备,则与音频输入设备很类似。当实物光通过镜头进行到摄像机后,它会通过视频设备的模数转换(A/D)模块,即光学传感器, 将光转换成数字信号,即 RGB(Red、Green、Blue)数据。
|
|
|
|
|
- 获得 RGB 数据后,还要通过 DSP(Digital Signal Processer)进行优化处理,如自动增强、白平衡、色彩饱和等都属于这一阶段要做的事情。
|
|
|
|
|
- 通过 DSP 优化处理后,你就得到了 24 位的真彩色图片。因为每一种颜色由 8 位组成,而一个像素由 RGB 三种颜色构成,所以一个像素就需要用 24 位表示,故称之为**24 位真彩色**。
|
|
|
|
|
- 另外,此时获得的 RGB 图像只是临时数据。因最终的图像数据还要进行压缩、传输,而编码器一般使用的输入格式为 YUV I420,所以在摄像头内部还有一个专门的模块用于将 RGB 图像转为 YUV 格式的图像。
|
|
|
|
|
- 那什么是 YUV 呢?YUV 也是一种色彩编码方法,主要用于电视系统以及模拟视频领域。它将亮度信息(Y)与色彩信息(UV)分离,即使没有 UV 信息一样可以显示完整的图像,只不过是黑白的,
|
|
|
|
|
这样的设计很好地解决了彩色电视机与黑白电视的兼容问题。
|
|
|
|
|
- YUV 格式还是蛮复杂的,它有好几种存储方式
|
|
|
|
|
|
|
|
|
|
- WebRTC 设备管理的基本概念
|
|
|
|
|
- MediaDevices,该接口提供了访问(连接到计算机上的)媒体设备(如摄像头、麦克风)以及截取屏幕的方法。实际上,它允许你访问任何硬件媒体设备。而咱们**要获取可用的音视频设备列表**,就是通过该接口中的方法来实现的。
|
|
|
|
|
- ps: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices
|
|
|
|
|
- MediaDeviceInfo,它表示的是每个输入 / 输出设备的信息。包含以下三个重要的属性:
|
|
|
|
|
- deviceID,**设备的唯一标识**;
|
|
|
|
|
- label,设备 **名称**;
|
|
|
|
|
- kind,设备 **种类**,可用于识别出是音频设备还是视频设备,是输入设备还是输出设备。
|
|
|
|
|
- ps: https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo
|
|
|
|
|
- **另外,label 可以用作指纹识别机制的一部分,以识别是否是合法用户**。对于这一点我们以后再专门讨论。
|
|
|
|
|
- Promise,它是一种 JavaScript 异步处理机制。其思想是,首先执行指定的业务逻辑,而不管逻辑的对错,然后再根据结果做具体的操作:如果成功了做些什么,失败了做些什么。
|
|
|
|
|
结合下面的例子,可以让你对 Promise 有个清楚的认识,生成 Promise 对象时,首先会执行 function 函数中的逻辑,该函数会根据随机数生成 timeOut,
|
|
|
|
|
然后定时地对 timeOut 做出判断:
|
|
|
|
|
- ps: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
|
|
|
|
|
- 如果 timeOut 小于 1,则调用 resolve 方法。resolve 又会调用 Promise 中 then 部分传入的函数。
|
|
|
|
|
- 如果 timeOut 大于等于 1,则调用 reject 方法。reject 则会调用 Promise 中 catch 部分传入的函数。
|
|
|
|
|
```js
|
|
|
|
|
new Promise(function (resolve, reject) {
|
|
|
|
|
console.log('start new Promise...');
|
|
|
|
|
|
|
|
|
|
// 产生随机值
|
|
|
|
|
var timeOut = Math.random() * 2;
|
|
|
|
|
console.log('set timeout to: ' + timeOut + ' seconds.');
|
|
|
|
|
|
|
|
|
|
// 设置一个定时器函数,根据随机值触发该函数执行
|
|
|
|
|
setTimeout(function () {
|
|
|
|
|
if (timeOut < 1) {
|
|
|
|
|
console.log('call resolve()...');
|
|
|
|
|
resolve('200 OK');
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
console.log('call reject()...');
|
|
|
|
|
reject('timeout in ' + timeOut + ' seconds.');
|
|
|
|
|
}
|
|
|
|
|
}, timeOut * 1000);
|
|
|
|
|
}).then(function (r) {
|
|
|
|
|
console.log('Done: ' + r);
|
|
|
|
|
}).catch(function (reason) {
|
|
|
|
|
console.log('Failed: ' + reason);
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- 获取音视频设备列表
|
|
|
|
|
- 首先,我们来看浏览器上 WebRTC 获取音视频设备列表的接口,其格式如下:
|
|
|
|
|
```js
|
|
|
|
|
MediaDevices.enumerateDevices()
|
|
|
|
|
```
|
|
|
|
|
- 通过调用 MediaDevices 的 **enumerateDevices()** 方法就可以获取到媒体输入和输出设备列表,例如: 麦克风、相机、耳机等。是不是非常简单?
|
|
|
|
|
- ps: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices
|
|
|
|
|
- 该函数返回的是一个 Promise 对象。我们只需要向它的 then 部分传入一个函数,就可以通过该函数获得所有的音视频设备信息了。
|
|
|
|
|
- 传入的函数有一个参数,它是一个 MediaDeviceInfo 类型的数组,用来存放 WebRTC 获取到的每一个音视频设备信息。
|
|
|
|
|
- 这样说可能有点抽象,还是让我们结合下面代码看一个具体的例子吧。
|
|
|
|
|
```js
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
// 判断浏览器是否支持这些 API
|
|
|
|
|
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
|
|
|
|
|
console.log("enumerateDevices() not supported.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 枚举 cameras and microphones.
|
|
|
|
|
navigator.mediaDevices.enumerateDevices()
|
|
|
|
|
.then(function(deviceInfos) {
|
|
|
|
|
|
|
|
|
|
// 打印出每一个设备的信息
|
|
|
|
|
deviceInfos.forEach(function(deviceInfo) {
|
|
|
|
|
console.log(deviceInfo.kind + ": " + deviceInfo.label +
|
|
|
|
|
" id = " + deviceInfo.deviceId);
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.catch(function(err) {
|
|
|
|
|
console.log(err.name + ": " + err.message);
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
- 总结起来,上面的代码中做了以下几件事儿:
|
|
|
|
|
- 首先,判断浏览器是否支持 MediaDevice 接口(老版本浏览器可能不支持)。
|
|
|
|
|
- 如果支持,则调用navigator.mediaDevices.enumerateDevices()方法获取音视频设备列表,该方法会返回一个 Promise 对象。
|
|
|
|
|
- 如果返回 Promise 对象成功,则执行 then 中的函数。而then分支中的函数非常简单,它遍历每一个 MediaDeviceInfo,
|
|
|
|
|
并将每个 MediaDeviceInfo 中的基本信息打印出来,也就是我们想要的每个音视频设备的基本信息。
|
|
|
|
|
- 但如果失败的话,则执行 catch 中的函数。
|
|
|
|
|
|
|
|
|
|
- 设备检测
|
|
|
|
|
- 在获取到电脑 / 手机上的所有设备信息后,我们就可以对设备的可用性做真正的检测了。在我们的设备列表中,
|
|
|
|
|
可以通过MediaDeviceInfo结构中的 **kind** 字段,将设备分类为音频设备或视频设备。
|
|
|
|
|
- ps: https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo
|
|
|
|
|
- 如果再细分的话,还可以通过 kind 字段再将音视设备分为输入设备和输出设备。如我们平时使用的耳机,从大的方面说它是一个音频设备,但它同时兼有**音频输入设备和音频输出设备**的功能。
|
|
|
|
|
- 对于区分出的音频设备和视频设备,每种不同种类的设备还会设置各自的默认设备。还是以耳机这个音频设备为例,将耳机插入电脑后,耳机就变成了音频的**默认设备**;将耳机拔出后,默认设备又切换成了系统的音频设备。
|
|
|
|
|
- 因此,在获取到所有的设备列表后,如果我们不指定某个具体设备,直接调用介绍的 getUserMedia API 来采集音视频数据时,它就会从设备列表中的默认设备上采集数据。
|
|
|
|
|
当然,我们是可以通过 MediaDeviceInfo 中的 deviceID 字段来指定从哪个具体设备采集数据的,不过这就是后话了。
|
|
|
|
|
- 如果我们能从指定的设备上采集到音视频数据,那说明这个设备就是有效的设备。我们在排查设备问题的时候,就可以利用上面的方法,对每个设备都一项一项进行检测,即**先排查视频设备,然后再排查音频设备**。
|
|
|
|
|
因此,需要**调用两次 getUserMedia API 进行设备检测**。
|
|
|
|
|
- 第一次,调用 getUserMedia API 只采集视频数据并将其展示出来。如果用户能看到自己的视频,说明视频设备是有效的;否则,设备无效,可以再次选择不同的视频设备进行重新检测。
|
|
|
|
|
- 第二次,如果用户视频检测通过了,再次调用 getUserMedia API 时,则只采集音频数据。由于音频数据不能直接展示,所以需要使用 JavaScript 中的 AudioContext 对象,将采集到的音频计算后,
|
|
|
|
|
再将其绘制到页面上。这样,当用户看到音频数值的变化后,说明音频设备也是有效的。
|
|
|
|
|
|
|
|
|
|
### 2.3 使用浏览器给自己拍照
|
|
|
|
|
- 在正式讲解如何进行拍照之前,你需要先了解**非编码帧(解码帧)和编码帧**这两个知识点,这会有利于你对后面拍照实现内容的理解。
|
|
|
|
|
|
|
|
|
|
- 非编码帧
|
|
|
|
|
- 当你要播放某个视频文件时,播放器会按照一定的时间间隔连续地播放从音视频文件中解码后的视频帧,这样视频就动起来了。同理,播放从摄像头获取的视频帧也是如此,只不过从摄像头获取得本来就是**非编码视频帧**,所以就不需要解码了。
|
|
|
|
|
- 通过上面的描述,你应该能得到以下两点信息:
|
|
|
|
|
- 播放的视频帧之间的时间间隔是非常小的。如按每秒钟 20 帧的帧率计算,每帧之间的间隔是 50ms。
|
|
|
|
|
- 播放器播的是**非编码帧(解码后的帧)**,这些非编码帧就是一幅幅独立的图像。
|
|
|
|
|
- 从摄像头里采集的帧或通过解码器解码后的帧都是**非编码帧**。非编码帧的格式一般是 YUV 格式或是 RGB 格式。
|
|
|
|
|
|
|
|
|
|
- 编码帧
|
|
|
|
|
- 相对于非编码帧,通过编码器(如 H264/H265、VP8/VP9)压缩后的帧称为**编码帧**。这里我们以 H264 为例,经过 H264 编码的帧包括以下三种类 型。
|
|
|
|
|
- I 帧:关键帧。压缩率低,可以单独解码成一幅完整的图像。
|
|
|
|
|
- P 帧:参考帧。压缩率较高,解码时依赖于前面已解码的数据。
|
|
|
|
|
- B 帧:前后参考帧。压缩率最高,解码时不光依赖前面已经解码的帧,而且还依赖它后面的 P 帧。换句话说就是,**B 帧后面的 P 帧要优先于它进行解码,然后才能将 B 帧解码**。
|
|
|
|
|
- 通过上面的介绍,现在你应该已经清楚地知道了:**从播放器里获取的视频帧一定是非编码帧。也就是说,拍照的过程其实是从连续播放的一幅幅画面中抽取正在显示的那张画面**。
|
|
|
|
|
|
|
|
|
|
- 如何获取视频流
|
|
|
|
|
- 在获得照片之前,你首先要通过浏览器的 API 获取视频流,并通过 HTML5 的 <video> 标签将视频播放出来。
|
|
|
|
|
```html
|
|
|
|
|
<html>
|
|
|
|
|
<head>
|
|
|
|
|
<title>WebRTC take picture</title>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<video autoplay playsinline id="player">
|
|
|
|
|
<script src="./js/client.js"></script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|
|
|
|
|
```
|
|
|
|
|
- 上面这段代码很简单,就是定义了一个 video 标签,用于播放从摄像头获取到的视频流。另外,它还引入了一段 JavaScript 脚本:
|
|
|
|
|
```js
|
|
|
|
|
'use strict'
|
|
|
|
|
|
|
|
|
|
// 获取 HTML 页面中的 video 标签
|
|
|
|
|
var videoplay = document.querySelector('video#player');
|
|
|
|
|
|
|
|
|
|
// 播放视频流
|
|
|
|
|
function gotMediaStream(stream){
|
|
|
|
|
videoplay.srcObject = stream;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleError(err){
|
|
|
|
|
console.log('getUserMedia error:', err);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 对采集的数据做一些限制
|
|
|
|
|
var constraints = {
|
|
|
|
|
video : {
|
|
|
|
|
width: 1280,
|
|
|
|
|
height: 720,
|
|
|
|
|
frameRate:15,
|
|
|
|
|
},
|
|
|
|
|
audio : false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 采集音视频数据流
|
|
|
|
|
navigator.mediaDevices.getUserMedia(constraints)
|
|
|
|
|
.then(gotMediaStream)
|
|
|
|
|
.catch(handleError);
|
|
|
|
|
```
|
|
|
|
|
- 在这段脚本中,我们调用了之前所讲的**getUserMedia**方法,该方法会打开摄像头,并通过它采集音视频流。然后再将采集到的视频流赋值给
|
|
|
|
|
HTML 中定义的**video标签的srcObject字段**,这样video标签就可以从摄像头源源不断地获得视频帧,并将它播放出来了。
|
|
|
|
|
|
|
|
|
|
- 如何拍照
|
|
|
|
|
- 实际上,浏览器提供了一个非常强大的对象,称为 **Canvas**。你可以把它想像成一块画布,既可以在这块画布上画上点、线或各种图形,也可以将一幅画直接绘制到上面。
|
|
|
|
|
- 在浏览器中,Canvas 的功能非常强大,可以处理很多图表方面的事情
|
|
|
|
|
|
|
|
|
|
- 首先,在 HTML 中增加以下代码:
|
|
|
|
|
```html
|
|
|
|
|
...
|
|
|
|
|
<button id="TakePhoto">Take</button>
|
|
|
|
|
...
|
|
|
|
|
<canvas id="picture"></canvas>
|
|
|
|
|
...
|
|
|
|
|
```
|
|
|
|
|
- 上面的 HTML 代码段,包括一个 <canvas> 标签和一个 <button> 标签。我们的设想是,当点击拍照按钮时,就可以从视频流中获取到一张当时正在显示的图片了。
|
|
|
|
|
- 显然,光有 HTML 部分肯定是不行的,还需要下面的 JavaScript 脚本进行控制。增加 JavaScript 代码如下:
|
|
|
|
|
```js
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
var picture = document.querySelector('canvas#picture');
|
|
|
|
|
picture.width = 640;
|
|
|
|
|
picture.height = 480;
|
|
|
|
|
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
picture.getContext('2d').drawImage(videoplay, 0, 0, picture.width, picture.height);
|
|
|
|
|
|
|
|
|
|
...
|
|
|
|
|
```
|
|
|
|
|
- 在上面的 JavaScript 代码中,首先获得 HTML 中的 Canvas 标签,并设置了 Canvas 的宽高; 然后调用 Canvas 上下文的 drawImage 方法,这样就可以从视频流中抓取当时正在显示的图片了。
|
|
|
|
|
- 这里最关键的点就是 **drawImage** 方法,其方法格式如下:
|
|
|
|
|
```js
|
|
|
|
|
void ctx.drawImage(image, dx, dy, dWidth, dHeight);
|
|
|
|
|
```
|
|
|
|
|
- image:可以是一幅图片,或 HTMLVideoElement。
|
|
|
|
|
- dx, dy:图片起点的 x、y 坐标。
|
|
|
|
|
- dWidth:图片的宽度。
|
|
|
|
|
- dHeight:图片的高度。
|
|
|
|
|
|
|
|
|
|
- 该方法的第一个参数特别重要,它既可以是一幅图片,也可以是一个 Video 元素。而 HTML 中的 <video> 标签就是一个 video 元素,所以它可以当作是 drawImage 方法的第一个参数。
|
|
|
|
|
这样就可以通过 Canvas 获取到照片了。
|
|
|
|
|
|
|
|
|
|
- 如何保存照片
|
|
|
|
|
- 照片拍好后,如何将它保存到本地文件系统中呢? 浏览器同样给我们提供了非常方便的方法,让我们来看一下具体代码吧。
|
|
|
|
|
- HTML 要先增加如下代码:
|
|
|
|
|
```html
|
|
|
|
|
...
|
|
|
|
|
<div>
|
|
|
|
|
<button id="save"> 保存 </button>
|
|
|
|
|
</div>
|
|
|
|
|
...
|
|
|
|
|
```
|
|
|
|
|
- 也就是当你点击保存这个 <button> 的时候,就可以将前面 Canvas 抓取的图片保存下来。不过,<button>只是触发一个事件,真正做事儿的是下面的 JavaScript 代码。具体逻辑如下:
|
|
|
|
|
```js
|
|
|
|
|
...
|
|
|
|
|
|
|
|
|
|
function downLoad(url){
|
|
|
|
|
var oA = document.createElement("a");
|
|
|
|
|
oA.download = 'photo';// 设置下载的文件名,默认是'下载'
|
|
|
|
|
oA.href = url;
|
|
|
|
|
document.body.appendChild(oA);
|
|
|
|
|
oA.click();
|
|
|
|
|
oA.remove(); // 下载之后把创建的元素删除
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
...
|
|
|
|
|
document.querySelector("button#save").onclick = function (){
|
|
|
|
|
downLoad(canvas.toDataURL("image/jpeg"););
|
|
|
|
|
}
|
|
|
|
|
....
|
|
|
|
|
```
|
|
|
|
|
- 在上面的代码中,当用户点击保存按钮时,会调用一个匿名函数。该函数的逻辑如下:
|
|
|
|
|
- 首先,通过 Canvas 的 toDataURL 方法获得图片的 URL 地址;
|
|
|
|
|
- 然后,将该 URL 地址当作参数传给 downLoad 函数;
|
|
|
|
|
- 最后,downLoad 函数做的事儿比较简单,就是创建一个<a>标签,当用户点击时就将图片下载下来。
|
|
|
|
|
- 通过上面的代码,你就可以通过浏览器为自己拍照,并同时将拍下来的照片保存到文件系统中了。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- 如何实现滤镜
|
|
|
|
|
- 从视频流中获取到照片后,你还可以通过滤镜为照片增加点特效,这样会让你的照片更加特别。
|
|
|
|
|
- 在浏览器中对于图片的滤镜处理是通过 CSS 来控制的。像前面一样,首先在 HTML 中增加 CSS 的滤镜代码如下:
|
|
|
|
|
```html
|
|
|
|
|
...
|
|
|
|
|
<head>
|
|
|
|
|
<style>
|
|
|
|
|
.none {
|
|
|
|
|
-webkit-filter: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.blur {
|
|
|
|
|
-webkit-filter: blur(3px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.grayscale {
|
|
|
|
|
-webkit-filter: grayscale(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.invert {
|
|
|
|
|
-webkit-filter: invert(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.sepia {
|
|
|
|
|
-webkit-filter: sepia(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
...
|
|
|
|
|
<select id="filter">
|
|
|
|
|
<option value="none">None</option>
|
|
|
|
|
<option value="blur">blur</option>
|
|
|
|
|
<option value="grayscale">Grayscale</option>
|
|
|
|
|
<option value="invert">Invert</option>
|
|
|
|
|
<option value="sepia">sepia</option>
|
|
|
|
|
</select>
|
|
|
|
|
...
|
|
|
|
|
</body>
|
|
|
|
|
```
|
|
|
|
|
- 上面的 HTML 代码中定义了以下四种 CSS 滤镜。
|
|
|
|
|
- blur:模糊度
|
|
|
|
|
- grayscale:灰度(黑白)
|
|
|
|
|
- invert:反转
|
|
|
|
|
- sepia:深褐色
|
|
|
|
|
- 并增加了一个 <select> 标签,以便让用户选择使用不同的滤镜。但最终的控制还是由下面的 JavaScript 脚本来做的,JavaScript 代码如下:
|
|
|
|
|
```js
|
|
|
|
|
...
|
|
|
|
|
picture.className = filtersSelect.value;
|
|
|
|
|
...
|
|
|
|
|
```
|
|
|
|
|
- 只需要这样简单的一行代码,你就可以将不同的滤镜应用于获取的照片上
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|