# WebRTC
# 概念
# WebRTC 是什么 ?
WebRTC 是由一家名为 Gobal IP Solutions,简称 GIPS 的瑞典公司开发的。Google 在 2011 年收购了 GIPS,并将其源代码开源。
简单来说,WebRTC 是一个可以在 Web 应用程序中实现音频,视频和数据的实时通信的开源项目。在实时通信中,音视频的采集和处理是一个很复杂的过程。比如音视频流的编解码、降噪和回声消除等,但是在 WebRTC 中,这一切都交由浏览器的底层封装来完成。我们可以直接拿到优化后的媒体流,然后将其输出到本地屏幕和扬声器,或者转发给其对等端。
我们可以在不需要任何第三方插件的情况下,实现一个浏览器到浏览器的点对点(P2P)连接,从而进行音视频实时通信。
# WebRTC API
WebRTC 提供了一些 API 供我们使用,在实时音视频通信的过程中,我们主要用到以下:
- mediaDevices
- getUserMedia:获取音频和视频流(MediaStream)
- RTCPeerConnection:点对点通信
- RTCDataChannel:数据通信
- MediaStream API:媒体流
# MediaDevices 媒体设备
MediaDevices
接口提供访问连接媒体输入的设备,如照相机和麦克风,以及屏幕共享等。它可以使你取得任何硬件资源的媒体数据。
事件 | |
---|---|
devicechange | 每当媒体设备连接到系统或从系统中移除时 |
方法 | |
---|---|
enumerateDevices() | |
getSupportedConstraints() | 返回一个对象,指明设备支持那些配置参数 |
getDisplayMedia() | 获取显示器或者窗口 |
getUserMedia() | 获取摄像头,麦克风,屏幕共享 |
MediaDevices.getUserMedia()
会提示用户给予使用媒体输入的许可,媒体输入会产生一个MediaStream
(opens new window),里面包含了请求的媒体类型的轨道。返回一个Promise
对象,由于用户不是必须选择所以可能既不会resolve
也不会reject
。
var video = document.querySelector("video");
var constraints = { audio: false, video: { width: 1280, height: 720 } };
navigator.mediaDevices
.getUserMedia(constraints)
.then((stream) => {
video.srcObject = stream;
})
.catch((err) => {
console.log(err);
});
# MediaStream 媒体流
MediaStream 表示一个媒体流对象,一个流包含几个轨道,比如视频和音频轨道
属性 | ||
---|---|---|
active | boolean | 这个流是否处于活动状态 |
ended | boolean | 这个流是否被完全读取 |
id | string | 这个流对象的唯一标识符 |
事件 | |
---|---|
onaddtrack | 一个 MediaStreamTrack 对象添加到这个流时 |
onended | 当流终止时 |
onremovetrack | 当一个 MediaStreamTrack 从这个流上移除时 |
其他方法 | |
---|---|
MediaStream.addTrack() | 添加一个 Track |
MediaStream.removeTrack() | 删除一个 Track |
MediaStream.clone() | 克隆一个 MediaStream,会有一个新的 ID |
MediaStream.getTracks() | 返回一个列表,所有 Track |
MediaStream.getAudioTracks() | 返回一个列表,所有 AudioTrack |
MediaStream.getVideoTracks() | 返回一个列表,所有 VideoTrack |
MediaStream.getTrackById() | 按 ID 查找 Track |
# MediaStreamTrack
MediaStreamTrack 接口在 User Agent 中表示一段媒体源,比如音轨或视频
属性 | ||
---|---|---|
enabled | boolean | 表示轨道是否有效 |
id | string | 唯一标识符 |
kind | video/audio | 表示轨道类型 |
label | string | 用户指定的标签 |
muted | boolean | 表示轨道是否为静音 |
readonly | boolean | 表示轨道是否只读,不能被修改 |
readyState | live/ended | 表示轨道的当前状态 |
remote | boolean | 表示数据是否时 RTC 提供的 |
事件 | ||
---|---|---|
onstarted | onmute | onunmete |
onoversonstrained | onended |
方法 | ||
---|---|---|
getConstraints() | applyConstraints() | getSettings() |
getCapabilities() | clone | stop() |
# RTCPeerConnection
RTCPeerConnection 接口代表一个由本地计算机到远端的 WebRTC 连接。该接口提供了创建,保持,监控,关闭连接的方法的实现。、
我们虽然把 WebRTC 称之为点对点的连接,但并不意味着,实现过程中不需要服务器的参与。因为在点对点的信道建立起来之前,二者之间是没有办法通信的。这也就意味着,在信令阶段,我们需要一个通信服务来帮助我们建立起这个连接。
//兼容性处理
let PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
let iceServers= [
{ url: "stun:stun.l.google.com:19302"}, // 谷歌的公共服务
{
url: "turn:***",
username: ***, // 用户名
credential: *** // 密码
}
]
let peer = new PeerConnection(iceServers)
事件 | |
---|---|
onaddstream | 当一个媒体流从远程对等体添加到 PeerConnection 连接中时 |
ondatachannel | 当一个 RTC 数据通道添加到连接中 |
onicecandidate | 当一个 ICE 候选添加到备选池中 |
oniceconnectionstatechang | 当一个连接对象的状态发生改变时 |
onidentityresult | 当身份断言生成时 |
.onidpassertionerror | 当身份断言关联的身份提供者遇到一个错误时 |
onidpvalidation | 当身份验证出错时 |
onsignalingstatechange | 当 signalingstate 更改的值时 |
onremovestream | 当从此连接中删除 mediastream 时 |
方法 | |
---|---|
RTCPeerConnection() | 构造函数,通过 new,返回一个 RTC 对等体连接对象 |
createOffer() | 呼叫方发送一个 offer 请求(成功回调,失败回调,配置对象) |
createAnswer() | 被呼叫方发送一个 answer 应答(成功回调,失败回调,配置对象) |
setLocalDesccription() | 设置与连接相关的本地描述 |
setRemoteDescription() | 设置与连接相关的远端描述 |
getLocalStreams() | 返回连接的本地媒体流数组 |
getRemoteStreams() | 返回连接的远端媒体流数组 |
addStream() | 添加一个媒体流 |
removeStream() | 移除一个媒体流 |
close() | 关闭一个 RTCPeerConnection 实例 |
createDataChannel() | 建立一个新的 DC 通道 |
基本用法
var pc = new RTCPeerConnection();
pc.onaddstream = (event) {
var vid = document.createElement("video");
document.appendChild(vid);
vid.srcObject = event.stream;
}
呼叫方
let offerconf = {
offerToReceiveAudio: 0,
offerToReceiveVideo: 1,
iceRestart: true,
}
navigator.getUserMedia({video: true}, function(stream) {
pc.onaddstream({stream: stream});
pc.addStream(stream);
pc.createOffer(offerconf).then((offer)=>{
pc.setLocalDescription(offer).then(()=>{
sendOffer()
})
}).catch().
})
应答方
pc.setRemoteDescription(offer).then(()=>{
pc.createAnswer().then((answer)=>{
pc.setLocalDescription(answer).then(()=>{
sendAnswer()
})
})
})
# RTCDataChannel
RTCDataChannel 接口代表在两者之间建立一个双向数据的通道,用于发送非音视频的信息。
可以用 createDataChannel()或者在现有的 RTCPeerConnection 实例上用 ondatachannel 事件接受,创建出 RTCDataChannel 类型的对象。
var pc = new RTCPeerConnection();
var dc = pc.createDataChannel("my channel");
dc.onmessage = function (event) {
console.log("received: " + event.data);
};
dc.onopen = function () {
console.log("datachannel open");
};
dc.onclose = function () {
console.log("datachannel close");
};
dc.onerror = function () {
console.log("datachannel error");
};
属性 | |
---|---|
label | 描述通道名字的一个字符串,这个字段可以不唯一 |
ordered | 返回一个 Boolean,表示传递信息的顺序是否有保证 |
protocol | 返回正在使用的子协议的名称,一个字符串,没有则返回“” |
id | 当 RTCDdataChannel 对象被创建时,作为通道的唯一标识 |
readyState | 表示数据连接的状态 |
事件 | |
---|---|
onopen | 当底层数据链路传输成功时 |
onmessage | 当有数据被接受时 |
onclose | 当底层链路被关闭时 |
onerror | 当遇到错误时 |
方法 | |
---|---|
close() | 关闭 channel,不会立即生效,等消息队列的数据发送完毕之后,channe 才会被关闭 |
send() | 将参数中的数据通过 channel 通道发送 |
# SDP
WebRTC 使用 Offer-Answer 模型交换 SDP,Offer 中有 SDP,Answer 中也有。例如 Alice 和 Bob 通过 WebRTC 通信:
Alice Offer
// Session description
v=0
# version SDP 协议版本,值固定为 0
o=- 2397106153131073818 2 IN IP4 127.0.0.1
# origin 代表会话的发起者
s=-
# session name 会话的名称,每个 SDP 中有且仅能有一个 s 描述,其值不能为空
c=IN IP4 0.0.0.0
connection data
// Time description
t=0 0 携带了会话的连接信息,其实就是 IP 地址
# timing 指定了会话的开始和结束时间,如果开始和结束时间都为 0,那么意味着这次会话是永久的
// Session Attribute
a=group:BUNDLE video
a=msid-semantic: WMS gLzQPGuagv3xXolwPiiGAULOwOLNItvl8LyS
// Media description
m=video 9 UDP/TLS/RTP/SAVPF 96 97
# m=<media> <port> <proto> <fmt> ...
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:l5KU
a=ice-pwd:+Sxmm3PoJUERpeHYL0HW4/T9
a=ice-options:trickle
a=fingerprint:sha-256 7C:93:85:40:01:07:91:BE:DA:64:A0:37:7E:61:CB:9D:91:9B:44:F6:C9:AC:3B:37:1C:00:15:4C:5A:B5:67:74
a=setup:actpass
a=mid:video
a=sendrecv
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 transport-cc
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=ssrc-group:FID 2527104241
a=ssrc:2527104241 cname:JPmKBgFHH5YVFyaJ
a=ssrc:2527104241 msid:gLzQPGuagv3xXolwPiiGAULOwOLNItvl8LyS c7072509-df47-4828-ad03-7d0274585a56
a=ssrc:2527104241 mslabel:gLzQPGuagv3xXolwPiiGAULOwOLNItvl8LyS
a=ssrc:2527104241 label:c7072509-df47-4828-ad03-7d0274585a56
Bob Answer
v=0
o=- 5443219974135798586 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE video
a=msid-semantic: WMS uiZ7cB0hsFDRGgTIMNp6TajUK9dOoHi43HVs
m=video 9 UDP/TLS/RTP/SAVPF 96 97
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:MUZf
a=ice-pwd:4QhikLcmGXnCfAzHDB++ZjM5
a=ice-options:trickle
a=fingerprint:sha-256 2A:5A:B8:43:66:05:B3:6A:E9:46:36:DF:DF:20:11:6A:F6:11:EA:D9:4E:26:E3:CE:5A:3A:C6:8D:03:49:7B:DE
a=setup:active
a=mid:video
a=sendrecv
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 transport-cc
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=ssrc-group:FID 3587783331
a=ssrc:3587783331 cname:INxZnBV2Sty1zlmN
a=ssrc:3587783331 msid:uiZ7cB0hsFDRGgTIMNp6TajUK9dOoHi43HVs a3b297e7-cdbe-464e-a32c-347465ace055
a=ssrc:3587783331 mslabel:uiZ7cB0hsFDRGgTIMNp6TajUK9dOoHi43HVs
a=ssrc:3587783331 label:a3b297e7-cdbe-464e-a32c-347465ace055
注意:SDP Line 是顺序相关的,比如 a=rtpmap:96
后面的都是它相关的设置,直到下一行是 a=rtpmap
或者其他属性
交换完 SDP 后,会交换 Candidate:
// Alice Candidate
candidate: candidate:1912876010 1 udp 2122260223 30.2.220.94 52832 typ host generation 0 ufrag l5KU network-id 1 network-cost 10
candidate: candidate:1015535386 1 tcp 1518280447 30.2.220.94 9 typ host tcptype active generation 0 ufrag l5KU network-id 1 network-cost 10
// Bob Candidate
candidate:1912876010 1 udp 2122260223 30.2.220.94 51551 typ host generation 0 ufrag MUZf network-id 1 network-cost 10
# MCU 与 SFU
MediaSoup 这种 SFU,客户端先给一个 Offer 给 SFU,SFU 只是检查这个 Offer 中的媒体特性,然后 SFU 会生成 Offer(包含会议中的其他客户端的流,如果没有人则没有 SSRC)给客户端,客户端发送 Answer 给 SFU。这种方式的好处是其他客户端加入,以及流的变更(比如关闭视频打开视频时),都可以使用 Reoffer,也就是统一由 SFU 发起新的 Offer,客户端响应,SFU 和客户端的交互模式只有一种。
# 获取设备
# enumerateDevices()
navigator.mediaDevices.enumerateDevices()
# getSupportedConstraints()
navigator.mediaDevices.getSupportedConstraints()
# getUserMedia()
navigator.mediaDevices.getUserMedia
# getVideoTracks()
mediaStream.getVideoTracks
# 预览效果1
# 播放视频
# srcObject
HTMLMediaElement.srcObject
# requestAnimationFrame
window.requestAnimationFrame
# 预览效果2
# 视频截图
# drawImage
CanvasRenderingContext2D.drawImage
void ctx.drawImage(image, dx, dy);
绘制到上下文的元素。允许任何的 canvas 图像源 CanvasImageSource,可以是
- CSSImageValue
- HTMLImageElement
- SVGImageElement
- HTMLVideoElement
- HTMLCanvasElement
- ImageBitmap
- OffscreenCanvas
# 预览效果3
# 视频录制
# MediaRecorder
let recorder = new MediaRecorder(videoStream, { mimeType: "video/webm;codec=h264" });
ondataavailable
该事件可用于获取录制的媒体资源
start()
开始录制媒体,这个方法调用时可以通过给 timeslice 参数设置一个毫秒值
stop
停止录制. 同时触发 dataavailable 事件,返回一个存储 Blob 内容的录制数据.之后不再记录
MediaRecorder - MDN (opens new window)
# createObjectURL
URL.createObjectURL()
objectURL = URL.createObjectURL(object);
object
用于创建 URL 的 File 对象、Blob 对象或者 MediaSource 对象。
objectURL
一个 DOMString 包含了一个对象 URL,该 URL 可用于指定源 object 的内容。
createObjectURL - MDN (opens new window)
# 预览效果4
# 本地 P2P 对讲
# 时序图
TIP
这里举例的是有信令服务器的情况下的流程图