{"id":18635900,"url":"https://github.com/qmcloud/webrtc_im","last_synced_at":"2025-04-04T23:06:51.709Z","repository":{"id":27808309,"uuid":"115254844","full_name":"qmcloud/WebRTC_IM","owner":"qmcloud","description":"纯 go 实现的分布式IM即时通讯系统。一对一呼叫、邀请呼叫、音视频通话、多人通话，陌生人交友、在线教学、在线医疗、腾讯会议，Zoom会议，钉钉课堂等多人音视频交互类场景.","archived":false,"fork":false,"pushed_at":"2023-05-05T02:20:52.000Z","size":17595,"stargazers_count":527,"open_issues_count":6,"forks_count":207,"subscribers_count":16,"default_branch":"master","last_synced_at":"2025-03-28T22:11:27.223Z","etag":null,"topics":["getusermedia","golang","php","redis","swoole-framework","webrtc"],"latest_commit_sha":null,"homepage":"https://www.onionnews.cn","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/qmcloud.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2017-12-24T10:09:38.000Z","updated_at":"2025-03-26T09:19:21.000Z","dependencies_parsed_at":"2024-04-05T11:45:26.022Z","dependency_job_id":null,"html_url":"https://github.com/qmcloud/WebRTC_IM","commit_stats":null,"previous_names":["qmcloud/webrtc_im","double-baller/webrtc_im"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qmcloud%2FWebRTC_IM","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qmcloud%2FWebRTC_IM/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qmcloud%2FWebRTC_IM/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/qmcloud%2FWebRTC_IM/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/qmcloud","download_url":"https://codeload.github.com/qmcloud/WebRTC_IM/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247261600,"owners_count":20910108,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["getusermedia","golang","php","redis","swoole-framework","webrtc"],"created_at":"2024-11-07T05:27:11.713Z","updated_at":"2025-04-04T23:06:51.682Z","avatar_url":"https://github.com/qmcloud.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"*  ### IM音视频聊天系统采用 golang + WebRTC \n*** \n## 演示：\n- Android下载：https://wwa.lanzouy.com/ii7TS00fxfva\n\n- IOS下载：https://wwa.lanzouy.com/iG8cL00fxm7i\n\n- H5：https://im.52webrtc.top\n\n***\n![演示地址](https://github.com/DOUBLE-Baller/WebRTC_IM/blob/master/IM.gif?raw=true)\n***\n**技术群：**\n![](https://img-blog.csdnimg.cn/20200623093238797.png)\n\n----------------\n如有兴趣可商业合作 （UI设计，定制开发，系统重构，代理推广等）\n\n微信：BCFind5 【请备注好信息】\n\n文档地址：https://www.52webrtc.top\n\n博客地址：https://blog.csdn.net/u012115197/article/details/106916635\n\n---------------------------------------\n\n## 基于B站开源GoIM架构方案：\n`GoIM` 一个支持集群的im及实时推送服务。\n## 核心功能\n* 1.支持tcp，websocket接入，消息互通\n* 2.离线消息同步\n* 3.单用户多设备同时在线\n* 4.单聊，群聊，以及超大群组 \u003e2W人\n* 5.服务水平扩展\n* 6.使用热插拔驱动设计\n\n## 特性\n * 轻量级\n * 高性能\n * 纯Golang实现\n * 支持单个、多个、单房间以及广播消息推送\n * 支持单个Key多个订阅者（可限制订阅者最大人数）\n * 心跳支持（应用心跳和tcp、keepalive）\n * 支持安全验证（未授权用户不能订阅）\n * 多协议支持（websocket，tcp）\n * 可拓扑的架构（job、logic模块可动态无限扩展）\n * 基于Kafka做异步消息推送\n\n\nTODO\n\n## 例子\n\n手把手教学跑起goim：https://github.com/DOUBLE-Baller/momo/tree/master/IM\n\nWebsocket: [Websocket Client Demo](https://github.com/DOUBLE-Baller/WebRTC_IM/tree/master/im/examples/javascript)\n\nAndroid: [Android](https://github.com/DOUBLE-Baller/WebRTC_IM/tree/master/goim-java-sdk)\n\niOS: [iOS](https://github.com/DOUBLE-Baller/WebRTC_IM/tree/master/goim-oc-sdk)\n\n## 文档\n[push http协议文档](./docs/push.md)推送接口\n\n## 集群\n\n### comet\n\ncomet 属于接入层，非常容易扩展，直接开启多个comet节点，修改配置文件中的base节点下的server.id修改成不同值（注意一定要保证不同的comet进程值唯一），前端接入可以使用LVS 或者 DNS来转发\n\n### logic\n\nlogic 属于无状态的逻辑层，可以随意增加节点，使用nginx upstream来扩展http接口，内部rpc部分，可以使用LVS四层转发\n\n### kafka\n\nkafka 可以使用多broker，或者多partition来扩展队列\n\n### router\n\nrouter 属于有状态节点，logic可以使用一致性hash配置节点，增加多个router节点（目前还不支持动态扩容），提前预估好在线和压力情况\n\n### job\n\njob 根据kafka的partition来扩展多job工作方式，具体可以参考下kafka的partition负载\n\n\n  \n***\nWebRtc篇 音视频部分 （重点难点部分）\n========\n\n# 背景介绍：\n##### 本文内容涉及到了WebRTC涉及的协议讲解、相关服务器的搭建、WebRTC核心API学习，最后包含一个WenRTC音视频通话的小实例开发教程实践（含完整代码）。测试过成都市内、成都↔武汉、成都↔北京、成都↔沈阳，基本都成功了。[了解更多...](https://github.com/DOUBLE-Baller/WebRTC_IM)\n:::\n\n\n# ​一、协议\n## 1.1 P2P通信原理与实现\n### 1.1.1 基本术语\n**防火墙（Firewall）**： 防火墙主要限制内网和公网的通讯，通常丢弃未经许可的数据包。防火墙会检测(但是不修改)试图进入内网数据包的IP地址和TCP/UDP端口信息。\n**网络地址转换协议**[（NAT）](http://en.wikipedia.org/wiki/NAT)： 用来给你的（私网）设备映射一个公网的IP地址的协议。一般情况下，路由器的WAN口有一个公网IP，所有连接这个路由器LAN口的设备会分配一个私有网段的IP地址（例如192.168.1.3）。私网设备的IP被映射成路由器的公网IP和唯一的端口，通过这种方式不需要为每一个私网设备分配不同的公网IP，但是依然能被外网设备发现。NAT不止检查进入数据包的头部，而且对其进行修改，从而实现同一内网中不同主机共用更少的公网IP（通常是一个）。\n**基本NAT（Basic NAT）**： 基本NAT会将内网主机的IP地址映射为一个公网IP，不改变其TCP/UDP端口号。基本NAT通常只有在当NAT有公网IP池的时候才有用。\n**网络地址-端口转换器（NAPT）**： 到目前为止最常见的即为NAPT，其检测并修改出入数据包的IP地址和端口号，从而允许多个内网主机同时共享一个公网IP地址。\n**锥形NAT（Cone NAT）**： 在建立了一对（公网IP，公网端口）和（内网IP，内网端口）二元组的绑定之后，Cone NAT会重用这组绑定用于接下来该应用程序的所有会话（同一内网IP和端口），只要还有一个会话还是激活的。 例如，假设客户端A建立了两个连续的对外会话，从相同的内部端点（10.0.0.1:1234）到两个不同的外部服务端S1和S2。Cone NAT只为两个会话映射了一个公网端点（155.99.25.11:62000）， 确保客户端端口的“身份”在地址转换的时候保持不变。由于基本NAT和防火墙都不改变数据包的端口号，因此这些类型的中间件也可以看作是退化的Cone NAT。\n```shell\n Server S1                                     Server S2\n18.181.0.31:1235                              138.76.29.7:1235\n       |                                             |\n       |                                             |\n       +----------------------+----------------------+\n                              |\n  ^  Session 1 (A-S1)  ^      |      ^  Session 2 (A-S2)  ^\n  |  18.181.0.31:1235  |      |      |  138.76.29.7:1235  |\n  v 155.99.25.11:62000 v      |      v 155.99.25.11:62000 v\n                              |\n                           Cone NAT\n                         155.99.25.11\n                              |\n  ^  Session 1 (A-S1)  ^      |      ^  Session 2 (A-S2)  ^\n  |  18.181.0.31:1235  |      |      |  138.76.29.7:1235  |\n  v   10.0.0.1:1234    v      |      v   10.0.0.1:1234    v\n                              |\n                           Client A\n                        10.0.0.1:1234\n```\n### 1.1.2 UDP打洞(UDP hole punching)\nP2P通信技术中被广泛采用的技术“UDP打洞”。UDP打洞技术依赖于通常防火墙和cone NAT允许正当的P2P应用程序在中间件中打洞且与对方建立直接链接的特性。\n在学习UDP打洞之前，我们先了解一下另外两种P2P通信技术。\n（1）中继（Relaying）\n中继是最可靠但效率最低的一种P2P通信技术，它的原理是通过一台服务器来中继转发不同客户端的数据。\n```shell\n Server S\n                          |\n                          |\n   +----------------------+----------------------+\n   |                                             |\n NAT A                                         NAT B\n   |                                             |\n   |                                             |\nClient A                                      Client B\n```\n\n\n什么意思呢？就是我和你开视频，我和你的视频数据会直接被我们共同连接上的一台服务器接收，这台服务器会将你我的视频数据分别转发响应给我和你的客户端。这样服务器压力就很大，带宽需求也非常大，当仅仅只有两个客户端连接服务器开视频的话，服务器的带宽就至少是客户端带宽的两倍，CPU消耗同样也是。那么当同时视频通话的人很多了，那么服务器的压力难以想象。\n所以中继是一种效率很低的P2P通信技术。\n（2）逆向连接（Connection reversal）\n这种连接只有在两个通信端点中有一个不存在中间件的时候有效。\n例如，客户端A在NAT之后而客户端B拥有全局IP地址，如下图：\n```shell\nServer S\n                        18.181.0.31:1235\n                               |\n                               |\n        +----------------------+----------------------+\n        |                                             |\n      NAT A                                           |\n155.99.25.11:62000                                    |\n        |                                             |\n        |                                             |\n     Client A                                      Client B\n  10.0.0.1:1234                               138.76.29.7:1234　\n```\n```shell\n客户端A内网地址为10.0.0.1，且应用程序正在使用TCP端口1234。A和服务器S建立了一个连接，服务器的IP地址为18.181.0.31，监听1235端口。NAT A给客户端A分配了TCP端口62000，地址为NAT的公网IP地址155.99.25.11， 作为客户端A对外当前会话的临时IP和端口。因此S认为客户端A就是155.99.25.11:62000。而B由于有公网地址，所以对S来说B就是138.76.29.7:1234。\n```\n\n\n当客户端B想要发起一个对客户端A的P2P链接时，要么链接A的外网地址155.99.25.11:62000，要么链接A的内网地址10.0.0.1:1234，然而两种方式链接都会失败。 链接10.0.0.1:1234失败自不用说，为什么链接155.99.25.11:62000也会失败呢？来自B的TCP SYN握手请求到达NAT A的时候会被拒绝，因为对NAT A来说只有外出的链接才是允许的。\n在直接链接A失败之后，B可以通过S向A中继一个链接请求，从而从A方向“逆向“地建立起A-B之间的点对点链接。\n现在很多P2P系统都实现了这种技术，但是这种技术有局限性，只有当其中一放客户端有公网IP的时候才能建立起连接。为什么现在很多P2P系统都实现了逆向连接技术，因为我们接下来要讲的UDP打洞技术，主要是依赖这种技术。\n## **UDP打洞正文开始**：\n现在最多的网路连接情况是双方都是在内网下，都需要通过NAT进行地址转换，所以上面的逆向连接不适用，但是可以利用逆向连接技术进行改造。\n假设客户端A和客户端B的地址都是内网地址，且在不同的NAT后面。A、B上运行的P2P应用程序和服务器S都使用了UDP端口1234，A和B分别初始化了 与Server的UDP通信，地址映射如图所示:\n```shell\n  Server S\n                        18.181.0.31:1234\n                               |\n                               |\n        +----------------------+----------------------+\n        |                                             |\n      NAT A                                         NAT B\n155.99.25.11:62000                            138.76.29.7:31000\n        |                                             |\n        |                                             |\n     Client A                                      Client B\n  10.0.0.1:1234                                 10.1.1.3:1234\n```\n\n\n现在假设客户端A打算与客户端B直接建立一个UDP通信会话。如果A直接给B的公网地址138.76.29.7:31000发送UDP数据，NAT B将很可能会无视进入的 数据（除非是Full Cone NAT），因为源地址和端口与S不匹配，而最初只与S建立过会话。B往A直接发信息也类似。\n假设A开始给B的公网地址发送UDP数据的同时，给服务器S发送一个中继请求，要求B开始给A的公网地址发送UDP信息。\nA往B的输出信息会导致NAT A打开 一个A的内网地址与与B的外网地址之间的新通讯会话，B往A亦然。一旦新的UDP会话在两个方向都打开之后，客户端A和客户端B就能直接通讯， 而无须再通过引导服务器S了。\nUDP打洞技术有许多有用的性质。一旦一个的P2P链接建立，链接的双方都能反过来作为“引导服务器”来帮助其他中间件后的客户端进行打洞， 极大减少了服务器的负载。应用程序不需要知道中间件具体是什么（如果有的话），因为以上的过程在没有中间件或者有多个中间件的情况下 也一样能建立通信链路。\n**还有一些特殊情况**：当通信双方都在同一局域网，也就是两个客户端都在一个内网下呢？是不是可以降低NAT转换，直接在内网上连接呢？此外还有，当一些大型企业，内网中有多级NAT转换呢？这里已不再本文的讨论中了，详细可以看以下参考文章详细了解：\n\u003e 参考文章：[https://www.52webrtc.top](https://www.52webrtc.top)\n\n学到这里，根据上面的原理是可以实现自己的一套程序和通信规则，但很多时候是需要对接第三方的协议，往往这个适配是比较麻烦的。因此就产生了标准化的通用规则（STUN、TURN、ICE），下面的几个章节将逐个介绍这些协议。\n## 1.2 STUN协议\nSTUN（[STUN/RFC3489(废弃)](http://www.rfc-editor.org/info/rfc3489)，[STUN/RFC5389](http://www.rfc-editor.org/info/rfc5389)）是P2P标准化通信规则（协议）之一。\n### 1.2.1 简介\nNAT的会话穿越功能[Session Traversal Utilities for NAT (STUN)](http://en.wikipedia.org/wiki/STUN) (缩略语的最后一个字母是NAT的首字母)是一个允许位于\n\n\nNAT后的客户端找出自己的公网地址，判断出路由器阻止直连的限制方法的协议。\nSTUN是一个C/S架构的协议，支持两种传输类型。一种是请求/响应（request/respond）类型，由客户端给服务器发送请求，并等待服务器返回响应；另一种是指示类型（indication transaction），由服务器或者客户端 发送指示，另一方不产生响应。对于请求/响应类型，允许客户端将响应和产生响应的请求连接起来； 对于指示类型，通常在debug时使用。我们主要了解请求/响应类型。\n### 1.2.2 通信过程\n客户端通过给公网的STUN服务器发送请求获得自己的公网地址信息，以及是否能够被（穿过路由器）访问。\n\n1. 客户端A向服务器产生一个Request（STUN叔叔，你能告诉我我的ip是多少吗）\n1. 服务器接收Request，检查报文是否合法，并生成Success响应或Error响应（A小朋友，你的ip是208.141.55.130:3255）\n\n![](https://cdn.nlark.com/yuque/0/2022/png/22838525/1645065022544-58092cd7-5f20-4898-bbdc-c89f628549b3.png#crop=0\u0026crop=0\u0026crop=1\u0026crop=1\u0026id=Fh2Xo\u0026originHeight=378\u0026originWidth=259\u0026originalType=binary\u0026ratio=1\u0026rotation=0\u0026showTitle=false\u0026status=done\u0026style=none\u0026title=)\n## 1.3 TURN协议\nTURN（[TURN/RFC5766](http://www.rfc-editor.org/info/rfc5766)）是P2P标准化通信规则（协议）之一，是对STUN的补充。\n### 1.3.1 简介\nTURN的全称为[Traversal Using Relays around NAT (TURN)](http://en.wikipedia.org/wiki/TURN) ，是STUN/RFC5389的一个拓展，主要添加了Relay功能。前面介绍的STUN协议处理的是市面上大多数的Cone NAT，但还有少量的设备使用的Symmetric NAT。因此传统的打洞方法不适用，为了保证这一部分设备能够建立通信，我们不得不通过中继（Relaying）的方法进行连接，这时就需要公网的服务器作为一个中继， 对来往的数据进行转发。这个转发的协议就被定义为TURN。这种情况会增加服务器负担，所以这是最坏的情况的通信解决方案。\nTURN服务器与客户端之间的连接都是基于UDP的，但是服务器和客户端之间可以通过其他各种连接来传输STUN报文, 比如TCP/UDP/TLS-over-TCP。客户端之间通过中继传输数据时候，如果用了TCP，也会在服务端转换为UDP，因此建议客户端使用 UDP来进行传输。至于为什么要支持TCP，那是因为一部分防火墙会完全阻挡UDP数据，而对于三次握手的TCP数据则不做隔离。\n### 1.3.2 通信过程\n客户端A向STUN服务器发送请求获取自己的公网地址，STUN服务器可以获取到客户端A的地址，但发现客户端A的使用的Symmetric NAT，因此STUN服务器告诉客户端A，我不能帮助你和客户端B建立连接，你们之间可以通过TURN进行连接。因此客户端A和客户端B同时去连接TURN服务器，通过TURN服务器进行中继连接。\n\n1. 客户端A向STUN服务器产生一个Request（STUN叔叔，你能告诉我我的ip是多少吗）\n1. STUN服务器响应（A小朋友，你的ip是208.141.55.130:3255，可是你的ip别人不能和你连接哦，你需要去找你TURN大伯，他是专门负责帮你连接）\n1. 客户端A向TURN服务器发起请求（TURN大伯，STUN叔叔叫我来找你）\n1. TURN服务器响应（A小侄儿，我知道了，但是现在还没有其他小朋友找你哦，你可以在这附近逛一逛，每10分钟要给我报告一下你还在这附近哦，一有其他小朋友来找你我就通知你。）\n\n![](https://cdn.nlark.com/yuque/0/2022/png/22838525/1645065089371-c383fdf2-2821-4973-af12-894d55c14f95.png#crop=0\u0026crop=0\u0026crop=1\u0026crop=1\u0026id=I4r5n\u0026originHeight=297\u0026originWidth=295\u0026originalType=binary\u0026ratio=1\u0026rotation=0\u0026showTitle=false\u0026status=done\u0026style=none\u0026title=)\n## 1.4 ICE协议\nTURN（[ICE/RFC5245](http://www.rfc-editor.org/info/rfc5245)）是P2P标准化通信规则（协议）之一，提供了完整的NAT传输解决方案。\nSTUN、TURN都是工具类协议，只提供穿透NAT的功能。且TURN本身就是被设计为ICE/RFC5245的一部分\n### 1.4.1 简介\nICE的全称为[Interactive Connectivity Establishment (ICE)](http://en.wikipedia.org/wiki/Interactive_Connectivity_Establishment)，即交互式连接建立。在实际的网络当中，有很多原因能导致简单的从A端到B端直连不能如愿完成。这需要绕过阻止建立连接的防火墙，给你的设备分配一个唯一可见的地址（通常情况下我们的大部分设备没有一个固定的公网地址），如果路由器不允许主机直连，还得通过一台服务器转发数据。ICE通过使用STUN、TURN、NAT、SDP技术完成上述工作。(引用自：[https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols))\nICE是一个用于在[Offer/Answer](http://www.rfc-editor.org/info/rfc3264)模式下的NAT传输协议，主要用于UDP下多媒体会话的建立，其使用了STUN协议以及TURN 协议，同时也能被其他实现了Offer/Answer模型的的其他程序所使用，比如[SIP](http://www.rfc-editor.org/info/rfc3261)(Session Initiation Protocol)。\n网络编程的ICE（Internate Communications Engine）：是一种用于分布式程序设计的网络通信中间件，本文指并非此ICE\n交互式连接ICE（Interactive Connectivity Establishment）：是一个允许你的浏览器和对端浏览器建立连接的协议框架。\n### 1.4.2 SDP会话描述\nICE信息的描述格式通常采用标准的[SDP](http://www.rfc-editor.org/info/rfc4566)，其全称为[Session Description Protocol (SDP)](http://en.wikipedia.org/wiki/Session_Description_Protocol) ，即会话描述协议。SDP不是一个真正的协议，而是一种数据格式，用于描述在设备之间共享媒体的连接。可以被其他传输协议用来交换必要的信息，如SIP和RTSP等。\n**SDP格式**：\nSDP由一行或多行UTF-8文本组成，每行以一个字符的类型开头，后跟等号（“ =”），然后是包含值或描述的结构化文本，其格式取决于类型。\nSDP会话描述包含了多行如下类型的文本:\n```shell\n\u003ctype\u003e=\u003cvalue\u003e\n```\n以给定字母开头的文本行通常称为“字母行”。例如，提供媒体描述的行的类型为“ m”，因此这些行称为“ m行”。\n```shell\nm=audio 49170 RTP/AVP 0\n```\n\u003ctype\u003e是大小写敏感的，其中一些行是必须要有的，有些是可选的，所有元素都必须以固定顺序给出。如下所示，其中可选的元素标记为* ：\n`会话描述`\n```shell\n     v=  (protocol version)\n     o=  (originator and session identifier)\n     s=  (session name)\n     i=* (session information)\n     u=* (URI of description)\n     e=* (email address)\n     p=* (phone number)\n     c=* (connection information -- not required if included in\n          all media)\n     b=* (zero or more bandwidth information lines)\n     One or more time descriptions (\"t=\" and \"r=\" lines; see below)\n     z=* (time zone adjustments)\n     k=* (encryption key)\n     a=* (zero or more session attribute lines)\n     Zero or more media descriptions\n```\n`时间信息描述:`\n```shell\n     t=  (time the session is active)\n     r=* (zero or more repeat times)\n```\n`多媒体信息描述(如果有的话):`\n```shell\n  m=  (media name and transport address)\n     i=* (media title)\n     c=* (connection information -- optional if included at\n          session level)\n     b=* (zero or more bandwidth information lines)\n     k=* (encryption key)\n     a=* (zero or more media attribute lines)\n```\n所有元素的type都为小写，并且不提供拓展.但是我们可以用a(attribute)字段来提供额外的信息。一个SDP描述的例子如下：\n```shell\nv=0\no=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\ns=SDP Seminar\ni=A Seminar on the session description protocol\nu=http://www.example.com/seminars/sdp.pdf\ne=j.doe@example.com (Jane Doe)\nc=IN IP4 224.2.17.12/127\nt=2873397496 2873404696\na=recvonly\nm=audio 49170 RTP/AVP 0\nm=video 51372 RTP/AVP 99\na=rtpmap:99 h263-1998/90000\n```\n\n\n具体字段的type/value描述和格式可以参考[RFC4566](http://www.rfc-editor.org/info/rfc4566)。\n### 1.4.3 Offer/Answer模型\nSDP用来描述多播主干网络的会话信息，但是并没有具体的交互操作细节是如何实现的，因此[RFC3264](https://link.zhihu.com/?target=http%3A//www.rfc-editor.org/info/rfc3264) 定义了一种基于SDP的Offer/Answer模型。\n在该模型中，会话参与者的其中一方生成一个SDP报文构成offer， 其中包含了一组offer希望使用的多媒体流和编解码方法，以及offer用来接收改数据的IP地址和端口信息。\noffer传输到会话的另一端(称为answer)，由这一端生成一个answer，即用来响应对应offer的SDP报文。\nanswer中包含不同offer对应的多媒体流，并指明该流是否可以接受。\n![](https://cdn.nlark.com/yuque/0/2022/png/22838525/1645065272967-f0b7952b-46e9-42b8-82e5-76e9f3babb86.png#crop=0\u0026crop=0\u0026crop=1\u0026crop=1\u0026id=Jxvx2\u0026originHeight=540\u0026originWidth=720\u0026originalType=binary\u0026ratio=1\u0026rotation=0\u0026showTitle=false\u0026status=done\u0026style=none\u0026title=)\n\n\n### 1.4.4 ICE工作流程\n一个典型的ICE工作环境如下，有两个端点A和B，都运行在各自的NAT之后(他们自己也许并不知道)，NAT的类型和性质也是未知的。L和R通过交换SDP信息在彼此之间建立多媒体会话，通常交换通过一个SIP服务器完成：\n```shell\n                 +-----------+\n                 |    SIP    |\n+-------+        |    Srvr   |         +-------+\n| STUN  |        |           |         | STUN  |\n| Srvr  |        +-----------+         | Srvr  |\n|       |        /           \\         |       |\n+-------+       /             \\        +-------+\n               /\u003c- Signaling -\u003e\\\n              /                 \\\n         +--------+          +--------+\n         |  NAT   |          |  NAT   |\n         +--------+          +--------+\n           /                       \\\n          /                         \\\n         /                           \\\n     +-------+                    +-------+\n     | Agent |                    | Agent |\n     |   A   |                    |   B   |\n     |       |                    |       |\n     +-------+                    +-------+\n```\n\n\nICE的基本思路是，每个终端都有一系列传输地址(包括传输协议，IP地址和端口)的候选，可以用来和其他端点进行通信。其中可能包括：\n\n- 直接和网络接口联系的传输地址(host address)\n- 经过NAT转换的传输地址,即反射地址(server reflective address)\n- TURN服务器分配的中继地址(relay address)\n\u003e 通过之前的学习，我们可以了解到每个终端的情况是比较复杂的（有的终端可能同时连着wifi和网线，有多个内网地址），所有每个终端有多种可以连接的方案。\n\n获取到这一系列传输地址后，会以一定优先级将地址排序。按照优先级和其他终端的传输地址进行组合检测连接可用性（连接性检查：Connectivity Checks）。\n两端连接性检查，是一个4次握手过程:\n```shell\nA                        B\n-                        -\nSTUN request -\u003e                  \\  A's\n          \u003c- STUN response       /  check\n\n           \u003c- STUN request       \\  B's\nSTUN response -\u003e                 /  check\n```\n**连接性检查详细过程**：\n\n1. 为中继候选地址生成许可(Permissions)；\n1. 从本地候选往远端候选发送Binding Request：在Binding请求中通常需要包含一些特殊的属性，以在ICE进行连接性检查的时候提供必要信息：\n   - PRIORITY 和 USE-CANDIDATE：优先级和候选\n   - ICE-CONTROLLED和ICE-CONTROLLING：标识本端是受控方还是主控方（offer生成方）。\n   - 生成Credential：STUN短期身份验证\n3. 处理Response：当收到Binding Response时，终端会将其与Binding Request相联系，通常生成事务ID。随后将会将此事务ID与候选地址对进行绑定。\n   - 成功响应：要同时满足三个条件（STUN传输产生一个Success Response；response的源IP和端口等于Binding Request的目的IP和端口；response的目的IP和端口等于Binding Request的源IP和端口）\n   - 失败响应：487错误，并将检测地址状态设置为Waiting\n\n以上仅对协议作了简单的介绍，具体服务器程序实现可参考：[https://github.com/evilpan/TurnServer](https://github.com/evilpan/TurnServer)\n## 1.5 经典WebRTC连接建立流程\n通过前面的协议了解学习，相信大家已经对WebRTC的底层连接流程有了一个模糊的意思，这里有张图展现了具体的连接流程。\n![](https://cdn.nlark.com/yuque/0/2022/png/22838525/1645065440169-112e9b07-0921-4b1f-b3f8-30126cb3091c.png#crop=0\u0026crop=0\u0026crop=1\u0026crop=1\u0026id=DFbWX\u0026originHeight=661\u0026originWidth=766\u0026originalType=binary\u0026ratio=1\u0026rotation=0\u0026showTitle=false\u0026status=done\u0026style=none\u0026title=)\n\u003e 引用自：[https://aggresss.blog.csdn.net/article/details/106832965](https://aggresss.blog.csdn.net/article/details/106832965)\n\n# 二、服务器搭建\n## 2.1 STUN/TURN服务器【可跳过】\n网上有公用的stun服务器，本节可直接跳过。\nSTUN服务器已有现成项目：[https://github.com/coturn/coturn](https://github.com/coturn/coturn)\n以下是在ubuntu上的安装和配置：\n### 2.1.1 安装coturn\n可以克隆github上的源码编译安装，在ubuntu里有直接的安装包\n```bash\napt-get -y update\napt-get -y install coturn\n```\n\n\n安装完毕后，先关闭coturn服务：\n```shell\nsystemctl stop coturn\n```\n​\n\n### 2.1.2 配置coturn\n**(1) 允许turnserver**\n首先需要允许turnserver，打开/etc/default/coturn文件，将注释去掉：\n```shell\nvim /etc/default/coturn\n```\n取消注释后如下：\n```shell\nTURNSERVER_ENABLED=1\n```\n**(2) 获取ip和SSL**\n首选需要获取一下自己的内网ip以及网卡:\n```shell\nifconfig\n```\n生成SSL证书:\n```shell\napt install openssl\nopenssl req -x509 -newkey rsa:2048 -keyout /etc/turn_server_pkey.pem -out /etc/turn_server_cert.pem -days 99999 -nodes \n```\n**(3) 配置**\n接下来正式改配置文件/etc/turnserver.conf，改之前先将原文件备份一个：\n```shell\nmv /etc/turnserver.conf /etc/turnserver.conf.bat\n```\n然后新建配置文件：\n```shell\nvim /etc/turnserver.conf\n```\n然后复制以下配置：\n```shell\nserver-name=turn.webrtc.zzboy.cn\nrealm=turn.webrtc.zzboy.cn\n\nfingerprint\n\nrelay-device=eth0   #与前ifconfig查到的网卡名称一致\n\nlistening-ip=192.168.0.186    #内网IP\nlistening-port=3478\ntls-listening-port=5349\nrelay-ip=192.168.0.186\nexternal-ip=121.36.105.109    #公网IP\n\nrelay-threads=50\nlt-cred-mech\nno-cli\nverbose\n\ncert=/etc/turn_server_cert.pem\npkey=/etc/turn_server_pkey.pem\n#pidfile=/var/run/turnserver.pid\nmin-port=49152\nmax-port=65535\nuser=jun:123456    #用户名密码，创建IceServer时用\n```\n### 2.1.3 测试\n工具：[Trickle ICE](https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/)\n点击打开上面的工具\n## 2.2 Nodejs构建信令服务器(Signal Server)\n信令服务器我直接使用的一个开源项目：[https://github.com/qdgx/WebRtcRoomServer](https://github.com/qdgx/WebRtcRoomServer)\n其实信令服务器已经涉及到实战了，这里就不讲具体实现，这里只先部署。\n单纯地看，信令服务器其实可以算作是一个后端项目，我们这里部署也只是对该项目进行服务器部署。这里我使用的这个开源项目是使用node.js开发的，因此部署步骤和node.js部署步骤相差无异。\n以下是我在ubuntu上的安装和配置：\n### 2.2.1 安装node环境\n**(1) 更新环境，安装curl、git**\n```shell\napt-get update\napt-get install -y curl git\n```\n**(2) 安装node.js**\n先去官网https://nodejs.org/，查看最新稳定长期支持版，发现最新稳定版是14.15.3 LTS，node.js的每个大版本号都有相对应的源，比如这里的14.15.3版本的源是 [https://deb.nodesource.com/setup_14.x](https://deb.nodesource.com/setup_14.x)\n所以在终端执行：\n```shell\ncurl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -\n```\n然后安装node.js\n```shell\napt-get install nodejs\n```\nnode -v 和 npm -v 查看node和npm是否安装成功\n### 2.2.2 克隆项目，安装依赖\n进入用户目录，克隆项目：\n```shell\ncd ~/ \u0026\u0026 git clone https://github.com/qdgx/WebRtcRoomServer.git\n```\n安装依赖：\n```shell\ncd ~/WebRtcRoomServer\nnpm i\n```\n启动服务：\n```shell\nnode app.js\n```\n在浏览器打开以下地址，测试一下是否访问：\n[https://你服务器外网地址:8443](https://xn--6qq22f55d4wakcs4l640b8l8b:8443/)\n只要浏览器提示该页面存在风险，即表示项目已生效，点击高级，选择接受风险继续访问即可。（为什么提示风险：因为这个项目的证书是自签名证书）\n![](https://cdn.nlark.com/yuque/0/2022/png/22838525/1645066216163-d0ea1e47-1827-4ec3-a6ab-5a727fd259e2.png#crop=0\u0026crop=0\u0026crop=1\u0026crop=1\u0026id=vInJW\u0026originHeight=1396\u0026originWidth=2062\u0026originalType=binary\u0026ratio=1\u0026rotation=0\u0026showTitle=false\u0026status=done\u0026style=none\u0026title=)\n如果无法访问，请检查服务器安全组是否打开了TCP和UDP协议的8443端口，有些服务器开端口需要在服务器上那配置安全组，比如阿里云ECS和华为云。\n### 2.2.3 pm2管理node服务\n直接用node app.js运行项目，在关闭终端后，node项目也会随之被关闭，因此需要使用额外的工具来保持node服务一直开启。\n安装pm2：\n```shell\nnpm install pm2@latest -g\n```\n启动服务：\n```shell\npm2 start app.js --name signal-server --watch\n```\n\n- name：给应用命名，可以不管\n- watch：相当于热更新，应用文件更新后会重启应用\n\u003e 有关pm2的使用，可以百度查询一下，也可以参考本人之前写的一篇文章：[https://www.zzboy.cn/Learning/f360ef90efef](https://www.zzboy.cn/Learning/f360ef90efef)\n\n# 三、API学习\n以下主要介绍下一章节实战开中需要到的常用接口，完整的接口学习可查看对应官方文档。\n## 3.1 [socket.io](http://socket.io/)\n官方文档：[https://socket.io/docs/v3/](https://socket.io/docs/v3/)\n中文w3chool：[https://www.w3cschool.cn/socket/](https://www.w3cschool.cn/socket/)\nSocket是一种**全双工通信**,当客户端和服务端建立起连接后，如果不主动断开，双方可以一直互相发送消息，适合于双方频繁通信的场景，也是支持服务端主动推送的一种通信方式。WebSocket是Html5推出的前端可以直接使用的API，不过目前项目中用的还是socket.io比较多。socket.io在浏览器环境下封装了WebSocket, 可以给开发者带来更好的体验，在功能上也更完善。\nsocket.io主要使用两个方法：\n\n- emit(description: string, data: any：监听事件；description是标识；data是需要发送的数据。\n- on(description: string, callback: function：监听事件；description表示监听的标识；callback是监到事件后处理方法，参数是emit发送的数据。\n\n通俗说，一个就是发送，一个是接收。发送方法需要指定谁(description)来接收；接收方法找到对应description接收。\n### 3.1.1 服务器端\n**(1) 安装**\n```shell\nnpm install socket.io\n```\n**(2) 初始化**\n```shell\nconst httpServer = require(\"http\").createServer(); // 创建http服务\n\n// 使用socket.io监听http服务\nconst socketIO = require(\"socket.io\");\nconst io = socketIO.listen(httpServer);\n\n// 也可以使用如下方式\nconst io = require(\"socket.io\")(httpServer, {\n  // options配置项\n});\n```\n配置项：是初始配置socket.io的一些参数，我们使用默认的接口，如需要配置，可以看文档了解具体配置项：[https://socket.io/docs/v3/server-api/#new-Server-httpServer-options](https://socket.io/docs/v3/server-api/#new-Server-httpServer-options)\n根据WebRTC安全策略，我们需要使用https，因此，比较**完整的初始化代码**为：\n```shell\nconst fs = require('fs');\nconst server = require('https').createServer({\n  key: fs.readFileSync('/tmp/key.pem'),\n  cert: fs.readFileSync('/tmp/cert.pem')\n});\nconst options = { /* ... */ };\nconst io = require('socket.io')(server, options);\n\nio.on('connection', socket =\u003e { /* ... */ });\n\nserver.listen(3000);\n\n```\n**(3) 方法**\n**io.on(‘connection’, fn)** ：监听客户端连接\n从上面初始化代码不难看出，socket.io第一个方法应该io.on('connection', fn)。\nconnection是保留description，当有客户端连接上当前服务器时，就会触发。\n我们需要在其回调中处理相关业务：\n```shell\nio.on('connection', socket =\u003e {\n  // 监听断开连接\n  socket.on('disconnect', reason =\u003e console.log(reason)) // socket断开监听，disconnect也是保留字段\n  \n\t// 其他业务监听\n  socket.on('join', data =\u003e console.log(`欢迎${data.name}进入直播间`));\n});\n```\n**socket.on(‘disconnect’, fn)** ：监听客户端断开连接\n```shell\nsocket.on('disconnect', reason =\u003e {\n  console.log(reason); // 断开原因有很多，可能是用户主动断开，也可能是浏览器直接关闭等\n})\n```\nsocket.emit() : 发送信息\n### 3.1.2 客户端\n## 3.2 音视频相关API\n### 3.2.1 navigator.mediaDevices\n浏览器API，可以通过该浏览器API获取用户媒体设备，通常只会用到一个方法：getUserMedia(options)，调用该方法时，浏览器会弹出请求音频或视频的权限，用户同意授权过后，即可获取到音视频流。\n```shell\nnavigator.mediaDevices.getUserMedia(options)\n.then(function(stream) {\n  /* use the stream */\n})\n.catch(function(err) {\n  /* handle the error */\n});\n```\n需要注意：navigator的mediaDevices属性需要在https环境下才会有，这是浏览器的限制。\n**options: 配置项**\n一般可直接设置为：{ audio: true, video: true }，表示为获取音频和视频。\n```shell\nnavigator.mediaDevices.getUserMedia({\n  audio: true,\n  video: true\n})\n.then(function(stream) {\n  /* use the stream */\n})\n.catch(function(err) {\n  /* handle the error */\n});\n```\n视频方面，也可以准确定义视频画面的宽高：\n```shell\nnavigator.mediaDevices.getUserMedia({\n  audio: true,\n  video: { width: 1280, height: 720 } // 当定义宽高是，视频算是true，请求视频权限\n})\n.then(function(stream) {\n  /* use the stream */\n})\n.catch(function(err) {\n  /* handle the error */\n});\n```\n\u003e 其他更多配置可参考：[https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia)\n\n### 3.2.2 video\n**(1) video标签**\n```shell\n\u003cvideo src=\"path/to/movie.mp4\" controls=\"controls\"\u003e\n您的浏览器不支持 video 标签。\n\u003c/video\u003e\n```\n属性：\n\n- autoplay: 如果出现该属性，则视频在就绪后马上播放\n- controls：如果出现该属性，则向用户显示控件，比如播放按钮\n- loop：如果出现该属性，则当媒介文件完成播放后再次开始播放\n- muted：规定视频的音频输出应该被静音\n- poster：规定视频下载时显示的图像，或者在用户点击播放按钮前显示的图像\n- preload：如果出现该属性，则视频在页面加载时进行加载，并预备播放。如果使用 “autoplay”，则忽略该属性\n- src：要播放的视频的 URL\n- width：设置视频播放器的宽度，单位px\n- height：设置视频播放器的高度，单位px\n\n我们在进行音视频通话时，通常\n**本地视频（我方视频）**应如下：\n```shell\n\u003cvideo id=\"local\" muted autoplay\u003e\n您的浏览器不支持 video 标签。\n\u003c/video\u003e\n```\n本地视频静音播放，因为我们无需我们自己发出的声音，因为我们到时候视频资源是从设备直接实时获取视频流，因此无需设置src，并且设置autoplay，可以让我们获取到视频流直接播放。\n**远程视频（对方视频）**应如下：\n```shell\n\u003cvideo id=\"remote\" poster=\"xxx\" autoplay\u003e\n您的浏览器不支持 video 标签。\n\u003c/video\u003e\n```\n远程视频同样设置autoplay属性，让接收到的视频流直接播放。另外可设置一个poster属性，可以在呼叫过程中或者被呼叫时，让页面显示呼叫中或者是显示对方头像肖像等，不然页面全黑会显得很尴尬。\n**(2) video对象**\n使用音视频通话，我们控制音视频的播放基本通过js实现的，就连前面介绍的video标签一般都是通过js创建。video对象有很多属性，我这里只简单介绍部分属性，能基本满足WebRTC音视频通话。\n我们要实现音视频实时通讯，传递的数据是音视频流，音视频流怎么让video播放出来呢？看看下面代码：\n```shell\n/**\n * 视频流绑定到video节点展示\n * @param {dom} video video节点\n * @param {obj} stream 视频流\n */\nconst pushStreamToVideo = (video, stream) =\u003e {\n  video.srcObject = stream;\n}\n\n// 获取video节点\nconst domLocalVideo = $('#local');\n\n// 调用摄像头\nnavigator.mediaDevices.getUserMedia({\n  audio: true,\n  video: true\n})\n.then(stream =\u003e {\n  pushStreamToVideo(domLocalVideo[0], stream); // 实时显示\n})\n.catch(err =\u003e {\n  alert(`getUserMedia() error: ${err.name}`)\n});\n```\n不难看出，video对象有个srcObject的属性，初始时该属性值是null，将我们获取到音视频流直接赋值给该属性，我们的video标签就可以实时播放了。上面这个例子是调用本地摄像头并展示到一个id=local的video标签上，需要在https上就可以正常运行了。\n**我们如何关闭视频呢？**\n方法一：简单粗暴，关闭页面或者关闭浏览器。（你会让用户这么干么？）\n方法二：使用MediaStream.getTracks()，获取到所有媒体流轨道，每条轨道调用一个方法stop()，就可以关闭当前流，摄像头也会停止录制。\n```shell\n/**\n * 关闭摄像头\n * @param {dom} video video节点\n */\nconst closeCamera = video =\u003e {\n  video.srcObject.getTracks()[0].stop(); // audio\n  video.srcObject.getTracks()[1].stop(); // video\n}\n```\n音频是第一条轨道，视频是第二条轨道，两个同时关闭即可。\n## 3.3 WebRTC\n官方文档（不推荐）：[https://www.w3.org/TR/webrtc/#peer-to-peer-connections](https://www.w3.org/TR/webrtc/#peer-to-peer-connections)\n官方文档中文翻译（不推荐）：[https://github.com/RTC-Developer/WebRTC-Documentation-in-Chinese/tree/master/resource](https://github.com/RTC-Developer/WebRTC-Documentation-in-Chinese/tree/master/resource)\nMDN Web Docs（推荐）：[https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API)\n### 3.3.1 RTCPeerConnection\n[https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/)\nRTCPeerConnection是浏览器之间点对点连接的核心API，用于处理对等体之间流数据的稳定和有效通信，\n```shell\nconst pc = new RTCPeerConnection(serverConfig);\n```\nserverConfig包含iceServers参数，它包含有关STUN和TURN服务器相关信息数组，在查找ICE的时候候选使用。可以在网上找一些公共的STUN服务器，也可以使用前面章节我们自己通过coturn搭建的STUN服务器。\n```shell\nconst serverConfig = {\n  iceServers: [\n    {\n      urls: 'stun:stun.xten.com'\n    },\n    {\n      urls: 'stun:你的服务器ip:3478', // 见2.1服务器搭建\n      username: '用户名',\n      credential: '密码'\n    }\n  ]\n}\n\n```\n\n\n**(1) onicecandidate = eventHandler**\n作用：监听RTCPeerConnection实例上发生icecandidate事件，该函数会返回ICE协商结果，我们需要将结果发送给信令服务器，交由信令服务器转发给对方。\n```shell\npc.onicecandidate = event =\u003e {\n  if (event.candidate) {\n    sendCandidateToRemotePeer(event.candidate);\n  } else {\n    /* there are no more candidates coming during this negotiation */\n  }\n};\n```\n**(2) ontrack = eventHandler **\n作用：监听RTCPeerConnection实例上接收到远程的数据流，该函数可获取到对端的媒体流。\n```shell\npc.ontrack = event =\u003e {\n  document.getElementById(\"received_video\").srcObject = event.streams[0];\n};\n```\n**(3) addTrack(track, stream…)**\n作用：设置轨道，该轨道将会在连同后传输到对端。\n```shell\nasync openCall(pc) {\n  const gumStream = await navigator.mediaDevices.getUserMedia({video: true, audio: true});\n  for (const track of gumStream.getTracks()) {\n    pc.addTrack(track);\n  }\n}\n\n```\nMDN不建议使用addStream()\n**(3) removeTrack(sender)**\n作用：删除轨道，删除已添加的轨道，用于挂断的时候\n```shell\nvar pc, sender;\nnavigator.getUserMedia({video: true}, function(stream) {\n  pc = new RTCPeerConnection();\n  var track = stream.getVideoTracks()[0];\n  sender = pc.addTrack(track, stream);\n});\n\ndocument.getElementById(\"closeButton\").addEventListener(\"click\", function(event) {\n  pc.removeTrack(sender);\n  pc.close();\n}, false);\n```\n不建议的：[onremovestream](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/onremovestream)\n**(5) setLocalDescription()/setRemoteDescription()**\n[setLocalDescription(sessionDescription)](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setLocalDescription)：\n设置本地offer，将自己的描述信息加入到PeerConnection中，参数类型：RTCSessionDescription（见下一小节 3.2.2 RTCSessionDescription）\n[setRemoteDescription(sessionDescription)](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setRemoteDescription)：\n设置远端的answer，将对方的描述信息加入到PeerConnection中，参数类型：RTCSessionDescription（见下一小节 3.2.2 RTCSessionDescription）\n![](https://cdn.nlark.com/yuque/0/2022/png/22838525/1645066636445-7b0906de-a8c3-4d15-af4f-c67616efd508.png#crop=0\u0026crop=0\u0026crop=1\u0026crop=1\u0026id=itLxU\u0026originHeight=540\u0026originWidth=720\u0026originalType=binary\u0026ratio=1\u0026rotation=0\u0026showTitle=false\u0026status=done\u0026style=none\u0026title=)\n通俗说：Alice为了和Bob建立合作关系(连接)，Alice我把拟好了一份合同，并签字了，我这里先保留扫描版，纸质合同通过快递(SDP)给你了，你通过快递(SDP)拿到合同后，先签字确认，这时候纸质合同上都有我们双方的签名了，但我这边还没有你的签名。你保存一下扫描版，然后通过快递把纸质再给我发回来，我拿到快递后，我也保存一下扫描版。这样，你我双放都有双方签名的扫描版合同。合同开始生效！\n**(6) createOffer()/createAnswer()**\n[createOffer([options])](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createOffer)：\n创建一个offer，表示我方的请求。通常在WebRTC通信中，我们会请求对方接收我们的音频和视频数据。\n```shell\nconst offerOptions = {\n  offerToReceiveAudio: true, // 请求接收音频\n  offerToReceiveVideo: true, // 请求接收视频\n},\npc.createOffer(offerOptions)\n        .then(offer =\u003e onCreateOfferSuccess(offer.sdp))\n        .catch(error =\u003e onCreateOfferError());\n\n```\n[createAnswer([options])](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createAnswer)：\n创建一个answer，回应对方offer。answer也是有offer作用的，在回应的时候，表示答应你，并向你请求。\n打个比方：A向B表白，请求B做A的女朋友。如果B接受了，表示B成了A女朋友。同时，这也有另外一层含义，表示B有请求：请A做我的男朋友。\n```shell\nconst answerOptions = {\n  offerToReceiveAudio: true, // 请求接收音频\n  offerToReceiveVideo: true, // 请求接收视频\n},\npc.createAnswer(answerOptions)\n        .then(answer =\u003e onCreateAnswerSuccess(answer.sdp))\n        .catch(error =\u003e onCreateAnswerError());\n```\n### 3.3.2 RTCSessionDescription\n用于生成Offer/Answer协商过程中SDP协议的相关描述。\n```shell\nnew RTCSessionDescription(rtcDescription)\n```\nrtcDescription只有两个属性：type，sdp\n\n- type只能设置：‘answer’，‘offer’，‘pranswer’，‘rollback’；\n- sdp是标准的SDP会话描述（可由createOffer/createAnswer生成）\n### 3.3.3 RTCIceCandidate\n[https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate)\n[https://blog.51cto.com/zhangjunhd/25481](https://blog.51cto.com/zhangjunhd/25481)\n用于建立ICE连接。通常我们不会手动去实例化一个RTCIceCandidate对象，在前面3.3.1 RTCPeerConnection中的onicecandidate事件回调就是一个RTCIceCandidate对象，我们只需要了解其中几个属性即可。\n\n- **candidate**: 用于连接性检测的对象\n- **sdpMid**: candidate的媒体流的识别标签\n- **sdpMLineIndex**: candidate的媒体流的相关联的SDP描述索引号\n- address: 本机IP地址\n- relatedAddress: 中继IP\n- port: 本机端口\n- relatedPort: 中继端口\n- component: 候选协议，只有两种情况：RTP(Real-Time Transport Protocol)， RTCP(Real-Time Transport Control Protocol)\n- foundation: 来自于STUN服务器的唯一标识符\n- priority: 优先级\n- tcpType: 如果使用的TCP协议，这个属性及表示TCP的状态\n- type: [RTCIceCandidateType类型](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidateType)\n- usernameFragment: ice-ufrag片段，用于生成ice-pwd，同一ICE进程的连接都将使用的是同一个片段。\n# 四、实战开发\n前面基本上已经列举了大部分基础知识，现在开始运用起来。\n本章实战开发，是开发一个 **web实时音视频聊天室** ：输入相同房间号，即可加入聊天室，进行视频聊天。\n主要有两个项目，前端界面([页面+WebRTC+socket.io](http://xn--+webrtc+socket-9768by1h.io/))，后端信令服务器控制转发([Express+socket.io](http://express+socket.io/))。\n整个项目完整代码：[WebRTC-demo](https://github.com/DOUBLE-Baller/momo/)\n## 4.1 环境准备\n\n- anywhere: npm i -g anywhere\n## 4.2 信令服务器\n因为信令服务器代码结构比较简单，咱们先开发信令服务器。观察1.5 经典WebRTC连接建立流程，不难发现，信令服务器主要需要实现：转发offer、转发answer、转发candidate的三大核心功能。此外，我们开发聊天室，还需要：创建聊天室、退出聊天室的功能。\n​\n\n### 4.2.1 搭建项目\n（1）创建一个文件夹signal-server，在目录下创建两个文件：\npackage.json\n```shell\n{\n  \"name\": \"signal-server\",\n  \"version\": \"1.0.0\",\n  \"author\": \"Patrick Jun\",\n  \"description\": \"A webRTC signal server\",\n  \"scripts\": {\n    \"start\": \"node app.js\"\n  },\n  \"dependencies\": {\n    \"express\": \"^4.17.1\",\n    \"express-session\": \"^1.17.1\",\n    \"socket.io\": \"^2.3.0\"\n  }\n}\n```\n\n\napp.js\n```shell\nconst https = require('https');  // https服务\nconst fs = require('fs');        // fs\nconst socketIO = require('socket.io');\n\n//读取密钥和签名证书\nconst options = {\n  key: fs.readFileSync('keys/server_key.pem'),\n  cert: fs.readFileSync('keys/server_crt.pem'),\n}\n\n// 构建https服务器\nconst apps = https.createServer(options);\n\nconst SSL_PORT = 8443;\n\napps.listen(SSL_PORT);\n\n\n// 构建signal server\nconst io = socketIO.listen(apps);\n\n// socket监听连接\nio.sockets.on('connection', (socket) =\u003e {\n  console.log('连接建立');\n  // 之后所有业务处理，写在这里面\n});\n\n```\n\n\n（2）创建证书\n在项目文件夹下，创建一个文件夹keys，然后开始生成自签名证书：\nlinux环境下：\n```shell\nopenssl req -x509 -newkey rsa:2048 -keyout ./keys/server_key.pem -out ./keys/server_crt.pem -days 99999 -nodes \n```\n\n\nwindows下：参考 [https://letsencrypt.org/zh-cn/docs/certificates-for-localhost/](https://letsencrypt.org/zh-cn/docs/certificates-for-localhost/)\n修改app.js，将秘钥和签名证书的路径改为你电脑中的绝对路径，例如：\n```shell\n//读取密钥和签名证书\nconst options = {\n  key: fs.readFileSync('D://signal-server/keys/server_key.pem'),\n  cert: fs.readFileSync('D://signal-server/keys/server_crt.pem'),\n}\n\n```\n\n\n（3）运行\n在项目根目录下，安装依赖：\n```shell\nnpm i\n```\n然后，启动：\n```shell\nnode app.js\n```\n打开浏览器，访问：https://localhost:8443\n访问时，浏览器会提示不安全的访问，这个时候，直接敲键盘：thisisunsafe 即可继续访问。当看到浏览器地址栏继续一直在请求中，那么就表示项目成功运行。\n### 4.2.2 房间功能\n房间功能主要包括：创建/加入房间、退出房间。\n业务处理，都放在连接成功后的回调函数里。\n（1）创建房间\n```shell\n// socket监听连接\nio.sockets.on('connection', (socket) =\u003e {\n  console.log('连接建立');\n  \n  // 创建/加入房间\n  socket.on('createAndJoinRoom', (message) =\u003e {\n    const { room } = message;\n    console.log('Received createAndJoinRoom：' + room);\n    // 判断room是否存在\n    const clientsInRoom = io.sockets.adapter.rooms[room];\n    const numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;\n    console.log('Room ' + room + ' now has ' + numClients + ' client(s)');\n    if (numClients === 0) {\n      // room 不存在 不存在则创建（socket.join）\n      // 加入并创建房间\n      socket.join(room);\n      console.log('Client ID ' + socket.id + ' created room ' + room);\n\n      // 发送消息至客户端 [id,room,peers]\n      const data = {\n        id: socket.id, //socket id\n        room: room, // 房间号\n        peers: [], // 其他连接\n      };\n      socket.emit('created', data);\n    } else {\n      // room 存在\n      // 加入房间中\n      socket.join(room);\n      console.log('Client ID ' + socket.id + ' joined room ' + room);\n      \n      // joined告知房间里的其他客户端 [id,room]\n      io.sockets.in(room).emit('joined', {\n        id: socket.id, //socket id\n        room: room, // 房间号\n      });\n\n\n      // 发送消息至客户端 [id,room,peers]\n      const data = {\n        id: socket.id, //socket id\n        room: room, // 房间号\n        peers: [], // 其他连接\n      };\n      // 查询其他连接\n      const otherSocketIds = Object.keys(clientsInRoom.sockets);\n      for (let i = 0; i \u003c otherSocketIds.length; i++) {\n        if (otherSocketIds[i] !== socket.id) {\n          data.peers.push({\n            id: otherSocketIds[i],\n          });\n        }\n      }\n      socket.emit('created', data);\n    }\n  });\n  \n});\n```\n\n\n（2）退出房间\n在加入房间监听后面，继续添加：\n```shell\n// 退出房间，转发exit消息至room其他客户端 [from,room]\nsocket.on('exit', (message) =\u003e {\n  console.log('Received exit: ' + message.from + ' message: ' + JSON.stringify(message));\n  const { room } = message;\n  // 关闭该连接\n  socket.leave(room);\n  // 转发exit消息至room其他客户端\n  const clientsInRoom = io.sockets.adapter.rooms[room];\n  if (clientsInRoom) {\n    const otherSocketIds = Object.keys(clientsInRoom.sockets);\n    for (let i = 0; i \u003c otherSocketIds.length; i++) {\n      const otherSocket = io.sockets.connected[otherSocketIds[i]];\n      otherSocket.emit('exit', message);\n    }\n  }\n});\n```\n\n\n还有一种情况，当socket连接异常断开时，也需要退出房间：\n```shell\n// socket关闭\nsocket.on('disconnect', function(reason){\n  const socketId = socket.id;\n  console.log('disconnect: ' + socketId + ' reason:' + reason );\n  const message = {\n    from: socketId,\n    room: '',\n  };\n  socket.broadcast.emit('exit', message);\n});\n```\n\n\n### 4.2.3 转发功能\n转发功能有：转发offer、转发answer、转发candidate\n（1）转发offer\n```shell\n// 转发offer消息至room其他客户端 [from,to,room,sdp]\nsocket.on('offer', (message) =\u003e {\n  // const room = Object.keys(socket.rooms)[1];\n  console.log('收到offer: from ' + message.from + ' room:' + message.room + ' to ' + message.to);\n  // 根据id找到对应连接\n  const otherClient = io.sockets.connected[message.to];\n  if (!otherClient) {\n    return;\n  }\n  // 转发offer消息至其他客户端\n  otherClient.emit('offer', message);\n});\n```\n\n\n（2）转发answer\n```shell\n// 转发answer消息至room其他客户端 [from,to,room,sdp]\nsocket.on('answer', (message) =\u003e {\n  // const room = Object.keys(socket.rooms)[1];\n  console.log('收到answer: from ' + message.from + ' room:' + message.room + ' to ' + message.to);\n  // 根据id找到对应连接\n  const otherClient = io.sockets.connected[message.to];\n  if (!otherClient) {\n    return;\n  }\n  // 转发answer消息至其他客户端\n  otherClient.emit('answer', message);\n});\n```\n\n\n（3）转发candidate\n```shell\n// 转发candidate消息至room其他客户端 [from,to,room,candidate[sdpMid,sdpMLineIndex,sdp]]\nsocket.on('candidate', (message) =\u003e {\n  console.log('收到candidate: from ' + message.from + ' room:' + room + ' to ' + message.to);\n  // 根据id找到对应连接\n  const otherClient = io.sockets.connected[message.to];\n  if (!otherClient) {\n    return;\n  }\n  // 转发candidate消息至其他客户端\n  otherClient.emit('candidate', message);\n});\n```\n## 4.3 前端\n前端可以分为三大功能：音视频设备控制和音视频显示控制、Offer/Answer沟通、ICE连接。\n![](https://cdn.nlark.com/yuque/0/2022/png/22838525/1645066923478-96900f7b-b55a-4f83-b69a-1814dd7db1eb.png#clientId=ue8377149-284d-4\u0026crop=0\u0026crop=0\u0026crop=1\u0026crop=1\u0026from=paste\u0026id=u3cd0059b\u0026margin=%5Bobject%20Object%5D\u0026originHeight=661\u0026originWidth=766\u0026originalType=url\u0026ratio=1\u0026rotation=0\u0026showTitle=false\u0026status=done\u0026style=none\u0026taskId=u447c6c20-4c62-4245-b4e5-b52c5b83acb\u0026title=)\n### 4.3.1 搭建项目\n（1）创建一个文件夹webrtc-client，在目录下创建一个index.html文件，创建一个目录`js\n```shell\n|- webrtc-client/\n   |- js/\n   |- index.html\n```\n（2）在js目录下创建几个文件，并在从网上下载socket.io.js和jquery.min.js文件\n```shell\n|- webrtc-client/\n   |- js/\n      |- config.js\n      |- sdk.js\n      |- main.js\n      |- socket.io.js  // 自行从网上下载\n      |- jquery.min.js // 自行从网上下载\n   |- index.html\n```\n（3）代码\nindex.html\n```shell\n\u003chtml\u003e\n\u003chead\u003e\n    \u003cmeta charset=\"UTF-8\"\u003e\n    \u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no\"\u003e\n    \u003ctitle\u003eWebRtc视频通话demo\u003c/title\u003e\n    \u003cstyle\u003e\n      video {\n        background-color: bisque;\n      }\n    \u003c/style\u003e\n    \u003cscript src=\"js/jquery.min.js\"\u003e\u003c/script\u003e\n    \u003cscript src=\"js/socket.io.js\"\u003e\u003c/script\u003e\n    \u003cscript src=\"js/config.js\"\u003e\u003c/script\u003e\n    \u003cscript src=\"js/sdk.js\"\u003e\u003c/script\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n    \u003cinput type=\"text\" id=\"room\" value=\"1\" placeholder=\"输入房间号\" /\u003e\n    \u003cbutton id=\"connect\"\u003e连接\u003c/button\u003e\n    \u003cbutton id=\"logout\"\u003e挂断\u003c/button\u003e\n    \u003cbr/\u003e\n\n    \u003ch3\u003e本地视频\u003c/h3\u003e\n    \u003cvideo id=\"localVideo\" style='width:200px;height:200px;' autoplay muted\u003e\u003c/video\u003e\n    \u003cbr/\u003e\n    \n    \u003ch3\u003e远程视频\u003c/h3\u003e\n    \u003cdiv id='remoteDiv'\u003e\u003c/div\u003e\n    \u003cscript src=\"js/main.js\"\u003e\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\nconfig.js\n```shell\n// WebRTC配置文件\nconst THSConfig = {\n  // 信令服务器\n  signalServer: 'wss://localhost:8443',\n  // Offer/Answer模型请求配置\n  offerOptions: {\n    offerToReceiveAudio: true, // 请求接收音频\n    offerToReceiveVideo: true, // 请求接收视频\n  },\n  // ICE服务器\n  iceServers: {\n    iceServers: [\n      { urls: 'stun:stun.xten.com' }, // Safri兼容：url -\u003e urls\n    ]\n  }\n}\n```\n\n\n### 4.3.2 兼容预处理\n因为部分web API在不同浏览器有不同的名称或者属性，因此需要处理兼容，以下是兼容代码，预先定义一下。\n编辑sdk.js：\n```shell\n// 兼容处理\nconst PeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;\nconst SessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription;\nconst GET_USER_MEDIA = navigator.getUserMedia ? \"getUserMedia\" :\n                     navigator.mozGetUserMedia ? \"mozGetUserMedia\" :\n                     navigator.webkitGetUserMedia ? \"webkitGetUserMedia\" : \"getUserMedia\";\nconst v = document.createElement(\"video\");\nconst SRC_OBJECT = 'srcObject' in v ? \"srcObject\" :\n                 'mozSrcObject' in v ? \"mozSrcObject\" :\n                 'webkitSrcObject' in v ? \"webkitSrcObject\" : \"srcObject\";\n```\n\n\n### 4.3.3 音视频控制\n音视频控制主要分打开关闭摄像头，视频流绑定到video标签，其实这一节前面3.2 音视频相关API已经学习过了，这里直接给出代码。\n接着编辑sdk.js\n```shell\n/**\n * 启动摄像头\n */\nconst openCamera = () =\u003e {\n  return navigator.mediaDevices[GET_USER_MEDIA]({\n    audio: true,\n    video: true\n  });\n}\n\n/**\n * 关闭摄像头\n * @param {dom} video video节点\n */\nconst closeCamera = video =\u003e {\n  video[SRC_OBJECT].getTracks()[0].stop(); // audio\n  video[SRC_OBJECT].getTracks()[1].stop(); // video\n}\n\n/**\n * 视频流绑定到video节点展示\n * @param {dom} video video节点\n * @param {obj} stream 视频流\n */\nconst pushStreamToVideo = (video, stream) =\u003e {\n  console.log('视频流绑定到video节点展示', video, stream)\n  video[SRC_OBJECT] = stream;\n}\n```\n编辑main.js：\n```shell\n/**\n * dom获取\n */\nconst btnConnect = $('#connect'); // 连接dom\nconst btnLogout = $('#logout'); // 挂断dom\nconst domLocalVideo = $('#localVideo'); // 本地视频dom\n\n/**\n * 连接\n */\nbtnConnect.click(() =\u003e {\n  //启动摄像头\n  if (localStream == null) {\n    openCamera().then(stream =\u003e {\n      pushStreamToVideo(domLocalVideo[0], stream);\n    }).catch(e =\u003e alert(`getUserMedia() error: ${e.name}`));\n  }\n});\n\n/**\n * 挂断\n */\nbtnLogout.click(() =\u003e {\n  closeCamera(domLocalVideo[0]);\n})\n```\n\n\n测试一下摄像头功能，因为开启摄像头需要使用https服务，因此在前端项目根目录打开控制台命令，运行：\n```shell\nanywhere 5000\n```\n\n\n然后浏览器打开命令行提示里的端口号为5001的那个https协议的地址，例如：https://192.168.1.4:5001/\n这时候，可能也会提示您的连接不是私密连接，点击高级，最下面继续前往。\n点击连接按钮，允许访问摄像头，看摄像头是否正常打开，页面视频是否出现，然后点击断开，看摄像头是否关闭、画面是否消失。\n### 4.3.4 Offer/Answer模型\n从这节开始，就正式涉及到WebRTC相关API了，下面先写几个全局变量，用于保存一些公用数据：\n编辑sdk.js\n```shell\n// socket连接\nconst socket = io(THSConfig.signalServer);\n// 本地socket id\nlet socketId;\n// 房间 id\nlet roomId;\n// 对RTCPeerConnection连接进行缓存\nlet rtcPeerConnects = {};\n// 本地stream\nlet localStream = null;\n```\n\n\n（1）加入房间\n在开始Offer/Answer模型前，我们必须得至少有两个客户端才行。因此，我们先写一下，怎么控制房间。\n咱们先整理一下思路，我们先让甲创建一个房间，然后，这个房间里只有甲一个人，无法进行Offer/Answer。这时候乙在进入房间时，可以获取一下房间的人数，如果房间有人，那么乙就给房间里的每一个人发送Offer请求。房间里的甲监听到了刚进来乙的Offer后，给乙回复Answer。这样就建立起了Offer/Answer模型。\n编辑sdk.js\n```shell\n/**\n * 连接（给signal server 发送创建或者加入房间的消息）\n * @param {string} roomid 房间号\n */\nconst connect = roomid =\u003e {\n  console.log('创建或者加入房间', roomid)\n  socket.emit('createAndJoinRoom', {\n    room: roomid\n  });\n}\n\n/**\n * 监听signal server创建房间或者加入房间成功的消息，signal server会判断房间里是否有人\n */\nsocket.on('created', async data =\u003e {\n  // data: [id,room,peers]\n  console.log('created: ', data);\n  // 保存signal server给我分配的socketId\n  socketId = data.id;\n  // 保存创建房间或者加入房间的room id\n  roomId = data.room;\n  // 如果data.peers = []，说明房间里没有人，是创建房间，以下步骤则不会执行\n  // 如果data.peers != []，说明房间里有人，是加入房间，给返回的每一个peers，创建WebRtcPeerConnection并发送offer消息\n  for (let i = 0; i \u003c data.peers.length; i++) {\n    let otherSocketId = data.peers[i].id;\n    // 创建WebRtcPeerConnection // 注意：这个函数是下一个步骤写的。\n    let pc = getWebRTCConnect(otherSocketId);\n    // 创建offer\n    const offer = await pc.createOffer(THSConfig.offerOptions);\n    // 发送offer\n    onCreateOfferSuccess(pc, otherSocketId, offer);\n  }\n})\n\n\n/**\n * offer创建成功回调\n * @param {*} pc \n * @param {*} otherSocketId \n * @param {*} offer \n */\nfunction onCreateOfferSuccess(pc, otherSocketId, offer) {\n  console.log('createOffer: success ' + ' id:' + otherSocketId + ' offer: ', offer);\n  // 设置本地setLocalDescription 将自己的描述信息加入到PeerConnection中\n  pc.setLocalDescription(offer);\n  // 构建offer\n  const message = {\n    from: socketId,\n    to: otherSocketId,\n    room: roomId,\n    sdp: offer.sdp\n  };\n  console.log('发送offer消息', message)\n  // 发送offer消息\n  socket.emit('offer', message);\n}\n\n```\n\n\n前面，可以算是把Offer发出去了，可以回顾4.2.3 转发功能，信令服务器收到Offer后，会将其转发给房间里的每一个用户，然后，我们就需要写一个监听，当信令服务器转发过来Offer后，我们应该进行Answer：\n继续编辑sdk.js\n```shell\n/**\n * 监听signal server转发过来的offer消息，将对方的描述信息加入到PeerConnection中，然后构建answer\n */\nsocket.on('offer', data =\u003e {\n  // data:  [from,to,room,sdp]\n  console.log('收到offer: ', data);\n  // 获取RTCPeerConnection\n  const pc = getWebRTCConnect(data.from);\n  console.log('getWebRTCConnect: ', pc);\n  // 构建RTCSessionDescription参数\n  const rtcDescription = {\n    type: 'offer',\n    sdp: data.sdp\n  };\n\n  console.log('offer设置远端setRemoteDescription')\n  // 设置远端setRemoteDescription\n  pc.setRemoteDescription(new SessionDescription(rtcDescription));\n  console.log('setRemoteDescription: ', rtcDescription);\n\n  // createAnswer\n  pc.createAnswer(THSConfig.offerOptions)\n    .then(offer =\u003e onCreateAnswerSuccess(pc, data.from, offer))\n    .catch(error =\u003e onCreateAnswerError(error));\n})\n\n/**\n * answer创建成功回调\n * @param {*} pc \n * @param {*} otherSocketId \n * @param {*} offer \n */\nfunction onCreateAnswerSuccess(pc, otherSocketId, offer) {\n  console.log('createAnswer: success ' + ' id:' + otherSocketId + ' offer: ', offer);\n  // 设置本地setLocalDescription，将对方的描述信息加入到PeerConnection中\n  pc.setLocalDescription(offer);\n  // 构建answer信息\n  const message = {\n    from: socketId,\n    to: otherSocketId,\n    room: roomId,\n    sdp: offer.sdp\n  };\n  console.log('发送answer消息', message)\n  // 发送answer消息\n  socket.emit('answer', message);\n}\n\n/**\n * answer创建失败回调\n * @param {*} error \n */\nfunction onCreateAnswerError(error) {\n  console.log('createAnswer: fail error ' + error);\n}\n```\n\n\n现在，我们把Answer信息回复出去了，通过信令服务器会转发指定的用户（刚刚发来offer的用户），然后我们还要添加一个监听Answer的信息：\n继续编辑sdk.js\n```shell\n/**\n * 监听signal server转发过来的answer消息，将对方的描述信息加入到PeerConnection中\n */\nsocket.on('answer', data =\u003e {\n  // data:  [from,to,room,sdp]\n  console.log('收到answer: ', data);\n  // 获取RTCPeerConnection\n  const pc = getWebRTCConnect(data.from);\n\n  // 构建RTCSessionDescription参数\n  const rtcDescription = {\n    type: 'answer',\n    sdp: data.sdp\n  };\n\n  console.log('answer设置远端setRemoteDescription')\n  console.log('setRemoteDescription: ', rtcDescription);\n  //设置远端setRemoteDescription\n  pc.setRemoteDescription(new SessionDescription(rtcDescription));\n})\n```\n\n\n（2）获取RTCPeerConnection、移除RTCPeerConnection\n接上一步骤，其中涉及到一个getWebRTCConnect的方法，这节就写如何实现它，以及本地如何管理与他人的连接。\n继续编辑sdk.js\n```shell\n// 对RTCPeerConnection连接进行缓存\nlet rtcPeerConnects = {};  // 这是开始前设置的全局变量\n\n/**\n * 获取RTCPeerConnection\n * @param {string} otherSocketId 对方socketId\n */\nfunction getWebRTCConnect(otherSocketId) {\n  if (!otherSocketId) return;\n  // 查询全局中是否已经保存了连接\n  let pc = rtcPeerConnects[otherSocketId];\n  console.log('建立连接：', otherSocketId, pc)\n  if (typeof (pc) === 'undefined') { // 如果没有保存，就创建RTCPeerConnection\n    // 构建RTCPeerConnection\n    pc = new PeerConnection(THSConfig.iceServers); // PeerConnection是4.3.2定义的兼容处理\n\n    // 设置获取icecandidate信息回调 此处可暂时忽略，将在4.3.5讲解\n    pc.onicecandidate = e =\u003e onIceCandidate(pc, otherSocketId, e);\n    // 设置获取对端stream数据回调-track方式 此处可暂时忽略，将在4.3.5讲解\n    pc.ontrack = e =\u003e {\n      console.log('我接到数据流了！！', pc, otherSocketId, e)\n      onTrack(pc, otherSocketId, e);\n    }\n    // 设置获取对端stream数据回调 此处可暂时忽略，将在4.3.5讲解\n    pc.onremovestream = e =\u003e onRemoveStream(pc, otherSocketId, e);\n    // peer设置本地流 此处可暂时忽略，将在4.3.5讲解\n    if (localStream != null) {\n      localStream.getTracks().forEach(track =\u003e {\n        pc.addTrack(track, localStream);\n      });\n    }\n\n    // 缓存peer连接\n    rtcPeerConnects[otherSocketId] = pc;\n  }\n  return pc;\n}\n\n/**\n * 移除RTCPeerConnection连接缓存\n * @param {string} otherSocketId 对方socketId\n */\nfunction removeRtcConnect(otherSocketId) {\n  delete rtcPeerConnects[otherSocketId];\n}\n```\n\n\n### 4.3.5 ICE连接/接收音视频流\nOffer/Answer模型让两个客户端互相建立了签订了合同，建立了信任的合作伙伴关系，接下来可以开始进行交易了（传输音视频数据）。在交易前，我们要互相知道对方真实的交易地址和银行账号（允许主机直连的地址，详细可回顾1.4ICE协议），我给你发货，你给我打钱。\n通常，在第一步乙的Offer发出后，乙客户端就开始通过ICE获取自己的地址（通过ICE协议可以了解，这个地址可能是自己的IP地址），只要等甲方同意（设置远程描述完成，这时候可能还未回复Answer），甲方就可以接收到乙客户端的音视频流了。同理，甲方回复的Answer之后，只要乙客户端同意，乙客户端也就能收到甲方的音视频流了。至此，双方都收到对方的视频流了，视频通话建立。\n回顾上一小节 4.3.4 (2) 获取RTCPeerConnection中的一段代码：\n```shell\n// 构建RTCPeerConnection\npc = new PeerConnection(THSConfig.iceServers); // PeerConnection是4.3.2定义的兼容处理\n\n// 1. 设置获取icecandidate信息回调\npc.onicecandidate = e =\u003e onIceCandidate(pc, otherSocketId, e);\n// 2. 设置获取对端stream数据回调-track方式  还有种方式是onaddstream，但这种方式已经不推荐使用了。\npc.ontrack = e =\u003e {\n  console.log('我接到数据流了！！', pc, otherSocketId, e)\n  onTrack(pc, otherSocketId, e);\n}\n// 3. 设置获取对端stream数据回调\npc.onremovestream = e =\u003e onRemoveStream(pc, otherSocketId, e);\n// 4. peer设置本地流\nif (localStream != null) {\n  localStream.getTracks().forEach(track =\u003e {\n    pc.addTrack(track, localStream);\n  });\n}\n```\n\n\n实例pc实际就是window.RTCPeerConnection对象，这个对象有几个回调方法在3.3.1节已经讲过了。\n（1）onicecandidate\n当ICE协商完成后，我们将协商结果发送至信令服务器，让其转发给指定的客户端。\n继续编辑sdk.js\n```shell\n/**\n * RTCPeerConnection 事件回调，获取icecandidate信息回调\n * @param {*} pc \n * @param {*} otherSocketId \n * @param {*} event \n */\nfunction onIceCandidate(pc, otherSocketId, event) {\n  console.log('onIceCandidate to ' + otherSocketId + ' candidate: ', event);\n  if (event.candidate !== null) {\n    // 构建信息 [from,to,room,candidate[sdpMid,sdpMLineIndex,sdp]]\n    const message = {\n      from: socketId,\n      to: otherSocketId,\n      room: roomId,\n      candidate: {\n        sdpMid: event.candidate.sdpMid,\n        sdpMLineIndex: event.candidate.sdpMLineIndex,\n        sdp: event.candidate.candidate\n      }\n    };\n    console.log('向信令服务器发送candidate', message)\n    // 向信令服务器发送candidate\n    socket.emit('candidate', message);\n  }\n}\n```\n\n\n远程客户端收到candidate后，添加candidate后即可接收到本机的音视频流：\n继续编辑sdk.js，添加监听事件：\n```shell\n/**\n * 监听signal server转发过来的candidate消息\n */\nsocket.on('candidate', data =\u003e {\n  // data:  [from,to,room,candidate[sdpMid,sdpMLineIndex,sdp]]\n  console.log('candidate: ', data);\n  const iceData = data.candidate;\n  \n  // 获取RTCPeerConnection\n  const pc = getWebRTCConnect(data.from);\n  \n  const rtcIceCandidate = new RTCIceCandidate({\n    candidate: iceData.sdp,\n    sdpMid: iceData.sdpMid,\n    sdpMLineIndex: iceData.sdpMLineIndex\n  });\n\n  console.log('添加对端Candidate')\n  // 添加对端Candidate\n  pc.addIceCandidate(rtcIceCandidate);\n})\n```\n\n\n（2）ontrack\n当监听到对方传递过来时音视频流后，动态创建一个video标签，显示接收到的音视频流数据。\n继续编辑sdk.js\n```shell\n/**\n * 获取对端stream数据回调-ontrack模式\n * @param {*} pc \n * @param {*} otherSocketId \n * @param {*} event \n */\n function onTrack(pc, otherSocketId, event) {\n  console.log('onTrack from: ' + otherSocketId);\n  let otherVideoDom = $('#' + otherSocketId);\n  if (otherVideoDom.length === 0) { // TODO 未知原因：会两次onTrack，就会导致建立两次dom\n    const video = document.createElement('video');\n    video.id = otherSocketId;\n    video.autoplay = 'autoplay';\n    video.muted = 'muted';\n    video.style.width = 200;\n    video.style.height = 200;\n    video.style.marginRight = 5;\n    $('#remoteDiv').append(video);\n  }\n  $('#' + otherSocketId)[0][SRC_OBJECT] = event.streams[0];\n}\n```\n\n\n（3）onremovestream\n监听对方停止传输视频流的时候，我方进行相应处理：\n继续编辑sdk.js\n```shell\n/**\n * onRemoveStream回调\n * @param {*} pc \n * @param {*} otherSocketId \n * @param {*} event \n */\nfunction onRemoveStream(pc, otherSocketId, event) {\n  console.log('onRemoveStream from: ' + otherSocketId);\n  // peer关闭\n  getWebRTCConnect(otherSocketId).close;\n  // 删除peer对象\n  removeRtcConnect(otherSocketId)\n  // 移除video\n  $('#' + otherSocketId).remove();\n}\n```\n\n\n（4）添加本地音视频流\n当我方开启摄像头后，全局变量localStream就不为null，我们需要往对方塞过去我们的的音视频数据，通过addTrack方法。这样，在对方同意（添加我方描述）后，就可以获取到我方的音视频数据了。\n### 4.3.6 完善逻辑\n前面的内容基本把整个逻辑讲完了，但是你现在启动项目运行，是不是还是只能看到自己，后面的步骤根本没有执行？\n因为前面的我们只打开了摄像头，还没有对接后续操作。\n现在编辑main.js，修改一下之前的代码：\n```shell\n/**\n * dom获取\n */\nconst btnConnect = $('#connect'); // 连接dom\nconst btnLogout = $('#logout'); // 挂断dom\nconst domLocalVideo = $('#localVideo'); // 本地视频dom\nconst domRoom = $('#room'); // 获取房间号输入框dom\n\n/**\n * 连接\n */\nbtnConnect.click(() =\u003e {\n  const roomid = domRoom.val(); // 获取用户输入的房间号\n  if (!roomid) {\n    alert('房间号不能为空');\n    return;\n  };\n  //启动摄像头\n  if (localStream == null) {\n    openCamera().then(stream =\u003e {\n      localStream = stream; // 保存本地视频到全局变量\n      pushStreamToVideo(domLocalVideo[0], stream);\n      connect(roomid); // 成功打开摄像头后，开始创建或者加入输入的房间号\n    }).catch(e =\u003e alert(`getUserMedia() error: ${e.name}`));\n  }\n});\n\n/**\n * 挂断\n */\nbtnLogout.click(() =\u003e {\n  closeCamera(domLocalVideo[0]);\n  logout(roomId); // 退出房间\n  \n  //移除远程视频\n  $('#remoteDiv').empty();\n})\n\n```\n\n\n编辑sdk.js，添加logout()方法，监听他人退出房间socket.on('exit')：\n```shell\n/**\n * 挂断（退出房间）\n * @param {string} roomid 房间号\n */\nconst logout = roomid =\u003e {\n  // 构建数据\n  const data = {\n    from: socketId, // 全局变量，我方的socketId\n    room: roomid, // 全局变量，当前房间号\n  };\n  // 向信令服务器发出退出信号，让其转发给房间里的其他用户\n  socket.emit('exit', data);\n  // 数据重置\n  socketId = '';\n  roomId = '';\n  // 关闭每个peer连接\n  for (let i in rtcPeerConnects) {\n    let pc = rtcPeerConnects[i];\n    pc.close();\n    pc = null;\n  }\n  // 重置RTCPeerConnection连接\n  rtcPeerConnects = {};\n  // 移除本地视频\n  localStream = null;\n}\n\n\n\n/**\n * 监听signal server转发过来的exit消息，和退出房间的客户端断开连接\n */\nsocket.on('exit', data =\u003e {\n  // data: [from,room]\n  console.log('exit: ', data);\n  // 获取RTCPeerConnection\n  const pc = rtcPeerConnects[data.from];\n  if (typeof (pc) == 'undefined') {\n    return;\n  } else {\n    // RTCPeerConnection关闭\n    getWebRTCConnect(data.from).close;\n\n    // 删除peer对象\n    removeRtcConnect(data.from)\n    console.log($('#' + data.from))\n    // 移除video\n    $('#' + data.from).remove();\n  }\n})\n```\n\n\n### 4.3.7 完整代码\n[https://github.com/DOUBLE-Baller/momo/](https://github.com/DOUBLE-Baller/momo/)\n# 五、总结\n现在，我们已经基本入门WebRTC了。可能前3章的协议、服务器、API的学习让我们感觉很枯燥，知识很杂乱。我想，大家通过第四章的实战开发，将之前的知识点串通起来，是不是有一点感觉了。其实前两章在现在看来，是可以不必着重学习的。没有这些协议和服务器的支持，不懂他们的连接原理，后面的学习应该会更加疑惑吧。\n前面的实战开发，是一个很简单的Web端的例子，没有涉及到安卓、iOS端如何进行WebRTC通信，如果需要继续深入学习，下一步可以往移动端WebRTC上学习，比如移动端打开摄像头都和Web不同。\n如果暂时没有深入WebRTC的学习话，可以基于这个实战项目进行横向的扩展。这个实战项目虽然看起来很简单，但是你可以给它加出很多功能来，会看起来很高大尚！比如：\n\n- 在线电话：咱们现在只是通过房间号进行连接，我们可以设置一个登陆页面，将用户的id作为房间号，每个用户登陆后直接创建一个房间。我们想要给某个用户打音视频电话的话，我们可以加入他的房间，对方也能检测到房间是否有人进来，这样对方可以做成收到来电了，对方接听后，我们就进行WebRTC连接，实现拨打电话的功能。\n- 视频会议：我们开发好注册登录功能，创建会议就相当于创建一个房间，只不过这个房间号是由我们系统来自动分配，别人登录后，通过该房间号就可以加入，即可实现视频会议功能。当然还可以扩展分享屏幕、白板等功能。\n\n本次WebRTC入门学习到此结束了，非常感谢您耐心地看完本篇长文。若有描述不对的地方，欢迎指出！\n对以下文章、项目和视频的作者们，表示非常感谢！感谢您们辛苦的成果！\n参考文章、文献、规范、项目、视频：\n\n[WebRTC协议介绍：] [https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols)\n\n[WebRTC中文社区：][https://webrtc.org.cn/](https://webrtc.org.cn/)\n\n[RTC开发者社区:] [https://rtcdeveloper.com/](https://rtcdeveloper.com/)\n\n[又拍云WebRTC实时通信服务实践:][https://segmentfault.com/a/1190000010339671](https://segmentfault.com/a/1190000010339671)\n\n[P2P通信原理:][https://zhuanlan.zhihu.com/p/26796476](https://zhuanlan.zhihu.com/p/26796476)\n\n[STUN协议详细介绍:][https://zhuanlan.zhihu.com/p/26797664](https://zhuanlan.zhihu.com/p/26797664)\n\n[TURN协议详细介绍：][https://zhuanlan.zhihu.com/p/26797422](https://zhuanlan.zhihu.com/p/26797422)\n\n[ICE协议详细介绍][https://zhuanlan.zhihu.com/p/26857913](https://zhuanlan.zhihu.com/p/26857913)\n\n[WebRTC PeerConnection建立连接过程] ：[https://aggresss.blog.csdn.net/article/details/106832965](https://aggresss.blog.csdn.net/article/details/106832965)\n\n[STUN/TURN服务器（C语言)]：[https://github.com/coturn/coturn](https://github.com/coturn/coturn)\n\n[STUN服务器（node）][https://github.com/enobufs/stun](https://github.com/enobufs/stun)\n\n[Build Zoom Clone Video Chat Web App in Node.js Express and [Socket.io](http://socket.io/) Using WebRTC and PeerJS Library：][https://www.youtube.com/watch?v=MX_r3Wm_BLE](https://www.youtube.com/watch?v=MX_r3Wm_BLE)\n\n[https://codingshiksha.com/javascript/build-zoom-clone-video-chat-web-app-in-node-js-express-and-socket-io-using-webrtc-and-peerjs-library/](https://codingshiksha.com/javascript/build-zoom-clone-video-chat-web-app-in-node-js-express-and-socket-io-using-webrtc-and-peerjs-library/)\n\n[Build Video Chat Web App From Scratch in 40 mins:] [https://www.youtube.com/watch?v=KLCcCTFivhM](https://www.youtube.com/watch?v=KLCcCTFivhM)\n\n[coturn服务器搭建:][https://www.jianshu.com/p/915eab39476d](https://www.jianshu.com/p/915eab39476d)\n\n[coturn服务器搭建]：[https://meetrix.io/blog/webrtc/coturn/installation.html](https://meetrix.io/blog/webrtc/coturn/installation.html)\n\n[coturn服务器搭建]：[https://ourcodeworld.com/articles/read/1175/how-to-create-and-configure-your-own-stun-turn-server-with-coturn-in-ubuntu-18-04](https://ourcodeworld.com/articles/read/1175/how-to-create-and-configure-your-own-stun-turn-server-with-coturn-in-ubuntu-18-04)\n\n[WebRtcRoomServer（信令服务器node）：][https://github.com/qdgx/WebRtcRoomServer](https://github.com/qdgx/WebRtcRoomServer)\n\n[MDN Web Docs:][https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API)\n\n[webRTC API之RTCPeerConnection:] [https://www.cnblogs.com/suRimn/p/11314914.html](https://www.cnblogs.com/suRimn/p/11314914.html)\n\n[RTP与RTCP协议介绍：] [https://blog.51cto.com/zhangjunhd/25481](https://blog.51cto.com/zhangjunhd/25481)\n\n  \n[中文版(一.介绍）：](http://www.iwebrtc.com/blog/web-real-time-communication-webrtc-media-transport-and-use-of-rtp-01/)  \n\n[中文版(二.基本原理）：](http://www.iwebrtc.com/blog/web-real-time-communication-webrtc-media-transport-and-use-of-rtp-02/)  \n \n[中文版(三.术语）：](http://www.iwebrtc.com/blog/web-real-time-communication-webrtc-media-transport-and-use-of-rtp-03/)  \n\n[中文版(四.核心协议）：](http://www.iwebrtc.com/blog/web-real-time-communication-webrtc-media-transport-and-use-of-rtp-04/)  \n\n[中文版(五.webrtc所使用RTP扩展）:](http://www.iwebrtc.com/blog/web-real-time-communication-webrtc-media-transport-and-use-of-rtp-05/)  \n\n[中文版(六.增强传输可靠性）：](http://www.iwebrtc.com/blog/web-real-time-communication-webrtc-media-transport-and-use-of-rtp-06/)  \n\n[中文版(七.速率控制和媒体适配）：](http://www.iwebrtc.com/blog/web-real-time-communication-webrtc-media-transport-and-use-of-rtp-07/)  \n\n[中文版(八.性能监控）：](http://www.iwebrtc.com/blog/web-real-time-communication-webrtc-media-transport-and-use-of-rtp-08/)  \n\n[中文版(九.未来扩展）：](http://www.iwebrtc.com/blog/web-real-time-communication-webrtc-media-transport-and-use-of-rtp-09/)  \n\n[中文版(十.信号考虑）：](http://www.iwebrtc.com/blog/web-real-time-communication-webrtc-media-transport-and-use-of-rtp-10/)  \n\n[中文版(十一.WebRTC API的考虑）：](http://www.iwebrtc.com/blog/web-real-time-communication-webrtc-media-transport-and-use-of-rtp-11/)  \n\n[中文版(十二.RTP实现）：](http://www.iwebrtc.com/blog/web-real-time-communication-webrtc-media-transport-and-use-of-rtp-12/)  \n\n[中文版(十三，遗留问题）：](http://www.iwebrtc.com/blog/web-real-time-communication-webrtc-media-transport-and-use-of-rtp-13/)  \n\n[中文版(十五，安全考虑）：](http://www.iwebrtc.com/blog/web-real-time-communication-webrtc-media-transport-and-use-of-rtp-15/)  \n\n[中文版(十六，致谢和参考资料）：](http://www.iwebrtc.com/blog/web-real-time-communication-webrtc-media-transport-and-use-of-rtp-15-2/)    \n\n[中文版(附录A：支持的RTP拓扑图）：](http://www.iwebrtc.com/blog/web-real-time-communication-webrtc-media-transport-and-use-of-rtpappendix-a/) \n\n[中文版(附录A1：点对点）：](http://www.iwebrtc.com/blog/web-real-time-communication-webrtc-media-transport-and-use-of-rtp-appendix-a1/)  \n\n[中文版(附录A2：单点多播）：](http://www.iwebrtc.com/blog/web-real-time-communication-webrtc-media-transport-and-use-of-rtp-appendix-a2/)  \n\n **WebRTC官方源码样例（不含移动端）**：http://github.com/webrtc/samples （看再多理论不如抠一遍源码）  \n **WebRTC在线演示效果**：[http://webrtc.github.io/samples](http://webrtc.github.io/samples) （可以清楚的看到每个接口是怎样被调用的）   \n*  ### 二、初学者入门\n**官方推荐的入门文章**：http://html5rocks.com/en/tutorials/webrtc/basics（个人感觉讲的有点绕，英文不好估计很难理解）  \n**使用WebRTC搭建前端视频聊天室——入门篇**：[http://chinawebrtc.org/?p=271](http://chinawebrtc.org/?p=271)（推荐这篇中文的入门，讲的很细，它的三篇后续教程也很值得一看）  \n**WebRTC体系结构**：[http://chinawebrtc.org/?p=338](http://chinawebrtc.org/?p=338)（对整体的把握是很重要的）\n通过WebRTC实现实时视频通信：[http://chinawebrtc.org/?p=462](http://chinawebrtc.org/?p=462) （不错的教程）\n\n**[js]** http://www.webrtc.org/native-code/development  \n**[android]** http://www.webrtc.org/native-code/android  \n**[iOS]** http://www.webrtc.org/native-code/ios  \n**看看大牛的编译实践**：  \nhttp://chinawebrtc.org/?p=339  \nhttp://chinawebrtc.org/?p=340  \nhttp://chinawebrtc.org/?p=260  \nhttp://chinawebrtc.org/?p=292  \nhttp://chinawebrtc.org/?p=391  \n`使用Tokbox瞬间实现在线视频`：[https://dashboard.tokbox.com/quickstart#1](https://dashboard.tokbox.com/quickstart#1)（需要注册申请一个sdk的key生成token,之后就很方便了）国外已经有视频教程了：[http://www.pluralsight.com/courses/webrtc-fundamentals](https://dashboard.tokbox.com/quickstart#1)（可试看，后需会员）  \nWebRTC在`android端`的教程：https://tech.appear.in/2015/05/25/Introduction-to-WebRTC-on-Android/    \n`WebRTC在iOS端的教程`：  https://tech.appear.in/2015/05/25/Getting-started-with-WebRTC-on-iOS/    \n`Play With WebRTC`：http://chinawebrtc.org/?p=530  \n手把手教程：  \n[http://io2014codelabs.appspot.com/static/codelabs/webrtc-file-sharing/#1](http://io2014codelabs.appspot.com/static/codelabs/webrtc-file-sharing/#)  \nhttps://bitbucket.org/webrtc/codelab  \n*  ### 三、高级教程\n**getUserMedia解释**  http://www.html5rocks.com/en/tutorials/getusermedia/intro/  \n信令机制的解释：http://www.html5rocks.com/en/tutorials/webrtc/infrastructure/  \n使用WebRTC搭建前端视频聊天室——信令篇：  http://chinawebrtc.org/?p=260  \n使用WebRTC搭建前端视频聊天室——点对点通信篇：  http://chinawebrtc.org/?p=273  \n使用WebRTC搭建前端视频聊天室——数据通道篇：  http://chinawebrtc.org/?p=274  \nWebRTC音视频引擎研究(1)–整体架构分析：  http://chinawebrtc.org/?p=355   \nWebRTC音视频引擎研究(2)–VoiceEngine音频编解码器数据结构以及参数设置：  http://chinawebrtc.org/?p=356  \nWebRTC Native APIs[翻译]：  http://chinawebrtc.org/?p=357  \nWebRTC源码分析1——视频显示:  http://chinawebrtc.org/?p=360  \nWebRTC源码分析2——图像缩放与颜色空间转换：  http://chinawebrtc.org/?p=365  \nWebRTC源码分析3——jpeg编解码：  http://chinawebrtc.org/?p=366  \nWebRTC源码分析4——AVI文件读写：  http://chinawebrtc.org/?p=371  \nWebRTC源码分析5——VoiceEngine代码解析：  http://chinawebrtc.org/?p=380  \nWebRTC源码分析6——音频模块结构分析：  http://chinawebrtc.org/?p=379  \nWebRTC源码分析6——AudioProcessing的使用：  http://chinawebrtc.org/?p=381  \nwebrtc 的回声抵消(aec、aecm)算法简介：  http://chinawebrtc.org/?p=382  \n建立一个WebRtc的Android客户端：  http://chinawebrtc.org/?p=260  \nWebRtc常见问题集锦：  http://chinawebrtc.org/?p=327\n*  ### 四、源码或示例 \n`这里面应该是最全最详细的了`：https://www.webrtc-experiment.com/  \n`这里面也有不少`：http://simpl.info/  \ngetUserMedia:\nASCII码的视频（getUserMedia + Canvas + ASCII conversion）:http://idevelop.ro/ascii-camera/  \n各种酷炫效果，还能这么玩居然（getUserMedia + WebGL）：http://webcamtoy.com  \nsvg滤镜https://rawgit.com/SenorBlanco/moggy/master/filterbooth.html  \nWebGl实现人脸面具：http://auduno.github.io/clmtrackr/examples/facedeform.html  \n用脸玩太空大战：http://shinydemos.com/facekat  \n一个录音显示声纹波动的demo:http://webaudiodemos.appspot.com/AudioRecorder  \n音频Demo大集合：http://webaudiodemos.appspot.com/  \ngUM + WebGL实现录音室：http://lab.aerotwist.com/webgl/audio-room  \nRTCDataChannel\n一个简单的例子：http://simpl.info/dc  \n文件分享：https://sharefest.me/  \n一个js类库：http://ozan.io/p/  \n实时通信的TogetherJS 类库：https://togetherjs.com/  \n用WebRTC实现BitTorrent:https://github.com/feross/webtorrent  \nRTCPeerConnection\n一个简单的例子：http://simpl.info/pc  \n视频聊天示例：https://apprtc.appspot.com/，源码https://code.google.com/p/webrtc/source/browse/#svn%2Ftrunk%2Fsamples%2Fjs%2Fapprtc  \n视频聊天示例：https://appear.in/，开发者api:https://developer.appear.in/  \nhttps://bistri.com/  \n视频聊天示例：https://talky.io/,源码：https://github.com/henrikjoreteg/SimpleWebRTC  \n视频聊天示例：https://tawk.com/  \n通过github视频聊天：https://gittogether.com/  \n视频聊天示例：http://codassium.com/  \n视屏聊天示例：https://vline.com/  \n视频聊天示例：https://www.lytespark.com/  \n视频聊天示例：https://vidtok.com/  \n视频聊天示例：http://www.easyrtc.com/，源码https://github.com/priologic/easyrtc  \n视频聊天示例（印度的）：https://www.miljul.in/  \nhttp://chotis2.dit.upm.es/（可fork on GitHub）  \nhttps://janus.conf.meetecho.com/(可fork on GitHub)  \ngoToMeeting在线版：https://free.gotomeeting.com/  \n婴儿监视器：https://webrtchacks.com/baby-motion-detector/  \n电话通讯：http://zingaya.com/  \n*  ### 五、一些api及类库 \n官方的PeerConnection的api：http://www.webrtc.org/blog/api-description  \n官方其它的一些的api：http://www.webrtc.org/native-code/native-apis  \nlibjingle的文档介绍https://developers.google.com/talk/libjingle/developer_guide?csw=1  \ngetUserMedia.js：https://github.com/addyosmani/getUserMedia.js  \nadapter.js：https://github.com/webrtc/adapter/blob/master/adapter.js  \nWebRTC的js类库里有些什么：https://webrtchacks.com/whats-in-a-webrtc-javascript-library/  \nWeb Audio API：http://webaudio.github.io/web-audio-api/  \nThe PeerJS library：简化了WebRTC传输数据的过程http://peerjs.com/  \n有关浏览器通话的js类库：http://phono.com/  \n封装SIP协议的js类库：客户端，https://code.google.com/p/sipml5/；http://jssip.net/  \n面部识别的js类库：https://github.com/auduno/clmtrackr  \n头部轨迹识别的js类库：https://github.com/auduno/headtrackr/；demo,http://simpl.info/headtrackr/  \nhttp://rtc.io/  \n开发WebRTC的工具列表（不能更全）：https://webrtchacks.com/vendor-directory/  \n*  ### 六、一些书籍 \nWebRTC-APIs and RTCWEB Protocols of the HTML5 Real-Time Web, Third Edition：http://webrtcbook.com/  \nReal-Time Communication with WebRTC by Salvatore Loreto \u0026 Simon Pietro Romano：https://bloggeek.me/book-webrtc-salvatore-simon/  \nGetting Started with WebRTC：https://www.packtpub.com/web-development/getting-started-webrtc    \n*  ### 七、标准及协议 \nWebRTC工作小组：http://www.w3.org/2011/04/webrtc/  \nw3c规定的WebRTC协议1.0http://www.w3.org/TR/webrtc/  \n媒体捕捉及媒体流协议：http://www.w3.org/TR/mediacapture-streams/  \nIETF协议http://datatracker.ietf.org/wg/rtcweb/documents/  \n各大浏览器是否支持：http://iswebrtcreadyyet.com/  \n*  ### 八、其它 \n国外Google group：https://groups.google.com/forum/?fromgroups#!forum/discuss-webrtc  \n国内china WebRTC社区：http://chinawebrtc.org/  \n*  ### 九、WebRTC 1.0: Real-time Communication Between Browsers 协议文档中文版汇总\n第一篇是描述整个文档的状态和概要：http://www.iwebrtc.com/blog/webrtc-1-0-real-time-communication-between-browsers-1/  \n第二篇是整个文档的介绍和术语：http://www.iwebrtc.com/blog/webrtc-1-0-real-time-communication-between-browsers-2/  \n第三篇从原文的4. Network Stream API开始，主要描述Network API和MediaStream接口（正式的内容从第三篇开始）:http://www.iwebrtc.com/blog/webrtc-1-0-real-time-communication-between-browsers-3/  \n第四篇从原文的4.3 AudioMediaStreamTrack开始，主要描述AudioMediaStreamTrack类：http://www.iwebrtc.com/blog/webrtc-1-0-real-time-communication-between-browsers-4/  \n第五篇从原文的5.Peer-to-peer connections开始，主要描述RTCPeerConnection类。原文的第五节是整个webrtc协议的重点，RTCPeerConnection是webrtc实现的核心功能。：http://www.iwebrtc.com/blog/webrtc-1-0-real-time-communication-between-browsers-5/  \n第六篇从原文的5.1 RTCPeerConnection开始，重点描述RTCPeerConnection的属性和方法。：http://www.iwebrtc.com/blog/webrtc-1-0-real-time-communication-between-browsers-6/  \n第七篇从原文的5.1.6 RTCPeerState Enum开始，仍然是原文的第5节的继续。：http://www.iwebrtc.com/blog/webrtc-1-0-real-time-communication-between-browsers-7/  \n第八篇从原文的5.1.9 RTCIceServer 类型开始，讲解和ICE Server交互相关的内容。：http://www.iwebrtc.com/blog/webrtc-1-0-real-time-communication-between-browsers-8/  \n第九篇从6. IANA Registrations开始，主要描述IANA Registrations相关的标准约束。：http://www.iwebrtc.com/blog/webrtc-1-0-real-time-communication-between-browsers-9/  \n第十篇从原文的7. Simple Example开始，展示了一个简单的javascript的例子。：http://www.iwebrtc.com/blog/webrtc-1-0-real-time-communication-between-browsers-10/  \n第十一篇从原文的9. Call Flow Browser to Browser开始，描述浏览器到浏览器的呼叫建立的流程图。（此处是重点内容）：http://www.iwebrtc.com/blog/webrtc-1-0-real-time-communication-between-browsers-11/  \n第十二篇从原文的10. Call Flow Browser to MCU开始，描述浏览器到MCU呼叫建立的流程图。：http://www.iwebrtc.com/blog/webrtc-1-0-real-time-communication-between-browsers-12/  \n第十三篇从原文11. Peer-to-peer Data API开始，描述创建点到点的数据传输通道的API。（这个很有用，可以用来传输语音和视频之外的数据，比如白板、共享桌面等）：http://www.iwebrtc.com/blog/webrtc-1-0-real-time-communication-between-browsers-13/  \n第十四篇从原文11.1.1 Attributes开始，接前一篇，继续描述DataChannel的属性等。：http://www.iwebrtc.com/blog/webrtc-1-0-real-time-communication-between-browsers-14/  \n第十五篇从原文12. Garbage collection开始，垃圾搜集策略以及事件汇总。：http://www.iwebrtc.com/blog/webrtc-1-0-real-time-communication-between-browsers-15/  \n第十六篇从原文15. Security Considerations开始，描述安全机制、修改日志、致谢、参考（基本上这一篇没怎么翻译，大部分可以直接无视。修改日志可以扫一眼，参考内容可以浏览一下）。：http://www.iwebrtc.com/blog/webrtc-1-0-real-time-communication-between-browsers-16/","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fqmcloud%2Fwebrtc_im","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fqmcloud%2Fwebrtc_im","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fqmcloud%2Fwebrtc_im/lists"}