webrtc学习笔记

必读:WebRTC API - Web API接口 | MDN
必读:WebRTC的前世今生

Web端用户视频推流使用webRTC方案,使用已有JS SDK(和这个兼容)。

WebRTC由Google主推,全称Web Browser Real Time Communication。目标是希望在Web端进行点对点音视频通信。

整个P2P过程很复杂,但是对于浏览器而言,WebRTC实际上只是提供了三个API:

  • MediaStream, 即getUserMedia(navigator.getUserMedia),获取媒体数据,如来自用户摄像头和麦克风的音视频流
  • RTCPeerConnection,用于peer跟peer之间呼叫和建立连接以便传输音视频数据流;这个不同浏览器的实现不同,官网推荐使用adapter.js进行适配
  • RTCDataChannel,用于peer跟peer之间传输音视频之外的一般数据。

MediaStream

参考MDN。来自navigator.getUserMedia(),这个方法接收三个参数:

  • 一个约束对象,如{ audio: false, video: true },除了这两种,其他外设也可以作为输入
  • 一个成功回调
  • 一个失败回调

返回的MediaStream对象有addTrack, getAudioTracks, getVideoTracks等方法。通过这些方法取出的MediaStreamTrack数组代表对应类型的流,可以把取出的这些流导入到<video>等标签输出。在Chrome或Opera中,URL.createObjectURL()方法可以转换一个MediaStream到一个Blob URL,可以被设置作为视频的源。除了这种方法,还可以使用AudioContextAPI,对音频做处理后输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
function gotStream(stream) {
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var audioContext = new AudioContext();

// Create an AudioNode from the stream
var mediaStreamSource = audioContext.createMediaStreamSource(stream);

// Connect it to destination to hear yourself
// or any other node for processing!
mediaStreamSource.connect(audioContext.destination);
}

navigator.getUserMedia({audio:true}, gotStream);

使用HTTPS请求getUserMedia会向用户给出一次提示。不建议在HTTP环境下使用。

这里是一个demo,打开console,查看全局变量stream就明白MediaStream结构了。

约束对象中可以商议视频分辨率等信息。它会影响获取到的视频流。

RTCPeerConnection

WebRTC使用RTCPeerConnection在浏览器间传递数据流,但在之前需要有一个交换信令的过程。这个过程不在WebRTC中定义,开发者可以使用任何想用的消息协议,比如WebSocket或XHR轮询什么的。信令过程需要传递三种信息:

  • 连接控制信息:初始化或者关闭连接报告错误。
  • 网络配置:对于外网,我们电脑的 IP 地址和端口?
  • 多媒体数据:使用什么编码解码器,浏览器可以处理什么信息

点对点的连接需要ICE(Interactive Connectivity Establishment)的帮助,ICE靠STUN和TURN服务器处理NAT穿透等复杂问题。起初连接建立在UDP之上,STUN服务器让位于NAT中的client获知自己的公网IP和端口。如果UDP建立失败,考虑TCP连接,再考虑HTTP和HTTPS连接。否则使用TURN服务器做中转工作。

W3C给了RTCPeerConnection的样例,

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
47
48
49
50
51
52
53
54
55
56
57
58
var signalingChannel = new SignalingChannel();
var configuration = { "iceServers": [{ "url": "stun:stun.example.org" }] };
var pc;

// 调用start()建立连接
function start() {
pc = new RTCPeerConnection(configuration);

// 有任何 ICE candidates 可用,
// 通过signalingChannel传递candidate给对方
pc.onicecandidate = function (evt) {
if (evt.candidate)
signalingChannel.send(JSON.stringify({ "candidate": evt.candidate }));
};

// let the "negotiationneeded" event trigger offer generation
pc.onnegotiationneeded = function () {
pc.createOffer(localDescCreated, logError);
}

// 远端流到达时,在remoteView中做展示
pc.onaddstream = function (evt) {
remoteView.src = URL.createObjectURL(evt.stream);
};

// 获取本地流,展示并传递
navigator.getUserMedia({ "audio": true, "video": true }, function (stream) {
selfView.src = URL.createObjectURL(stream);
pc.addStream(stream);
}, logError);
}

function localDescCreated(desc) {
pc.setLocalDescription(desc, function () {
signalingChannel.send(JSON.stringify({ "sdp": pc.localDescription }));
}, logError);
}

signalingChannel.onmessage = function (evt) {
if (!pc)
start();

var message = JSON.parse(evt.data);
if (message.sdp) {
pc.setRemoteDescription(new RTCSessionDescription(message.sdp), function () {
// 接收到offer时,回应一个answer
if (pc.remoteDescription.type == "offer")
pc.createAnswer(localDescCreated, logError);
}, logError);
} else {
// 接收对方candidate并加入自己的RTCPeerConnection
pc.addIceCandidate(new RTCIceCandidate(message.candidate));
}
};

function logError(error) {
log(error.name + ": " + error.message);
}

在开始建立连接时,调用start(),创建RTCPeerConnection对象,接着完成下面步骤:

  • 交换网络信息onicecandidate回调在有任何candidate出现时,将通过SignalChannel(使用额外方法创建,如WebSocket)传递给对方。同样地,在通过SignalChannel接收到对方发来的该信息时,加入这个candidate到RTCPeerConnection中。
  • 交换多媒体信息,使用SDP(Session Description Protocol)与对端交换多媒体资讯,在onnegotiationneeded中,调用createOffer通过setLocalDescription创建RTCSessionDecription对象进行本地存储,并传给对方。接收方通过setRemoteDescription方法设定remote description。

上述过程称为JavaScript Session Establishment Protocol(JSEP)。一旦这个signaling完成了,数据可以直接的在端到端之间进行数据传输。如果失败了,通过中介服务器relay服务进行转发。

通常RTCPeerConnection的API太复杂,所以有很多在此基础上的库封装出了更加友善的API。

JS-SDK接入流程

参考:Agora:视频通话API

  1. 准备工作,包括界面绘制等
  2. 向远端注册当前用户,获取token,为后面做准备
  3. 使用createClient()方法创建客户端
  4. 指定回调,包括已订阅流、已添加流、移除、失败等生命周期事件的回调
  5. 初始化客户端,传入appId和成功回调
  6. 初始化成功后,调用join方法根据获取到的token加入指定房间(原理是WebRTC的stream有id)
  7. 指定配置创建本地流(getUserMedia),发布本地流,播放本地流

RTMP相关

必读:RTMP H5 直播流技术解析

一次RTMP握手的模拟。

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 握手协议模拟
class C {
constructor(time, random) {
this.time = time || 0;
this.random = random || Buffer.alloc(0); // Buffer类型
}

get C0() {
const buf = Buffer.alloc(1, 3);
return buf;
}

get C1() {
const buf = Buffer.alloc(1536);
return buf;
}

/**
* write C2
*/
get C2() {
let buf = Buffer.alloc(1536);
buf.writeUInt32BE(this.time, 4);
this.random.copy(buf, 8, 0, 1528);
}
}

// 客户端
const client = new net.socket();
const RTMP_C = new C();

client.connect({
port: 1935,
host: 'example.live.com',
() => {
console.log('connected');
}
});

client.on('data', (data) => {
if (!data) {
console.warn('Empty Buffer.');
}
if (!RTMP_C.S0 && data.length > 0) {
RTMP_C.S0 = data.readUInt8(0);
data = data.slice(1);
}

if (!RTMP_C.S1 && data.length >= 1536) {
RTMP_C.time = data.readUInt32BE(0);
RTMP_C.random = data.slice(8, 1536);
RTMP_C.S1 = true;
data = data.slice(1536)
console.log('send C2');
client.write(RTMP_C.C2);
}

if (!RTMP_C.S2 && data.length >= 1536) {
RTMP_C.S2 = true;
data = data.slice(1536);
}
});