本章带大家基于WebRTC实现多端非音视频数据传输,其中包括:1.文本聊天,2.传输文件,通过本章的学习大家可以掌握好如何用WebRTC的数据通道,传输非音视频数据。

14-1 【基础铺垫,学前有概念】传输非音视频数据基础知识

image.png
image.png

image.png
image.png
image.png
image.png

image.png

14-2 【来点实战】端到端文本聊天

1、效果

两人连接后,即可以互相发送数据。
image.png

2、index.html

  1. <html>
  2. <head>
  3. <title>WebRTC PeerConnection</title>
  4. <link href="./css/main.css" rel="stylesheet" />
  5. </head>
  6. <body>
  7. <div>
  8. <div>
  9. <button id="connserver">Connect Sig Server</button>
  10. <button id="leave" disabled>Leave</button>
  11. </div>
  12. <div>
  13. <label>BandWidth:</label>
  14. <select id="bandwidth" disabled>
  15. <option value="unlimited" selected>unlimited</option>
  16. <option value="2000">2000</option>
  17. <option value="1000">1000</option>
  18. <option value="500">500</option>
  19. <option value="250">250</option>
  20. <option value="125">125</option>
  21. </select>
  22. kbps
  23. </div>
  24. <div class="preview">
  25. <div>
  26. <h2>Local:</h2>
  27. <video id="localvideo" autoplay playsinline muted></video>
  28. <h2>Remote:</h2>
  29. <video id="remotevideo" autoplay playsinline></video>
  30. </div>
  31. <div>
  32. <h2>Chat:<h2>
  33. <textarea id="chat" disabled></textarea>
  34. <textarea id="sendtxt" disabled></textarea>
  35. <button id="send" disabled>Send</button>
  36. </div>
  37. </div>
  38. <div class="preview">
  39. <div class="graph-container" id="bitrateGraph">
  40. <div>Bitrate</div>
  41. <canvas id="bitrateCanvas"></canvas>
  42. </div>
  43. <div class="graph-container" id="packetGraph">
  44. <div>Packets sent per second</div>
  45. <canvas id="packetCanvas"></canvas>
  46. </div>
  47. </div>
  48. </div>
  49. <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"></script>
  50. <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  51. <script src="js/main_bw.js"></script>
  52. <script src="js/third_party/graph.js"></script>
  53. </body>
  54. </html>

3、main_bw.js

等第二个人来的时候,才创建DataChannel。
主要是实现

  1. //create data channel for transporting non-audio/video data
  2. dc = pc.createDataChannel('chatchannel');
  3. dc.onmessage = receivemsg;
  4. dc.onopen = dataChannelStateChange;
  5. dc.onclose = dataChannelStateChange;

要实现receivemsg,dataChannelStateChange,dataChannelStateChange函数。

完整代码如下:

  1. 'use strict'
  2. var localVideo = document.querySelector('video#localvideo');
  3. var remoteVideo = document.querySelector('video#remotevideo');
  4. var btnConn = document.querySelector('button#connserver');
  5. var btnLeave = document.querySelector('button#leave');
  6. var optBw = document.querySelector('select#bandwidth');
  7. var chat = document.querySelector('textarea#chat');
  8. var send_txt = document.querySelector('textarea#sendtxt');
  9. var btnSend = document.querySelector('button#send');
  10. var bitrateGraph;
  11. var bitrateSeries;
  12. var packetGraph;
  13. var packetSeries;
  14. var lastResult;
  15. var pcConfig = {
  16. 'iceServers': [{
  17. 'urls': 'turn:stun.al.learningrtc.cn:3478',
  18. 'credential': "mypasswd",
  19. 'username': "garrylea"
  20. }]
  21. };
  22. var localStream = null;
  23. var remoteStream = null;
  24. var pc = null;
  25. var dc = null;
  26. var roomid;
  27. var socket = null;
  28. var offerdesc = null;
  29. var state = 'init';
  30. function sendMessage(roomid, data){
  31. console.log('send message to other end', roomid, data);
  32. if(!socket){
  33. console.log('socket is null');
  34. }
  35. socket.emit('message', roomid, data);
  36. }
  37. function dataChannelStateChange() {
  38. var readyState = dc.readyState;
  39. console.log('Send channel state is: ' + readyState);
  40. if (readyState === 'open') {
  41. send_txt.disabled = false;
  42. send.disabled = false;
  43. } else {
  44. send_txt.disabled = true;
  45. send.disabled = true;
  46. }
  47. }
  48. function conn(){
  49. socket = io.connect();
  50. socket.on('joined', (roomid, id) => {
  51. console.log('receive joined message!', roomid, id);
  52. state = 'joined'
  53. //如果是多人的话,第一个人不该在这里创建peerConnection
  54. //都等到收到一个otherjoin时再创建
  55. //所以,在这个消息里应该带当前房间的用户数
  56. //
  57. //create conn and bind media track
  58. createPeerConnection();
  59. bindTracks();
  60. btnConn.disabled = true;
  61. btnLeave.disabled = false;
  62. console.log('receive joined message, state=', state);
  63. });
  64. socket.on('otherjoin', (roomid) => {
  65. console.log('receive joined message:', roomid, state);
  66. //如果是多人的话,每上来一个人都要创建一个新的 peerConnection
  67. //
  68. if(state === 'joined_unbind'){
  69. createPeerConnection();
  70. bindTracks();
  71. }
  72. //create data channel for transporting non-audio/video data
  73. dc = pc.createDataChannel('chatchannel');
  74. dc.onmessage = receivemsg;
  75. dc.onopen = dataChannelStateChange;
  76. dc.onclose = dataChannelStateChange;
  77. state = 'joined_conn';
  78. call();
  79. console.log('receive other_join message, state=', state);
  80. });
  81. socket.on('full', (roomid, id) => {
  82. console.log('receive full message', roomid, id);
  83. socket.disconnect();
  84. hangup();
  85. closeLocalMedia();
  86. state = 'leaved';
  87. console.log('receive full message, state=', state);
  88. alert('the room is full!');
  89. });
  90. socket.on('leaved', (roomid, id) => {
  91. console.log('receive leaved message', roomid, id);
  92. state='leaved'
  93. socket.disconnect();
  94. console.log('receive leaved message, state=', state);
  95. btnConn.disabled = false;
  96. btnLeave.disabled = true;
  97. optBw.disabled = true;
  98. });
  99. socket.on('bye', (room, id) => {
  100. console.log('receive bye message', roomid, id);
  101. //state = 'created';
  102. //当是多人通话时,应该带上当前房间的用户数
  103. //如果当前房间用户不小于 2, 则不用修改状态
  104. //并且,关闭的应该是对应用户的peerconnection
  105. //在客户端应该维护一张peerconnection表,它是
  106. //一个key:value的格式,key=userid, value=peerconnection
  107. state = 'joined_unbind';
  108. hangup();
  109. console.log('receive bye message, state=', state);
  110. });
  111. socket.on('disconnect', (socket) => {
  112. console.log('receive disconnect message!', roomid);
  113. if(!(state === 'leaved')){
  114. hangup();
  115. closeLocalMedia();
  116. }
  117. state = 'leaved';
  118. btnConn.disabled = false;
  119. btnLeave.disabled = true;
  120. optBw.disabled = true;
  121. });
  122. socket.on('message', (roomid, data) => {
  123. console.log('receive message!', roomid, data);
  124. if(data === null || data === undefined){
  125. console.error('the message is invalid!');
  126. return;
  127. }
  128. if(data.hasOwnProperty('type') && data.type === 'offer') {
  129. pc.setRemoteDescription(new RTCSessionDescription(data));
  130. //create answer
  131. pc.createAnswer()
  132. .then(getAnswer)
  133. .catch(handleAnswerError);
  134. }else if(data.hasOwnProperty('type') && data.type === 'answer'){
  135. optBw.disabled = false
  136. pc.setRemoteDescription(new RTCSessionDescription(data));
  137. }else if (data.hasOwnProperty('type') && data.type === 'candidate'){
  138. var candidate = new RTCIceCandidate({
  139. sdpMLineIndex: data.label,
  140. candidate: data.candidate
  141. });
  142. pc.addIceCandidate(candidate)
  143. .then(()=>{
  144. console.log('Successed to add ice candidate');
  145. })
  146. .catch(err=>{
  147. console.error(err);
  148. });
  149. }else{
  150. console.log('the message is invalid!', data);
  151. }
  152. });
  153. roomid = '111111';
  154. socket.emit('join', roomid);
  155. return true;
  156. }
  157. function connSignalServer(){
  158. //开启本地视频
  159. start();
  160. return true;
  161. }
  162. function getMediaStream(stream){
  163. localStream = stream;
  164. localVideo.srcObject = localStream;
  165. //这个函数的位置特别重要,
  166. //一定要放到getMediaStream之后再调用
  167. //否则就会出现绑定失败的情况
  168. //setup connection
  169. conn();
  170. bitrateSeries = new TimelineDataSeries();
  171. bitrateGraph = new TimelineGraphView('bitrateGraph', 'bitrateCanvas');
  172. bitrateGraph.updateEndDate();
  173. packetSeries = new TimelineDataSeries();
  174. packetGraph = new TimelineGraphView('packetGraph', 'packetCanvas');
  175. packetGraph.updateEndDate();
  176. }
  177. function getDeskStream(stream){
  178. localStream = stream;
  179. }
  180. function handleError(err){
  181. console.error('Failed to get Media Stream!', err);
  182. }
  183. function shareDesk(){
  184. if(IsPC()){
  185. navigator.mediaDevices.getDisplayMedia({video: true})
  186. .then(getDeskStream)
  187. .catch(handleError);
  188. return true;
  189. }
  190. return false;
  191. }
  192. function start(){
  193. if(!navigator.mediaDevices ||
  194. !navigator.mediaDevices.getUserMedia){
  195. console.error('the getUserMedia is not supported!');
  196. return;
  197. }else {
  198. var constraints = {
  199. video: true,
  200. audio: false
  201. }
  202. navigator.mediaDevices.getUserMedia(constraints)
  203. .then(getMediaStream)
  204. .catch(handleError);
  205. }
  206. }
  207. function getRemoteStream(e){
  208. remoteStream = e.streams[0];
  209. remoteVideo.srcObject = e.streams[0];
  210. }
  211. function handleOfferError(err){
  212. console.error('Failed to create offer:', err);
  213. }
  214. function handleAnswerError(err){
  215. console.error('Failed to create answer:', err);
  216. }
  217. function getAnswer(desc){
  218. pc.setLocalDescription(desc);
  219. optBw.disabled = false;
  220. //send answer sdp
  221. sendMessage(roomid, desc);
  222. }
  223. function getOffer(desc){
  224. pc.setLocalDescription(desc);
  225. offerdesc = desc;
  226. //send offer sdp
  227. sendMessage(roomid, offerdesc);
  228. }
  229. function receivemsg(e){
  230. var msg = e.data;
  231. if(msg){
  232. console.log(msg);
  233. chat.value += "->" + msg + "\r\n";
  234. }else{
  235. console.error('received msg is null');
  236. }
  237. }
  238. function createPeerConnection(){
  239. //如果是多人的话,在这里要创建一个新的连接.
  240. //新创建好的要放到一个map表中。
  241. //key=userid, value=peerconnection
  242. console.log('create RTCPeerConnection!');
  243. if(!pc){
  244. pc = new RTCPeerConnection(pcConfig);
  245. pc.onicecandidate = (e)=>{
  246. if(e.candidate) {
  247. sendMessage(roomid, {
  248. type: 'candidate',
  249. label:event.candidate.sdpMLineIndex,
  250. id:event.candidate.sdpMid,
  251. candidate: event.candidate.candidate
  252. });
  253. }else{
  254. console.log('this is the end candidate');
  255. }
  256. }
  257. pc.ondatachannel = e=> {
  258. if(!dc){
  259. dc = e.channel;
  260. dc.onmessage = receivemsg;
  261. dc.onopen = dataChannelStateChange;
  262. dc.onclose = dataChannelStateChange;
  263. }
  264. }
  265. pc.ontrack = getRemoteStream;
  266. }else {
  267. console.log('the pc have be created!');
  268. }
  269. return;
  270. }
  271. //绑定永远与 peerconnection在一起,
  272. //所以没必要再单独做成一个函数
  273. function bindTracks(){
  274. console.log('bind tracks into RTCPeerConnection!');
  275. if( pc === null && localStream === undefined) {
  276. console.error('pc is null or undefined!');
  277. return;
  278. }
  279. if(localStream === null && localStream === undefined) {
  280. console.error('localstream is null or undefined!');
  281. return;
  282. }
  283. //add all track into peer connection
  284. localStream.getTracks().forEach((track)=>{
  285. pc.addTrack(track, localStream);
  286. });
  287. }
  288. function call(){
  289. if(state === 'joined_conn'){
  290. var offerOptions = {
  291. offerToRecieveAudio: 1,
  292. offerToRecieveVideo: 1
  293. }
  294. pc.createOffer(offerOptions)
  295. .then(getOffer)
  296. .catch(handleOfferError);
  297. }
  298. }
  299. function hangup(){
  300. if(!pc) {
  301. return;
  302. }
  303. offerdesc = null;
  304. pc.close();
  305. pc = null;
  306. }
  307. function closeLocalMedia(){
  308. if(!(localStream === null || localStream === undefined)){
  309. localStream.getTracks().forEach((track)=>{
  310. track.stop();
  311. });
  312. }
  313. localStream = null;
  314. }
  315. function leave() {
  316. socket.emit('leave', roomid); //notify server
  317. dc.close();
  318. dc = null;
  319. hangup();
  320. closeLocalMedia();
  321. btnConn.disabled = false;
  322. btnLeave.disabled = true;
  323. optBw.disabled = true;
  324. send_txt.disabled = true;
  325. send.disabled = true;
  326. }
  327. function chang_bw()
  328. {
  329. optBw.disabled = true;
  330. var bw = optBw.options[optBw.selectedIndex].value;
  331. var vsender = null;
  332. var senders = pc.getSenders();
  333. senders.forEach( sender => {
  334. if(sender && sender.track.kind === 'video'){
  335. vsender = sender;
  336. }
  337. });
  338. var parameters = vsender.getParameters();
  339. if(!parameters.encodings){
  340. return;
  341. }
  342. if(bw === 'unlimited'){
  343. return;
  344. }
  345. parameters.encodings[0].maxBitrate = bw * 1000;
  346. vsender.setParameters(parameters)
  347. .then(()=>{
  348. optBw.disabled = false;
  349. console.log('Successed to set parameters!');
  350. })
  351. .catch(err => {
  352. console.error(err);
  353. })
  354. }
  355. // query getStats every second
  356. window.setInterval(() => {
  357. if (!pc) {
  358. return;
  359. }
  360. const sender = pc.getSenders()[0];
  361. if (!sender) {
  362. return;
  363. }
  364. sender.getStats().then(res => {
  365. res.forEach(report => {
  366. let bytes;
  367. let packets;
  368. if (report.type === 'outbound-rtp') {
  369. if (report.isRemote) {
  370. return;
  371. }
  372. const now = report.timestamp;
  373. bytes = report.bytesSent;
  374. packets = report.packetsSent;
  375. if (lastResult && lastResult.has(report.id)) {
  376. // calculate bitrate
  377. const bitrate = 8 * (bytes - lastResult.get(report.id).bytesSent) /
  378. (now - lastResult.get(report.id).timestamp);
  379. // append to chart
  380. bitrateSeries.addPoint(now, bitrate);
  381. bitrateGraph.setDataSeries([bitrateSeries]);
  382. bitrateGraph.updateEndDate();
  383. // calculate number of packets and append to chart
  384. packetSeries.addPoint(now, packets -
  385. lastResult.get(report.id).packetsSent);
  386. packetGraph.setDataSeries([packetSeries]);
  387. packetGraph.updateEndDate();
  388. }
  389. }
  390. });
  391. lastResult = res;
  392. });
  393. }, 1000);
  394. function sendText(){
  395. var data = send_txt.value;
  396. if(data != null){
  397. dc.send(data);
  398. }
  399. //更好的展示
  400. send_txt.value = "";
  401. chat.value += '<- ' + data + '\r\n';
  402. }
  403. btnConn.onclick = connSignalServer
  404. btnLeave.onclick = leave;
  405. optBw.onchange = chang_bw;
  406. btnSend.onclick = sendText;

4、运行

  1. npm install socket.io@2.0.4 log4js express serve-index
  2. sudo node server.js

14-3 【练手的机会来了】文件实时传输

image.png

image.png

效果
发送方
image.png
接收方
image.png

14-4 相关资料

14chapter.zip