上一节讲了 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(httpServer)
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; }
|
开启服务
执行
然后在浏览器输入 https://localhost:3000
即可访问。
注意:通信双方需要在同一局域网下。