上一节讲了 webRTC 的原理,今天我们就来实践一下。
我们知道,webRTC 是点对点的连接,它不需要服务器的参与,但是需要一个信令服务器来传递信令,这样才能使双方建立起连接。
这里我们用 node.js 来充当信令服务器,通过 websocket(socket.io)来传递信令。
新建目录 demo,在 demo 下新建 index.js 文件(信令服务器)和 文件夹 public(存放静态文件)。在 public 下新建 index.html 和 main.js 文件。
安装
| 12
 3
 
 | npm install expressnpm install fs
 npm install socket.io
 
 | 
编写信令服务器
index.js
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 
 | var express = require("express");const { createServer } = require("https");
 const { Server } = require("socket.io");
 const fs = require('fs');
 
 let options = {
 key: fs.readFileSync('./privatekey.key'),
 cert: fs.readFileSync('./certificate.crt')
 }
 
 var app = express();
 app.use('/', express.static("public"));
 
 var httpsServer = createServer(options, app)
 
 var io = new Server(httpsServer)
 
 
 io.on("connection", socket => {
 socket.on("add_room", () => {
 socket.join("room");
 socket.emit("conn");
 });
 
 
 socket.on('signalOffer', function (message) {
 socket.to('room').emit('signalOffer', message);
 });
 
 
 socket.on('signalAnswer', function (message) {
 socket.to('room').emit('signalAnswer', message);
 });
 
 
 socket.on('iceOffer', function (message) {
 socket.to('room').emit('iceOffer', message);
 });
 
 
 socket.on('iceAnswer', function (message) {
 socket.to('room').emit('iceAnswer', message);
 });
 });
 
 
 httpsServer.listen(3000, () => {
 console.log("服务开启");
 })
 
 | 
这里用 https 协议是因为摄像头和麦克风只能在 https 环境下才能被正常调用。
静态文件
index.html
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 
 | <html><head>
 <title>videoCall</title>
 </head>
 
 <div class="container">
 <h1>音视频通话</h1>
 <hr>
 <div class="video_container" align="center">
 
 <video id="local_video" controls autoplay muted webkit-playsinline></video>
 
 <video id="remote_video" controls autoplay muted webkit-playsinline></video>
 </div>
 <hr>
 <button id="startButton">加入房间</button>
 <button id="hangupButton">挂断</button>
 
 <script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.4.1/dist/socket.io.min.js"></script>
 <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
 <script src="main.js"></script>
 </div>
 </html>
 
 | 
main.js
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 
 | var localVideo = document.getElementById('local_video');
 
 var remoteVideo = document.getElementById('remote_video');
 
 
 var startButton = document.getElementById('startButton');
 
 var hangupButton = document.getElementById('hangupButton');
 
 var pc;
 var localStream;
 
 var socket = io.connect();
 
 
 
 const offerOptions = {
 offerToReceiveVideo: 1,
 offerToReceiveAudio: 1
 };
 
 hangupButton.disabled = true;
 
 startButton.addEventListener('click', startAction);
 hangupButton.addEventListener('click', hangupAction);
 
 
 function startAction () {
 
 if (navigator.mediaDevices === undefined) {
 navigator.mediaDevices = {};
 }
 if (navigator.mediaDevices.getUserMedia === undefined) {
 navigator.mediaDevices.getUserMedia = function (constraints) {
 var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia || navigator.oGetUserMedia;
 
 if (!getUserMedia) {
 return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
 }
 
 return new Promise(function (resolve, reject) {
 getUserMedia.call(navigator, constraints, resolve, reject);
 });
 }
 }
 
 
 navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(function (mediastream) {
 localStream = mediastream;
 localVideo.srcObject = mediastream;
 startButton.disabled = true;
 socket.emit('add_room');
 }).catch(function (e) {
 alert(e);
 });
 }
 
 
 socket.on('conn', function () {
 hangupButton.disabled = false;
 pc = new RTCPeerConnection();
 
 localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
 
 pc.createOffer(offerOptions).then(function (offer) {
 pc.setLocalDescription(offer);
 socket.emit('signalOffer', offer);
 });
 
 pc.addEventListener('icecandidate', function (event) {
 var iceCandidate = event.candidate;
 if (iceCandidate) {
 
 socket.emit('iceOffer', iceCandidate);
 }
 });
 });
 
 
 socket.on('signalOffer', function (message) {
 pc.setRemoteDescription(new RTCSessionDescription(message));
 
 pc.createAnswer().then(function (answer) {
 pc.setLocalDescription(answer);
 socket.emit('signalAnswer', answer);
 })
 
 
 pc.addEventListener('track', function (event) {
 event.streams.forEach(stream => {
 remoteVideo.srcObject = stream;
 });
 });
 });
 
 
 socket.on('signalAnswer', function (message) {
 pc.setRemoteDescription(new RTCSessionDescription(message));
 console.log('remote answer');
 
 
 pc.addEventListener('track', function (event) {
 event.streams.forEach(stream => {
 remoteVideo.srcObject = stream;
 });
 });
 });
 
 
 socket.on('iceOffer', function (message) {
 addIceCandidates(message)
 });
 
 
 socket.on('iceAnswer', function (message) {
 addIceCandidates(message)
 });
 
 
 function addIceCandidates (message) {
 if (pc !== 'undefined') {
 pc.addIceCandidate(new RTCIceCandidate(message));
 }
 }
 
 
 function hangupAction () {
 localStream.getTracks().forEach(track => track.stop());
 pc.close();
 pc = null;
 hangupButton.disabled = true;
 startButton.disabled = false;
 }
 
 | 
webRTC 中交换描述和 ice 的具体过程:
第一步:创建一个 RTCPeerConnection,并添加本地媒体流。
| 12
 3
 
 | pc = new RTCPeerConnection();
 localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
 
 | 
第二步:如果是发起者,那么创建 offer,添加到本地描述,然后通过信令服务器发送给接收方。
| 12
 3
 4
 
 | pc.createOffer(offerOptions).then(function (offer) {pc.setLocalDescription(offer);
 socket.emit('signalOffer', offer)
 });
 
 | 
第三步:接收方获取到 offer,将其添加到远程描述,然后创建 answer,添加到本地描述,并将 answer 发送给发起方。
| 12
 3
 4
 5
 6
 
 | pc.setRemoteDescription(new RTCSessionDescription(message)); 
 pc.createAnswer().then(function (answer) {
 pc.setLocalDescription(answer);
 socket.emit('signalAnswer', answer);
 })
 
 | 
第四步:发起方获取到 answer,将其添加到远程描述。
| 1
 | pc.setRemoteDescription(new RTCSessionDescription(message));
 | 
第五步:交换 ice。其实在创建 offer 或 answer 时会触发 icecandidate 事件,通过该事件可以获取到 ice 信息,然后将信息发送给对方,对方将其添加即可。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | pc.addEventListener('icecandidate', function (event) {const iceCandidate = event.candidate;
 if (iceCandidate) {
 socket.emit('iceOffer', iceCandidate);
 }
 });
 
 socket.on('iceOffer', function (message) {
 pc.addIceCandidate(new RTCIceCandidate(message));
 });
 
 | 
开启服务
执行
然后在浏览器输入 https://localhost:3000 即可访问。
注意:通信双方需要在同一局域网下。