WebRTC对等连接(一):点对点通信

原文标题:Learning WebRTC peer-to-peer communication, part 1
作者:Swizec Teller

WebRTC对等通信

  • 在之前的工作中,我们使用了区块链技术来实时共享客户端模块,本次我们用RTCPeerConnection建立了一个对等连接。
    点击此处查看GitHub代码
  • 自iOS11之后,WebRTC可以在所有浏览器中工作了,用户可以实时使用。
    点击此处运行代码
    我嵌入了一个HTML标签,浏览器的安全保护机制不允许这样做。

WebRTC提供了三种API:

  • 从设备中获取音频和视频(mediastream)
  • 建立了一个对等连接(RTCPeerConnection)
  • 传递任意数据(RTCDataChannel)

本文使用了mediastream和PeerConnection.

无服务器的实时对等连接通信客户端

WechatIMG148

按照如下步骤在同一网页不同客户端之间建立连接

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}

我们使用三个布尔值来控制按钮: nullpc1pc2

第一步:开始

当点击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按钮,这样就启动了两点连接,pc1pc2,使它们可以相互交流。
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, pc1pc2

两个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没有提到发信号。

发信号在同一个网页两个客户端之间是很简单的,两个客户端就在内存中,只需要启动两个客户端。

但是在现实世界中,你需要这些客户端可以在不同计算机的不同浏览器中运行,如何做才能使它们能互相找到对方呢?如果有上千个客户端呢?

你需要一些通信频道,这些频道知道所有客户端在哪儿,并且说:嗨!你,连接我这里,或者是,你,离开我这里。

这对分布式去中心化区块链不起作用。

填写常用邮箱,接收社区更新

WebRTC 中文社区由

运营