原文标题:Learning WebRTC peer-to-peer communication, part 1
作者:Swizec Teller
WebRTC对等通信
- 在之前的工作中,我们使用了区块链技术来实时共享客户端模块,本次我们用RTCPeerConnection建立了一个对等连接。
点击此处查看GitHub代码 - 自iOS11之后,WebRTC可以在所有浏览器中工作了,用户可以实时使用。
点击此处运行代码
我嵌入了一个HTML标签,浏览器的安全保护机制不允许这样做。
WebRTC提供了三种API:
- 从设备中获取音频和视频(mediastream)
- 建立了一个对等连接(RTCPeerConnection)
- 传递任意数据(RTCDataChannel)
本文使用了mediastream和PeerConnection.
无服务器的实时对等连接通信客户端
按照如下步骤在同一网页不同客户端之间建立连接
1.实例化两个RTCPeerConnection
对象。
2.添加彼此为ICE candidates。
3.对第一个对象使用createoffer
建立请求。
4.对两个对象设置本地/远程’描述’。
5.对第二个对象createAnswer
。
6.对两个对象设置远程/本地’描述’。
7.进行直接交流。
以下是代码实现
我们从一个react Component出发, 它可以传达两个视频和三个按钮, 并且有一些默认状态可供操作。
1class WebRTCPeerConnection extends React.Component {
2 state = {
3 startDisabled: false,
4 callDisabled: true,
5 hangUpDisabled: true,
6 servers: null,
7 pc1: null,
8 pc2: null,
9 localStream: null
10 };
11 localVideoRef = React.createRef();
12 remoteVideoRef = React.createRef();
13
14 start = () => {
15 // start media devices
16 };
17
18 call = () => {
19 // initiate a call
20 };
21
22 hangUp = () => {
23 // hang up connection
24 };
25 render() {
26 const { startDisabled, callDisabled, hangUpDisabled } = this.state;
27 return (
28 <div>
29 <video
30 ref={this.localVideoRef}
31 autoPlay
32 muted
33 style={{ width: "240px", height: "180px" }}
34 />
35 <video
36 ref={this.remoteVideoRef}
37 autoPlay
38 style={{ width: "240px", height: "180px" }}
39 />
40 <div>
41 <button onClick={this.start} disabled={startDisabled}>
42 Start
43 </button>
44 <button onClick={this.call} disabled={callDisabled}>
45 Call
46 </button>
47 <button onClick={this.hangUp} disabled={hangUpDisabled}>
48 Hang Up
49 </button>
50 </div>
51 </div>
52 );
53 }
54}
我们使用三个布尔值来控制按钮: null
、 pc1
、 pc2
。
第一步:开始
当点击Start
开始按钮时,请求视频/音频许可并且开始一个localstream
本地流。
1 start = () => {
2 this.setState({
3 startDisabled: true
4 });
5 navigator.mediaDevices
6 .getUserMedia({
7 audio: true,
8 video: true
9 })
10 .then(this.gotStream)
11 .catch(e => alert("getUserMedia() error:" + e.name));
12 };
13 gotStream = stream => {
14 this.localVideoRef.current.srcObject = stream;
15 this.setState({
16 callDisabled: false,
17 localStream: stream
18 });
19 };
使用this.setState
来禁止开始按钮,使用navigator.getUserMedia
来进入媒体.如果允许,我们在localVideo
中开始数据流并且把它加入到状态中。
第二步:调用
现在你可以按Call
按钮,这样就启动了两点连接,pc1
和pc2
,使它们可以相互交流。
1.call
开始请求。
2.onCreateOfferSuccess
更新pc1,pc2,并且初始化应答。
3.onCreateAnswerSuccess
结束连接。
4.gotRemoteStream
激发建立第二个视频。
1 call = () => {
2 this.setState({
3 callDisabled: true,
4 hangUpDisabled: false
5 });
6 let { localStream } = this.state;
7 let servers = null,
8 pc1 = new RTCPeerConnection(servers),
9 pc2 = new RTCPeerConnection(servers);
10 pc1.onicecandidate = e => this.onIceCandidate(pc1, e);
11 pc1.oniceconnectionstatechange = e => this.onIceStateChange(pc1, e);
12 pc2.onicecandidate = e => this.onIceCandidate(pc2, e);
13 pc2.oniceconnectionstatechange = e => this.onIceStateChange(pc2, e);
14 pc2.ontrack = this.gotRemoteStream;
15 localStream
16 .getTracks()
17 .forEach(track => pc1.addTrack(track, localStream));
18 pc1
19 .createOffer({
20 offerToReceiveAudio: 1,
21 offerToReceiveVideo: 1
22 })
23 .then(this.onCreateOfferSuccess, error =>
24 console.error(
25 "Failed to create session description",
26 error.toString()
27 )
28 );
29 this.setState({
30 servers,
31 pc1,
32 pc2,
33 localStream
34 });
35 };
这段代码几乎是前端模板。
我们开始或禁用相应的按钮,从状态中得到本地流localStream
,并且初始化servers
, pc1
和pc2
。
两个pc*
都有许多事件听众,onIceCandidate
会将其连接,并且通过onIceStateChange
打印故障信息,gotRemoteStream
会将其添加到正确的video
组件。
接着我们把localStream
添加到第一个客户端,pc1
会产生一个接收视频和音频的请求,当完成这些之后,就更新了组分状态。
onCreateOfferSuccess
当pc1
成功请求之后,我们更新客户端中本地和远程的描述。我不确定这些描述包括什么,但是这些资料很重要。
1 onCreateOfferSuccess = desc => {
2 let { pc1, pc2 } = this.state;
3 pc1
4 .setLocalDescription(desc)
5 .then(
6 () =>
7 console.log("pc1 setLocalDescription complete createOffer"),
8 error =>
9 console.error(
10 "pc1 Failed to set session description in createOffer",
11 error.toString()
12 )
13 );
14 pc2.setRemoteDescription(desc).then(
15 () => {
16 console.log("pc2 setRemoteDescription complete createOffer");
17 pc2
18 .createAnswer()
19 .then(this.onCreateAnswerSuccess, error =>
20 console.error(
21 "pc2 Failed to set session description in createAnswer",
22 error.toString()
23 )
24 );
25 },
26 error =>
27 console.error(
28 "pc2 Failed to set session description in createOffer",
29 error.toString()
30 )
31 );
32 };
pc1
更新本地描述,pc2
更新远程描述,pc2
同样产生了一个回复,就像这样:好,我接受你的请求,我们开始吧。
onCreateAnswerSuccess
当pc2
成功回复后,我们开始建立另一轮描述,只不过这次顺序正好相反。
1 onCreateAnswerSuccess = desc => {
2 let { pc1, pc2 } = this.state;
3 pc1
4 .setRemoteDescription(desc)
5 .then(
6 () =>
7 console.log(
8 "pc1 setRemoteDescription complete createAnswer"
9 ),
10 error =>
11 console.error(
12 "pc1 Failed to set session description in onCreateAnswer",
13 error.toString()
14 )
15 );
16 pc2
17 .setLocalDescription(desc)
18 .then(
19 () =>
20 console.log(
21 "pc2 setLocalDescription complete createAnswer"
22 ),
23 error =>
24 console.error(
25 "pc2 Failed to set session description in onCreateAnswer",
26 error.toString()
27 )
28 );
29 };
pc1
建立远程描述而pc2
建立本地描述。我想这就像是:从pc1
视角看来,对它是本地的,对pc2
是远程的,反过来对pc2
也一样。
现在,我们就有了两个视频流,可以在同一个网页中相互交流。
onIceCandidate
在这一步,两个pc
都说它们得到了ICE candidate。我不知道实际发生了什么,但是这给了我们一个来区分这两个客户端分别在对谁交流的机会。
1 onIceCandidate = (pc, event) => {
2 let { pc1, pc2 } = this.state;
3 let otherPc = pc === pc1 ? pc2 : pc1;
4 otherPc
5 .addIceCandidate(event.candidate)
6 .then(
7 () => console.log("addIceCandidate success"),
8 error =>
9 console.error(
10 "failed to add ICE Candidate",
11 error.toString()
12 )
13 );
14 };
我们猜测其它客户端,把它添加成为一个candidate。 如果我们的客户端多于两个,这会很有意思。
第三步:结束
结束很简单,只需要关闭两个客户端。
1 hangUp = () => {
2 let { pc1, pc2 } = this.state;
3 pc1.close();
4 pc2.close();
5 this.setState({
6 pc1: null,
7 pc2: null,
8 hangUpDisabled: true,
9 callDisabled: false
10 });
11 };
有趣的地方
第一部分连接,也就是两个客户端互相找到对方,成为发射信号。WebRTC spec没有提到发信号。
发信号在同一个网页两个客户端之间是很简单的,两个客户端就在内存中,只需要启动两个客户端。
但是在现实世界中,你需要这些客户端可以在不同计算机的不同浏览器中运行,如何做才能使它们能互相找到对方呢?如果有上千个客户端呢?
你需要一些通信频道,这些频道知道所有客户端在哪儿,并且说:嗨!你,连接我这里,或者是,你,离开我这里。
这对分布式去中心化区块链不起作用。