多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
[TOC] ## 注意要点 网络连接要在音视频数据获取到之后,否则有可能绑定音视频流失败。 当一端退出房间后,另一端的peerconnection要关闭重建,否则与新用户互通时媒体协商会失败。 异步事件处理 ## 1v1视频通话实现 [https://github.com/yf30301200/webrtc\_js](https://github.com/yf30301200/webrtc_js) ## client.html ``` <html> <head> <title>WebRTC PeerConnection</title> </head> <body> <div> <div> <button id="connserver"> Connection signal</button> <button id="leave">Leaved</button> </div> <div id="preview"> <div style="width:500px; height:auto; float:left;"> <h2>Local:</h2> <video id="localvideo" autoplay playsinline></video> <h2> Offer SDP:</h2> <textarea id="offer" cols="40" rows="30"></textarea> </div> <div style="width:500px; height:auto; float:left;"> <h2>Remote:</h2> <video id="remotevideo" autoplay playsinline></video> <h2> Answer SDP:</h2> <textarea id="answer" cols="40" rows="30"></textarea> </div> </div> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"></script> <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script> <script src="js/room.js"></script> </body> </html> ``` ## client.js ``` 'use strict' var JOINED_CONN = 'joined_conn'; var JOINED_UNBIND = 'joined_unbind'; var FULL = 'FULL'; var LEAVED = 'leaved'; var localVideo = document.querySelector('video#localvideo'); var remoteVideo = document.querySelector('video#remotevideo'); var offer = document.querySelector('textarea#offer'); var answer = document.querySelector('textarea#answer'); var btnConn = document.querySelector('button#connserver'); var btnLeave = document.querySelector('button#leave'); var localStream = null; var remoteStream = null; var socket = null; var state = 'init'; var roomid = '111111'; var pc = null; btnConn.onclick = connSignalServer; btnLeave.onclick = leave; function connSignalServer() { start(); return true; } function start() { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { console.error('the getUserMedia is not supported!'); return; }else{ var constraints = { video: { width:300, height:300 }, audio: false } navigator.mediaDevices.getUserMedia(constraints) .then(getMediaStream) .catch(handleError) } } /* 信令部分 */ function conn() { socket = io.connect(); // 与服务器连接 socket.on('joined', (roomid, id) => { console.log('reveive join message:', roomid, id); createPeerConnection(); bindTracks(); btnConn.disabled = true; btnLeave.disabled = false; }); socket.on('otherjoin', (roomid, id) => { console.log('reveive otherjoin message:', roomid, id, state); if (state === JOINED_UNBIND) { createPeerConnection(); bindTracks(); } state = JOINED_CONN; // 媒体协商 console.log('receive otherjoin message:state', state); call(); }); socket.on('full', (roomid, id) => { console.log('reveive full message:', roomid, id); closeLocalMedia(); state = LEAVED; console.log('receive full message:state:', state); socket.disconnect(); alert('the room is full!'); }); socket.on('leaved', (roomid, id) => { console.log('reveive leaved message:', roomid, id); state = LEAVED; console.log('receive leaved message state=', state); socket.disconnect(); btnConn.disabled = false; btnLeave.disabled = true; }); socket.on('bye', (roomid, id) => { console.log('reveive bye message:', roomid, id); state = JOINED_UNBIND; closePeerConnection(); console.log('receive bye message state=', state); }); socket.on('message', (roomid, data)=> { console.log('reveive client message:', roomid, data); if(data === null || data === undefined){ console.error('the message is invalid!'); return; } //媒体协商 if (data.hasOwnProperty('type') && data.type === 'offer') { offer.value = data.sdp; pc.setRemoteDescription(new RTCSessionDescription(data)); pc.createAnswer() .then(getAnswer) .catch(handleAnswerError); }else if (data.hasOwnProperty('type') && data.type === 'answer') { answer.value = data.sdp; pc.setRemoteDescription(new RTCSessionDescription(data)); }else if (data.hasOwnProperty('type') && data.type === 'candidate') { var candidate = new RTCIceCandidate({ sdpMLineIndex: data.label, candidate: data.candidate }); pc.addIceCandidate(candidate); } else { console.error('the message is invalid!', data); } }); socket.emit('join', roomid); return; } function call() { if (state === JOINED_CONN) { if (pc) { let options = { offerToReceiveAudio: 0, offerToReceiveVideo: 1 }; pc.createOffer(options) .then(getOffer) .catch(handleOfferError) } } } function leave() { if (socket) { socket.emit('leave', roomid); } //释放资源 closePeerConnection(); closeLocalMedia(); btnConn.disabled = false; btnLeave.disabled = true; } function createPeerConnection() { console.log('create RTCPeerConnection!'); if (!pc){ let pcConfig = { 'iceServers': [{ 'urls':'turn:192.168.1.13:3478', 'credential':'test', 'username':'test' }] }; pc = new RTCPeerConnection(pcConfig); pc.onicecandidate = (e)=>{ if (e.candidate) { console.log('find an new candidate', e.candidate); sendMessage(roomid, { type: 'candidate', label: e.candidate.sdpMLineIndex, id: e.candidate.sdpMid, candidate: e.candidate.candidate }); } } pc.ontrack = getRemoteStream; } } function getMediaStream(stream) { if (localStream) { stream.getAudioTracks().forEach((track) => { localStream.addTrack(track); stream.removeTrack(track); }); } else { localStream = stream; } localVideo.srcObject = localStream; conn(); } function getRemoteStream(e) { remoteStream = e.streams[0]; remoteVideo.srcObject = e.streams[0]; } function closePeerConnection() { console.log('close RTCPeerConnection!'); if (pc) { pc.close(); pc = null; } } function closeLocalMedia() { if (localStream && localStream.getTracks()) { localStream.getTracks().forEach((track) => { track.stop(); }); } localStream = null; } function handleError(err) { console.error('Faile to getMedia Stream!', err); } function handleAnswerError(err) { console.error('Faile to create Answer!', err); } function handleOfferError(err) { console.error('Faile to create Offer!', err); } function sendMessage(roomid, data) { //console.log('send p2p message:', roomid, data) if (socket) { socket.emit('message', roomid, data); } } function bindTracks() { console.log('bind tracks into RTCPeerConnection!'); if( pc === null || pc === undefined) { console.error('pc is null or undefined!'); return; } if(localStream === null || localStream === undefined) { console.error('localstream is null or undefined!'); return; } // 本地采集的音视频流添加到pc if (localStream) { localStream.getTracks().forEach((track)=>{ pc.addTrack(track, localStream); }); } } function getAnswer(desc) { pc.setLocalDescription(desc); console.log('answer sdp:', desc); answer.value = desc.sdp; sendMessage(roomid, desc); } function getOffer(desc) { pc.setLocalDescription(desc); offer.value = desc.sdp; sendMessage(roomid, desc); } ``` ## Signal Server ``` 'use strict' var express = require('express'); var serveIndex= require('serve-index'); var http = require('http'); var https = require('https'); var fs = require('fs'); var socketIo = require('socket.io'); var log4js = require('log4js'); var USERVOUNT = 3 log4js.configure({ appenders: { file: { type: 'file', filename: 'app.log', layout: { type: 'pattern', pattern: '%r %p - %m', } } }, categories: { default: { appenders: ['file'], level: 'debug' } } }); var logger = log4js.getLogger(); var app = express(); app.use('/', express.static('./public')); var options = { key: fs.readFileSync('./cert/privatekey.pem'), cert: fs.readFileSync('./cert/certificate.pem') } var httpServer = http.createServer(app); var httpsServer = https.createServer(options, app); var io = socketIo.listen(httpsServer); io.sockets.on('connection', (socket) => { socket.on('message', (room, data) => { socket.to(room).emit('message', room, data); }) socket.on('join',(room)=>{ socket.join(room); var myRoom = io.sockets.adapter.rooms[room]; var users = (myRoom) ? Object.keys(myRoom.sockets).length : 0;//拿到房间里所有的人数 logger.debug('the number of user in room is:' + users); if (users < USERVOUNT) { socket.emit('joined', room, socket.id); //给本人回消息 if (users > 1) { socket.to(room).emit('otherjoin', room, socket.id); } }else { socket.leave(room); socket.emit('full', room, socket.id); } //socket.emit('joined', room, socket.id); //给本人回消息 //socket.to(room).emit('joined', root, socket.id); //除自己之外 //socket.broadcast.emit('joined', room, socket.id); //除自己外,全部站点 //io.in(room).emit('joined', room, socket.id)//房间内所有人 }); socket.on('leave',(room)=>{ var myRoom = io.sockets.adapter.rooms[room]; var users = (myRoom) ? Object.keys(myRoom.sockets).length : 0;//拿到房间里所有的人数 logger.debug('the user number of room is:' + (users-1)); socket.to(room).emit('bye', room, socket.id); //除了自己以外的其他人发送bye socket.emit('leaved', room, socket.id);// 给自己发送leaved //socket.leave(room); //socket.emit('joined', room, socket.id); //给本人回消息 //socket.to(room).emit('joined', root, socket.id); //除自己之外 //io.in(room).emit('joined', room, socket.id)//房间内所有人 //socket.broadcast.emit('leave', room, socket.id); //除自己外,全部站点 }); }); httpsServer.listen(8888,'0.0.0.0', function() { console.log('HTTPS Server is running on: http://0.0.0.0:%s', 8888); }); httpServer.listen(8887,'0.0.0.0', function() { console.log('HTTP Server is running on: http://0.0.0.0:%s', 8887); }); //可以根据请求判断是http还是https app.get('/', function (req, res) { if(req.protocol === 'https') { res.status(200).send('This is https visit!'); } else { res.status(200).send('This is http visit!'); } }); ```