相关API简介

在前面的章节中,已经对WebRTC相关的重要知识点进行了介绍,包括涉及的网络协议、会话描述协议、如何进行网络穿透等,剩下的就是WebRTC的API了。

WebRTC通信相关的API非常多,主要完成了如下功能:

  1. 信令交换
  2. 通信候选地址交换
  3. 音视频采集
  4. 音视频发送、接收

相关API太多,为避免篇幅过长,文中部分采用了伪代码进行讲解。详细代码参考文章末尾,也可以在笔者的Github上找到,有问题欢迎留言交流。

信令交换

信令交换是WebRTC通信中的关键环节,交换的信息包括编解码器、网络协议、候选地址等。对于如何进行信令交换,WebRTC并没有明确说明,而是交给应用自己来决定,比如可以采用WebSocket。

发送方伪代码如下:

  1. const pc = new RTCPeerConnection(iceConfig);
  2. const offer = await pc.createOffer();
  3. await pc.setLocalDescription(offer);
  4. sendToPeerViaSignalingServer(SIGNALING_OFFER, offer); // 发送方发送信令消息

接收方伪代码如下:

  1. const pc = new RTCPeerConnection(iceConfig);
  2. await pc.setRemoteDescription(offer);
  3. const answer = await pc.createAnswer();
  4. await pc.setLocalDescription(answer);
  5. sendToPeerViaSignalingServer(SIGNALING_ANSWER, answer); // 接收方发送信令消息

候选地址交换服务

当本地设置了会话描述信息,并添加了媒体流的情况下,ICE框架就会开始收集候选地址。两边收集到候选地址后,需要交换候选地址,并从中知道合适的候选地址对。

候选地址的交换,同样采用前面提到的信令服务,伪代码如下:

  1. // 设置本地会话描述信息
  2. const localPeer = new RTCPeerConnection(iceConfig);
  3. const offer = await pc.createOffer();
  4. await localPeer.setLocalDescription(offer);
  5. // 本地采集音视频
  6. const localVideo = document.getElementById('local-video');
  7. const mediaStream = await navigator.mediaDevices.getUserMedia({
  8. video: true,
  9. audio: true
  10. });
  11. localVideo.srcObject = mediaStream;
  12. // 添加音视频流
  13. mediaStream.getTracks().forEach(track => {
  14. localPeer.addTrack(track, mediaStream);
  15. });
  16. // 交换候选地址
  17. localPeer.onicecandidate = function(evt) {
  18. if (evt.candidate) {
  19. sendToPeerViaSignalingServer(SIGNALING_CANDIDATE, evt.candidate);
  20. }
  21. }

音视频采集

可以使用浏览器提供的getUserMedia接口,采集本地的音视频。

  1. const localVideo = document.getElementById('local-video');
  2. const mediaStream = await navigator.mediaDevices.getUserMedia({
  3. video: true,
  4. audio: true
  5. });
  6. localVideo.srcObject = mediaStream;

音视频发送、接收

将采集到的音视频轨道,通过addTrack进行添加,发送给远端。

  1. mediaStream.getTracks().forEach(track => {
  2. localPeer.addTrack(track, mediaStream);
  3. });

远端可以通过监听ontrack来监听音视频的到达,并进行播放。

  1. remotePeer.ontrack = function(evt) {
  2. const remoteVideo = document.getElementById('remote-video');
  3. remoteVideo.srcObject = evt.streams[0];
  4. }

完整代码

包含两部分:客户端代码、服务端代码。

1、客户端代码

  1. const socket = io.connect('http://localhost:3000');
  2. const CLIENT_RTC_EVENT = 'CLIENT_RTC_EVENT';
  3. const SERVER_RTC_EVENT = 'SERVER_RTC_EVENT';
  4. const CLIENT_USER_EVENT = 'CLIENT_USER_EVENT';
  5. const SERVER_USER_EVENT = 'SERVER_USER_EVENT';
  6. const CLIENT_USER_EVENT_LOGIN = 'CLIENT_USER_EVENT_LOGIN'; // 登录
  7. const SERVER_USER_EVENT_UPDATE_USERS = 'SERVER_USER_EVENT_UPDATE_USERS';
  8. const SIGNALING_OFFER = 'SIGNALING_OFFER';
  9. const SIGNALING_ANSWER = 'SIGNALING_ANSWER';
  10. const SIGNALING_CANDIDATE = 'SIGNALING_CANDIDATE';
  11. let remoteUser = ''; // 远端用户
  12. let localUser = ''; // 本地登录用户
  13. function log(msg) {
  14. console.log(`[client] ${msg}`);
  15. }
  16. socket.on('connect', function() {
  17. log('ws connect.');
  18. });
  19. socket.on('connect_error', function() {
  20. log('ws connect_error.');
  21. });
  22. socket.on('error', function(errorMessage) {
  23. log('ws error, ' + errorMessage);
  24. });
  25. socket.on(SERVER_USER_EVENT, function(msg) {
  26. const type = msg.type;
  27. const payload = msg.payload;
  28. switch(type) {
  29. case SERVER_USER_EVENT_UPDATE_USERS:
  30. updateUserList(payload);
  31. break;
  32. }
  33. log(`[${SERVER_USER_EVENT}] [${type}], ${JSON.stringify(msg)}`);
  34. });
  35. socket.on(SERVER_RTC_EVENT, function(msg) {
  36. const {type} = msg;
  37. switch(type) {
  38. case SIGNALING_OFFER:
  39. handleReceiveOffer(msg);
  40. break;
  41. case SIGNALING_ANSWER:
  42. handleReceiveAnswer(msg);
  43. break;
  44. case SIGNALING_CANDIDATE:
  45. handleReceiveCandidate(msg);
  46. break;
  47. }
  48. });
  49. async function handleReceiveOffer(msg) {
  50. log(`receive remote description from ${msg.payload.from}`);
  51. // 设置远端描述
  52. const remoteDescription = new RTCSessionDescription(msg.payload.sdp);
  53. remoteUser = msg.payload.from;
  54. createPeerConnection();
  55. await pc.setRemoteDescription(remoteDescription); // TODO 错误处理
  56. // 本地音视频采集
  57. const localVideo = document.getElementById('local-video');
  58. const mediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
  59. localVideo.srcObject = mediaStream;
  60. mediaStream.getTracks().forEach(track => {
  61. pc.addTrack(track, mediaStream);
  62. // pc.addTransceiver(track, {streams: [mediaStream]}); // 这个也可以
  63. });
  64. // pc.addStream(mediaStream); // 目前这个也可以,不过接口后续会废弃
  65. const answer = await pc.createAnswer(); // TODO 错误处理
  66. await pc.setLocalDescription(answer);
  67. sendRTCEvent({
  68. type: SIGNALING_ANSWER,
  69. payload: {
  70. sdp: answer,
  71. from: localUser,
  72. target: remoteUser
  73. }
  74. });
  75. }
  76. async function handleReceiveAnswer(msg) {
  77. log(`receive remote answer from ${msg.payload.from}`);
  78. const remoteDescription = new RTCSessionDescription(msg.payload.sdp);
  79. remoteUser = msg.payload.from;
  80. await pc.setRemoteDescription(remoteDescription); // TODO 错误处理
  81. }
  82. async function handleReceiveCandidate(msg){
  83. log(`receive candidate from ${msg.payload.from}`);
  84. await pc.addIceCandidate(msg.payload.candidate); // TODO 错误处理
  85. }
  86. /**
  87. * 发送用户相关消息给服务器
  88. * @param {Object} msg 格式如 { type: 'xx', payload: {} }
  89. */
  90. function sendUserEvent(msg) {
  91. socket.emit(CLIENT_USER_EVENT, JSON.stringify(msg));
  92. }
  93. /**
  94. * 发送RTC相关消息给服务器
  95. * @param {Object} msg 格式如{ type: 'xx', payload: {} }
  96. */
  97. function sendRTCEvent(msg) {
  98. socket.emit(CLIENT_RTC_EVENT, JSON.stringify(msg));
  99. }
  100. let pc = null;
  101. /**
  102. * 邀请用户加入视频聊天
  103. * 1、本地启动视频采集
  104. * 2、交换信令
  105. */
  106. async function startVideoTalk() {
  107. // 开启本地视频
  108. const localVideo = document.getElementById('local-video');
  109. const mediaStream = await navigator.mediaDevices.getUserMedia({
  110. video: true,
  111. audio: true
  112. });
  113. localVideo.srcObject = mediaStream;
  114. // 创建 peerConnection
  115. createPeerConnection();
  116. // 将媒体流添加到webrtc的音视频收发器
  117. mediaStream.getTracks().forEach(track => {
  118. pc.addTrack(track, mediaStream);
  119. // pc.addTransceiver(track, {streams: [mediaStream]});
  120. });
  121. // pc.addStream(mediaStream); // 目前这个也可以,不过接口后续会废弃
  122. }
  123. function createPeerConnection() {
  124. const iceConfig = {"iceServers": [
  125. {url: 'stun:stun.ekiga.net'},
  126. {url: 'turn:turnserver.com', username: 'user', credential: 'pass'}
  127. ]};
  128. pc = new RTCPeerConnection(iceConfig);
  129. pc.onnegotiationneeded = onnegotiationneeded;
  130. pc.onicecandidate = onicecandidate;
  131. pc.onicegatheringstatechange = onicegatheringstatechange;
  132. pc.oniceconnectionstatechange = oniceconnectionstatechange;
  133. pc.onsignalingstatechange = onsignalingstatechange;
  134. pc.ontrack = ontrack;
  135. return pc;
  136. }
  137. async function onnegotiationneeded() {
  138. log(`onnegotiationneeded.`);
  139. const offer = await pc.createOffer();
  140. await pc.setLocalDescription(offer); // TODO 错误处理
  141. sendRTCEvent({
  142. type: SIGNALING_OFFER,
  143. payload: {
  144. from: localUser,
  145. target: remoteUser,
  146. sdp: pc.localDescription // TODO 直接用offer?
  147. }
  148. });
  149. }
  150. function onicecandidate(evt) {
  151. if (evt.candidate) {
  152. log(`onicecandidate.`);
  153. sendRTCEvent({
  154. type: SIGNALING_CANDIDATE,
  155. payload: {
  156. from: localUser,
  157. target: remoteUser,
  158. candidate: evt.candidate
  159. }
  160. });
  161. }
  162. }
  163. function onicegatheringstatechange(evt) {
  164. log(`onicegatheringstatechange, pc.iceGatheringState is ${pc.iceGatheringState}.`);
  165. }
  166. function oniceconnectionstatechange(evt) {
  167. log(`oniceconnectionstatechange, pc.iceConnectionState is ${pc.iceConnectionState}.`);
  168. }
  169. function onsignalingstatechange(evt) {
  170. log(`onsignalingstatechange, pc.signalingstate is ${pc.signalingstate}.`);
  171. }
  172. // 调用 pc.addTrack(track, mediaStream),remote peer的 onTrack 会触发两次
  173. // 实际上两次触发时,evt.streams[0] 指向同一个mediaStream引用
  174. // 这个行为有点奇怪,github issue 也有提到 https://github.com/meetecho/janus-gateway/issues/1313
  175. let stream;
  176. function ontrack(evt) {
  177. // if (!stream) {
  178. // stream = evt.streams[0];
  179. // } else {
  180. // console.log(`${stream === evt.streams[0]}`); // 这里为true
  181. // }
  182. log(`ontrack.`);
  183. const remoteVideo = document.getElementById('remote-video');
  184. remoteVideo.srcObject = evt.streams[0];
  185. }
  186. // 点击用户列表
  187. async function handleUserClick(evt) {
  188. const target = evt.target;
  189. const userName = target.getAttribute('data-name').trim();
  190. if (userName === localUser) {
  191. alert('不能跟自己进行视频会话');
  192. return;
  193. }
  194. log(`online user selected: ${userName}`);
  195. remoteUser = userName;
  196. await startVideoTalk(remoteUser);
  197. }
  198. /**
  199. * 更新用户列表
  200. * @param {Array} users 用户列表,比如 [{name: '小明', name: '小强'}]
  201. */
  202. function updateUserList(users) {
  203. const fragment = document.createDocumentFragment();
  204. const userList = document.getElementById('login-users');
  205. userList.innerHTML = '';
  206. users.forEach(user => {
  207. const li = document.createElement('li');
  208. li.innerHTML = user.userName;
  209. li.setAttribute('data-name', user.userName);
  210. li.addEventListener('click', handleUserClick);
  211. fragment.appendChild(li);
  212. });
  213. userList.appendChild(fragment);
  214. }
  215. /**
  216. * 用户登录
  217. * @param {String} loginName 用户名
  218. */
  219. function login(loginName) {
  220. localUser = loginName;
  221. sendUserEvent({
  222. type: CLIENT_USER_EVENT_LOGIN,
  223. payload: {
  224. loginName: loginName
  225. }
  226. });
  227. }
  228. // 处理登录
  229. function handleLogin(evt) {
  230. let loginName = document.getElementById('login-name').value.trim();
  231. if (loginName === '') {
  232. alert('用户名为空!');
  233. return;
  234. }
  235. login(loginName);
  236. }
  237. function init() {
  238. document.getElementById('login-btn').addEventListener('click', handleLogin);
  239. }
  240. init();

2、服务端代码

  1. // 添加ws服务
  2. const io = require('socket.io')(server);
  3. let connectionList = [];
  4. const CLIENT_RTC_EVENT = 'CLIENT_RTC_EVENT';
  5. const SERVER_RTC_EVENT = 'SERVER_RTC_EVENT';
  6. const CLIENT_USER_EVENT = 'CLIENT_USER_EVENT';
  7. const SERVER_USER_EVENT = 'SERVER_USER_EVENT';
  8. const CLIENT_USER_EVENT_LOGIN = 'CLIENT_USER_EVENT_LOGIN';
  9. const SERVER_USER_EVENT_UPDATE_USERS = 'SERVER_USER_EVENT_UPDATE_USERS';
  10. function getOnlineUser() {
  11. return connectionList
  12. .filter(item => {
  13. return item.userName !== '';
  14. })
  15. .map(item => {
  16. return {
  17. userName: item.userName
  18. };
  19. });
  20. }
  21. function setUserName(connection, userName) {
  22. connectionList.forEach(item => {
  23. if (item.connection.id === connection.id) {
  24. item.userName = userName;
  25. }
  26. });
  27. }
  28. function updateUsers(connection) {
  29. connection.emit(SERVER_USER_EVENT, { type: SERVER_USER_EVENT_UPDATE_USERS, payload: getOnlineUser()});
  30. }
  31. io.on('connection', function (connection) {
  32. connectionList.push({
  33. connection: connection,
  34. userName: ''
  35. });
  36. // 连接上的用户,推送在线用户列表
  37. // connection.emit(SERVER_USER_EVENT, { type: SERVER_USER_EVENT_UPDATE_USERS, payload: getOnlineUser()});
  38. updateUsers(connection);
  39. connection.on(CLIENT_USER_EVENT, function(jsonString) {
  40. const msg = JSON.parse(jsonString);
  41. const {type, payload} = msg;
  42. if (type === CLIENT_USER_EVENT_LOGIN) {
  43. setUserName(connection, payload.loginName);
  44. connectionList.forEach(item => {
  45. // item.connection.emit(SERVER_USER_EVENT, { type: SERVER_USER_EVENT_UPDATE_USERS, payload: getOnlineUser()});
  46. updateUsers(item.connection);
  47. });
  48. }
  49. });
  50. connection.on(CLIENT_RTC_EVENT, function(jsonString) {
  51. const msg = JSON.parse(jsonString);
  52. const {payload} = msg;
  53. const target = payload.target;
  54. const targetConn = connectionList.find(item => {
  55. return item.userName === target;
  56. });
  57. if (targetConn) {
  58. targetConn.connection.emit(SERVER_RTC_EVENT, msg);
  59. }
  60. });
  61. connection.on('disconnect', function () {
  62. connectionList = connectionList.filter(item => {
  63. return item.connection.id !== connection.id;
  64. });
  65. connectionList.forEach(item => {
  66. // item.connection.emit(SERVER_USER_EVENT, { type: SERVER_USER_EVENT_UPDATE_USERS, payload: getOnlineUser()});
  67. updateUsers(item.connection);
  68. });
  69. });
  70. });

写在后面

WebRTC的API非常多,因为WebRTC本身就比较复杂,随着时间的推移,WebRTC的某些API(包括某些协议细节)也在改动或被废弃,这其中也有向后兼容带来的复杂性,比如本地视频采集后加入传输流,可以采用 addStream 或 addTrack 或 addTransceiver,再比如会话描述版本从plan-b迁移到unified-plan。

建议亲自动手撸一遍代码,加深了解。

相关链接

2019.08.02-video-talk-using-webrtc

https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection

onremotestream called twice for each remote stream

WebRTC:一个视频聊天的简单例子的更多相关文章

  1. ICE第二篇--一个"hello world"的简单例子

    1 本文介绍一个hello world输出的例子. ice应用的步骤如下: 1. 编写 Slice 定义并编译它. 2. 编写服务器并编译它. 3. 编写客户并编译它. 基本框架图示: 本文代码图示: ...

  2. 使用WebRTC搭建前端视频聊天室——入门篇

    http://segmentfault.com/a/1190000000436544 什么是WebRTC? 众所周知,浏览器本身不支持相互之间直接建立信道进行通信,都是通过服务器进行中转.比如现在有两 ...

  3. 在Ubuntu上部署一个基于webrtc的多人视频聊天服务

    最近研究webrtc视频直播技术,网上找了些教程最终都不太能顺利跑起来的,可能是文章写的比较老,使用的一些开源组件已经更新了,有些配置已经不太一样了,所以按照以前的步骤会有问题.折腾了一阵终于跑起来了 ...

  4. 使用WebRTC搭建前端视频聊天室——信令篇

    博客原文地址 建议看这篇之前先看一下使用WebRTC搭建前端视频聊天室——入门篇 如果需要搭建实例的话可以参照SkyRTC-demo:github地址 其中使用了两个库:SkyRTC(github地址 ...

  5. WebRTC实现网页版多人视频聊天室

    因为产品中要加入网页中网络会议的功能,这几天都在倒腾 WebRTC,现在分享下工作成果. 话说 WebRTC Real Time Communication 简称 RTC,是谷歌若干年前收购的一项技术 ...

  6. WebRTC搭建前端视频聊天室——信令篇

    这篇文章讲述了WebRTC中所涉及的信令交换以及聊天室中的信令交换,主要内容来自WebRTC in the real world: STUN, TURN and signaling,我在这里提取出的一 ...

  7. 使用WebRTC搭建前端视频聊天室——点对点通信篇

    WebRTC给我们带来了浏览器中的视频.音频聊天体验.但个人认为,它最实用的特性莫过于DataChannel——在浏览器之间建立一个点对点的数据通道.在DataChannel之前,浏览器到浏览器的数据 ...

  8. 一个简单例子:贫血模型or领域模型

    转:一个简单例子:贫血模型or领域模型 贫血模型 我们首先用贫血模型来实现.所谓贫血模型就是模型对象之间存在完整的关联(可能存在多余的关联),但是对象除了get和set方外外几乎就没有其它的方法,整个 ...

  9. 5分钟快速打造WebRTC视频聊天

    百度一下WebRTC,我想也是一堆.本以为用这位朋友( 搭建WebRtc环境 )的SkyRTC-demo 就可以一马平川的实现聊天,结果折腾了半天,文本信息都发不出去,更别说视频了.于是自己动手. 想 ...

随机推荐

  1. PATB 1004 成绩排名 (20)

    1004. 成绩排名 (20) 时间限制 400 ms 内存限制 65536 kB 代码长度限制 8000 B 判题程序 Standard 作者 CHEN, Yue 读入n名学生的姓名.学号.成绩,分 ...

  2. visudo 与 /etc/sudoers

    增加多个用户免密码登录 User_Alias USER_OPS = zouyi,hanerhui,shibeibei,gaoxudong,xiaoyuelin,wangsongfeng,sunjian ...

  3. 使用new新建动态二维数组(多注意)

    #include<iostream> using namespace std; int main() { //设想要建立一个rows行,cols列的矩阵 //使用new进行新建 int r ...

  4. 使用 cxf的程序 在win10 测试部署时报空指针异常

    2018-11-08 15:50:55.072 DEBUG 21524 --- [nio-8080-exec-1] o.s.b.w.s.f.OrderedRequestContextFilter  : ...

  5. black box黑盒测试

    软件规格说明书 等价类划分,完备性,无冗余性(不能有交集).   健壮等价类:无效等价类 边界值分析,对于一个含有n个变量的程序,采用边界值分析法测试程序会产生4n+1个测试用例           ...

  6. SPOJ:NPC2016A(数学)

    http://www.spoj.com/problems/NPC2016A/en/ 题意:在一个n*n的平面里面,初始在(x,y)需要碰到每条边一次,然后返回(x,y),问最短路径是多长. 思路:像样 ...

  7. SQL参数化查询

    参数化查询(Parameterized Query 或 Parameterized Statement)是指在设计与数据库链接并访问数据时,在需要填入数值或数据的地方,使用参数 (Parameter) ...

  8. CAD2014学习笔记-常用绘图命令和工具

    基于 虎课网huke88.com CAD教程 圆的绘制 快捷键c:选定圆心绘制半径长度的圆 快捷键c + 命令行输入 3p(三点成圆) 2p(两点成圆) t(选定两个圆的切点绘制与两圆相切的圆,第三部 ...

  9. MapReduce之提交job源码分析 FileInputFormat源码解析

    MapReduce之提交job源码分析 job 提交流程源码详解 //runner 类中提交job waitForCompletion() submit(); // 1 建立连接 connect(); ...

  10. [leetcode]95 Unique Binary Search Trees II (Medium)

    原题 字母题添加链接描述 一开始完全没有思路.. 百度看了别人的思路,对于这种递归构造的题目还是不熟,得多做做了. 这个题目难在构造出来.一般构造树都需要递归. 从1–n中任意选择一个数当做根节点,所 ...