上一节讲了 webRTC 的原理,今天我们就来实践一下。
我们知道,webRTC 是点对点的连接,它不需要服务器的参与,但是需要一个信令服务器来传递信令,这样才能使双方建立起连接。
这里我们用 node.js 来充当信令服务器,通过 websocket(socket.io)来传递信令。
新建目录 demo,在 demo 下新建 index.js 文件(信令服务器)和 文件夹 public(存放静态文件)。在 public 下新建 index.html 和 main.js 文件。
安装
1 2 3
| npm install express npm install fs npm install socket.io
|
编写信令服务器
index.js
1 2 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
1 2 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
1 2 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,并添加本地媒体流。
1 2 3
| pc = new RTCPeerConnection();
localStream.getTracks().forEach(track => pc.addTrack(track, localStream));
|
第二步:如果是发起者,那么创建 offer,添加到本地描述,然后通过信令服务器发送给接收方。
1 2 3 4
| pc.createOffer(offerOptions).then(function (offer) { pc.setLocalDescription(offer); socket.emit('signalOffer', offer) });
|
第三步:接收方获取到 offer,将其添加到远程描述,然后创建 answer,添加到本地描述,并将 answer 发送给发起方。
1 2 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 信息,然后将信息发送给对方,对方将其添加即可。
1 2 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
即可访问。
注意:通信双方需要在同一局域网下。