音視頻文章匯總,上一篇文章《00-WebRTC入門》介紹了nodejs作為信令服務(wù)器棺棵,客戶端和服務(wù)器端的交互選擇websocket作為通信協(xié)議称开。本文從代碼層面實(shí)現(xiàn)一對(duì)一視頻通話蹄殃。
1.一對(duì)一通話原理
主要分為四大塊:
I.信令設(shè)計(jì):進(jìn)入房間敬鬓,離開房間等
II.媒體協(xié)商:交換彼此客戶端的媒體信息sdp
III.加入Stream/Track
IV.網(wǎng)絡(luò)協(xié)商:Candidate雏掠,網(wǎng)絡(luò)地址,端口號(hào)等
先看一張圖
1.1信令協(xié)議設(shè)計(jì)
采用json封裝格式
- join 加入房間
- respjoin
當(dāng)join房間后發(fā)現(xiàn)房間已經(jīng)存在另一個(gè)人時(shí)則返回另一個(gè)人的uid创橄;如果只有自己則不返回 - leave 離開房間腐巢,服務(wù)器收到leave信令則檢查同一房間是否有其他人,如果有其他人則通知他有人離開
- newpeer
服務(wù)器通知客戶端有新人加入腻脏,收到newpeer
則發(fā)起連接請(qǐng)求 - peerleave
服務(wù)器通知客戶端有人離開 - offer 轉(zhuǎn)發(fā)offer sdp
- answer 轉(zhuǎn)發(fā)answer sdp
- candidate 轉(zhuǎn)發(fā)candidate sdp
Join
var jsonMsg = {
'cmd': 'join',
'roomId': roomId,
'uid': localUserId,
};
respjoin
jsonMsg = {
'cmd': 'resp‐join',
'remoteUid': remoteUid
};
leave
var jsonMsg = {
'cmd': 'leave',
'roomId': roomId,
'uid': localUserId,
};
newpeer
var jsonMsg = {
'cmd': 'new‐peer',
'remoteUid': uid
};
peerleave
var jsonMsg = {
'cmd': 'peer‐leave',
'remoteUid': uid
};
offer
var jsonMsg = {
'cmd': 'offer',
'roomId': roomId,
'uid': localUserId,
'remoteUid':remoteUserId,
'msg': JSON.stringify(sessionDescription)
};
answer
var jsonMsg = {
'cmd': 'answer',
'roomId': roomId,
'uid': localUserId,
'remoteUid':remoteUserId,
'msg': JSON.stringify(sessionDescription)
};
candidate
var jsonMsg = {
'cmd': 'candidate',
'roomId': roomId,
'uid': localUserId,
'remoteUid':remoteUserId,
'msg': JSON.stringify(candidateJson)
};
1.2媒體協(xié)商
createOffer
基本格式
aPromise = myPeerConnection.createOffer([options])?
[options]
var options = {
offerToReceiveAudio: true, // 告訴另一端鸦泳,你是否想接收音頻,默認(rèn)true
offerToReceiveVideo: true, // 告訴另一端永品,你是否想接收視頻做鹰,默認(rèn)true
iceRestart: false, // 是否在活躍狀態(tài)重啟ICE網(wǎng)絡(luò)協(xié)商
};
iceRestart:只有在處于活躍的時(shí)候,iceRestart=false才有作用鼎姐。
createAnswer
基本格式
aPromise = RTCPeerConnection .createAnswer([ options ])? 目前createAnswer的options是
無(wú)效的钾麸。
setLocalDescription
基本格式
aPromise = RTCPeerConnection .setLocalDescription(sessionDescription);
setRemoteDescription
基本格式
aPromise = pc.setRemoteDescription(sessionDescription);
1.3加入Stream/Track
addTrack
基本格式
rtpSender = rtcPeerConnection .addTrack(track,stream ...);
track:添加到RTCPeerConnection中的媒體軌(音頻track/視頻track)
stream:getUserMedia中拿到的流炕桨,指定track所在的stream
1.4網(wǎng)絡(luò)協(xié)商
addIceCandidate
基本格式
aPromise = pc.addIceCandidate(候選人);
candidate
屬性 | 說明 |
---|---|
candidate | 候選者描述信息 |
sdpMid | 與候選者相關(guān)的媒體流的識(shí)別標(biāo)簽 |
sdpMLineIndex | 在SDP中 m=的索引值 |
usernameFragment | 包括了遠(yuǎn)端的唯一標(biāo)識(shí) |
Android和Web端不同饭尝。
1.5RTCPeerConnection補(bǔ)充
1.5.1構(gòu)造函數(shù)
configuration可選
屬性說明
candidate 候選者描述信息
sdpMid 與候選者相關(guān)的媒體流的識(shí)別標(biāo)簽
sdpMLineIndex 在SDP中 m=的索引值
usernameFragment 包括了遠(yuǎn)端的唯一標(biāo)識(shí)
bundlePolicy 一般用maxbundle
banlanced:音頻與視頻軌使用各自的傳輸通道
maxcompat:
每個(gè)軌使用自己的傳輸通道
maxbundle:
都綁定到同一個(gè)傳輸通道
iceTransportPolicy 一般用all
指定ICE的傳輸策略
relay:只使用中繼候選者
all:可以使用任何類型的候選者
iceServers
其由RTCIceServer組成,每個(gè)RTCIceServer都是一個(gè)ICE代理的服務(wù)器
屬性 | 含義 |
---|---|
credential | 憑據(jù)献宫,只有TURN服務(wù)使用 |
credentialType | 憑據(jù)類型钥平,可以password或oauth |
urls | 用于連接服中的ur數(shù)組 |
username | 用戶名,只有TURN服務(wù)使用 |
rtcpMuxPolicy 一般用require
rtcp的復(fù)用策略姊途,該選項(xiàng)在收集ICE候選者時(shí)使用
屬性 | 含義 |
---|---|
negotiate | 收集RTCP與RTP復(fù)用的ICE候選者涉瘾,如果RTCP能復(fù)用就與RTP復(fù)用知态,如果不能復(fù)用,就將他們單獨(dú)使用 |
require | 只能收集RTCP與RTP復(fù)用的ICE候選者立叛,如果RTCP不能復(fù)用负敏,則失敗 |
1.5.2重要事件
onicecandidate 收到候選者時(shí)觸發(fā)的事件
ontrack 獲取遠(yuǎn)端流
onconnectionstatechange PeerConnection的連接狀態(tài),參考: https://developer.mozilla.org/enUS/
docs/Web/API/RTCPeerConnection/connectionState
pc.onconnectionstatechange = function(event) {
switch(pc.connectionState) {
case "connected":
// The connection has become fully connected
break;
case "disconnected":
case "failed":
// One or more transports has terminated unexpectedly or in an error
break;
case "closed":
// The connection has been closed
break;
}
}
oniceconnectionstatechange ice連接事件 具體參考:https://developer.mozilla.org/enUS/docs/Web/API/RTCPeerConnection/iceConnectionState
1.6實(shí)現(xiàn)WebRTC音視頻通話
開發(fā)步驟
- 客戶端顯示界面
- 打開攝像頭并顯示到頁(yè)面
- websocket連接
- join秘蛇、newpeer
其做、respjoin
信令實(shí)現(xiàn) - leave、peerleave
信令實(shí)現(xiàn) - offer赁还、answer妖泄、candidate信令實(shí)現(xiàn)
- 綜合調(diào)試和完善
1.6.1客戶端顯示界面
<!DOCTYPE html>
<link rel="shortcut icon" href="#">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>WebRTC Demo</title>
</head>
<h1>WebRTC Demo</h1>
<div id="buttons">
<input id="zero-RoomId" type="text" placeholder="請(qǐng)輸入房間ID" maxlength="40"/>
<button id="joinBtn" type="button">加入</button>
<button id="leaveBtn" type="button">離開</button>
</div>
<div id="videos">
<video id="localVideo" autoplay muted playsinline>本地窗口</video>
<video id="remoteVideo" autoplay playsinline>遠(yuǎn)端窗口</video>
</div>
<script src="js/main.js"></script>
<script src="js/adapter-latest.js"></script>
</html>
1.6.2打開攝像頭并顯示到界面
1.6.3WebSocket連接
fishRTCEngine = new FishRTCEngine("wss://192.168.1.102:8098/ws");
fishRTCEngine.createWebSocket();
FishRTCEngine.prototype.createWebSocket = function () {
fishRTCEngine = this;
fishRTCEngine.signaling = new WebSocket(this.wsUrl);
fishRTCEngine.signaling.onopen = function () {
fishRTCEngine.onOpen();
};
fishRTCEngine.signaling.onmessage = function (ev) {
fishRTCEngine.onMessage(ev);
};
fishRTCEngine.signaling.onerror = function (ev) {
fishRTCEngine.onError(ev);
};
fishRTCEngine.signaling.onclose = function (ev) {
fishRTCEngine.onClose(ev);
};
};
1.6.4 join、newpeer秽浇、respjoin信令實(shí)現(xiàn)
思路:(1)點(diǎn)擊加入開妞浮庐;
(2)響應(yīng)加入按鈕事件甚负;
(3)將join發(fā)送給服務(wù)器柬焕;
(4)服務(wù)器 根據(jù)當(dāng)前房間的人數(shù)
做處理,如果房間已經(jīng)有人則通知房間里面的人有新人加入(newpeer)梭域,并通知自己房間里面是什么人(respjoin)斑举。
1.6.5 leave、peerleave信令實(shí)現(xiàn)
思路:(1)點(diǎn)擊離開按鈕病涨;
(2)響應(yīng)離開按鈕事件富玷;
(3)將leave發(fā)送給服務(wù)器;
(4)服務(wù)器處理leave既穆,將發(fā)送者刪除并通知房間(peerleave)的其他人赎懦;
(5)房間的其他人在客戶端響應(yīng)peerleave事件。
// One or more transports has terminated unexpectedly or in an error
break;
case "closed":
// The connection has been closed
break;
}
}
1.6.6 offer幻工、answer励两、candidate信令實(shí)現(xiàn)
思路:
(1)收到newpeer
(handleRemoteNewPeer處理),作為發(fā)起者創(chuàng)建RTCPeerConnection囊颅,綁定事件響應(yīng)函數(shù)当悔,
加入本地流;
(2)創(chuàng)建offer sdp踢代,設(shè)置本地sdp盲憎,并將offer sdp發(fā)送到服務(wù)器;
(3)服務(wù)器收到offer sdp 轉(zhuǎn)發(fā)給指定的remoteClient胳挎;
(4)接收者收到offer饼疙,也創(chuàng)建RTCPeerConnection,綁定事件響應(yīng)函數(shù)慕爬,加入本地流宏多;
(5)接收者設(shè)置遠(yuǎn)程sdp儿惫,并創(chuàng)建answer sdp,然后設(shè)置本地sdp并將answer sdp發(fā)送到服務(wù)器伸但;
(6)服務(wù)器收到answer sdp 轉(zhuǎn)發(fā)給指定的remoteClient肾请;
(7)發(fā)起者收到answer sdp,則設(shè)置遠(yuǎn)程sdp更胖;
(8)發(fā)起者和接收者都收到ontrack回調(diào)事件铛铁,獲取到對(duì)方碼流的對(duì)象句柄;
(9)發(fā)起者和接收者都開始請(qǐng)求打洞却妨,通過onIceCandidate獲取到打洞信息(candidate)并發(fā)送給對(duì)方
(10)如果P2P能成功則進(jìn)行P2P通話饵逐,如果P2P不成功則進(jìn)行中繼轉(zhuǎn)發(fā)通話。
1.6.7 綜合調(diào)試和完善
思路:
(1)點(diǎn)擊離開時(shí)彪标,要將RTCPeerConnection關(guān)閉(close)倍权;
(2)點(diǎn)擊離開時(shí),要將本地?cái)z像頭和麥克風(fēng)關(guān)閉捞烟;
(3)檢測(cè)到客戶端退出時(shí)薄声,服務(wù)器再次檢測(cè)該客戶端是否已經(jīng)退出房間。
(4)RTCPeerConnection時(shí)傳入ICE server的參數(shù)题画,以便當(dāng)在公網(wǎng)環(huán)境下可以進(jìn)行正常通話默辨。
客戶端代碼
"use strict";
// join 主動(dòng)加入房間
// leave 主動(dòng)離開房間
// new-peer 有人加入房間,通知已經(jīng)在房間的人
// peer-leave 有人離開房間苍息,通知已經(jīng)在房間的人
// offer 發(fā)送offer給對(duì)端peer
// answer發(fā)送offer給對(duì)端peer
// candidate 發(fā)送candidate給對(duì)端peer
const SIGNAL_TYPE_JOIN = "join";
const SIGNAL_TYPE_RESP_JOIN = "resp-join"; // 告知加入者對(duì)方是誰(shuí)
const SIGNAL_TYPE_LEAVE = "leave";
const SIGNAL_TYPE_NEW_PEER = "new-peer";
const SIGNAL_TYPE_PEER_LEAVE = "peer-leave";
const SIGNAL_TYPE_OFFER = "offer";
const SIGNAL_TYPE_ANSWER = "answer";
const SIGNAL_TYPE_CANDIDATE = "candidate";
var localUserId = Math.random().toString(36).substr(2); //本地uid
var remoteUserId = -1; //對(duì)端uid
var roomId = 0;
var localVideo = document.querySelector("#localVideo");
var remoteVideo = document.querySelector("#remoteVideo");
var localStream = null;
var remoteStream = null;
var pc = null; //RTCPeerConnection
var fishRTCEngine;
function handleIceCandidate(event) {
console.info("handleIceCandidate");
if (event.candidate) {
//不為空才發(fā)送candidate
var jsonMsg = {
cmd: "candidate",
roomId: roomId,
uid: localUserId,
remoteUid: remoteUserId,
msg: JSON.stringify(event.candidate),
};
var message = JSON.stringify(jsonMsg);
fishRTCEngine.sendMessage(message);
// console.info("handleIceCandidate message: "+message);
console.info("send Candidate message:");
} else {
//不再去請(qǐng)求打洞了
console.warn("End of candidates");
}
}
function handleRemoteStreamAdd(event) {
console.info("handleRemoteStreamAdd");
remoteStream = event.streams[0];
remoteVideo.srcObject = remoteStream;
}
function handleConnectionStateChange(){
if(pc != null){
console.info("handleConnectionStateChange: " + pc.connectionState);
}
}
function handleIceConnectionStateChange(){
if(pc != null){
console.info("handleIceConnectionStateChange: " + pc.iceConnectionState);
}
}
function createPeerConnection() {
var defaultConfiguration = {
bundlePolicy: "max-bundle",
rtcpMuxPolicy: "require",
iceTransportPolicy: "relay", //relay or all
// 修改ice數(shù)組測(cè)試效果缩幸,需要進(jìn)行封裝
iceServers: [
{
urls: [
"turn:192.168.1.102:3478?transport=udp",
"turn:192.168.1.102:3478?transport=tcp", // 可以插入多個(gè)進(jìn)行備選
],
username: "ydy",
credential: "123456",
},
{
urls: ["stun:192.168.1.102:3478"],
},
],
};
pc = new RTCPeerConnection(defaultConfiguration);
pc.onicecandidate = handleIceCandidate;
pc.ontrack = handleRemoteStreamAdd;
pc.oniceconnectionstatechange = handleIceConnectionStateChange;
pc.onconnectionstatechange = handleConnectionStateChange;
localStream.getTracks().forEach((track) => pc.addTrack(track, localStream));
}
function createOfferAndSendMessage(session) {
pc.setLocalDescription(session)
.then(function () {
var jsonMsg = {
cmd: "offer",
roomId: roomId,
uid: localUserId,
remoteUid: remoteUserId,
msg: JSON.stringify(session),
};
var message = JSON.stringify(jsonMsg);
fishRTCEngine.sendMessage(message);
// console.info("send offer message: "+message);
console.info("send offer message: ");
})
.catch(function (error) {
console.error("offer setLocalDescription failed: " + error);
});
}
function handleCreateOfferError(error) {
console.error("handleCreateOfferError failed: " + error);
}
function createAnswerAndSendMessage(session) {
console.info("doAnswer createAnswerAndSendMessage");
pc.setLocalDescription(session)
.then(function () {
var jsonMsg = {
cmd: "answer",
roomId: roomId,
uid: localUserId,
remoteUid: remoteUserId,
msg: JSON.stringify(session),
};
var message = JSON.stringify(jsonMsg);
fishRTCEngine.sendMessage(message);
console.info("send answer message: ");
// console.info("send answer message: "+message);
})
.catch(function (error) {
console.error("answer setLocalDescription failed: " + error);
});
}
function handleCreateAnswerError(error) {
console.error("handleCreateAnswerError failed: " + error);
}
var FishRTCEngine = function (wsUrl) {
this.init(wsUrl);
fishRTCEngine = this;
return this;
};
FishRTCEngine.prototype.init = function (wsUrl) {
//設(shè)置wbsocket url
this.wsUrl = wsUrl;
//websocket對(duì)象
this.signaling = null;
};
FishRTCEngine.prototype.createWebSocket = function () {
fishRTCEngine = this;
fishRTCEngine.signaling = new WebSocket(this.wsUrl);
fishRTCEngine.signaling.onopen = function () {
fishRTCEngine.onOpen();
};
fishRTCEngine.signaling.onmessage = function (ev) {
fishRTCEngine.onMessage(ev);
};
fishRTCEngine.signaling.onerror = function (ev) {
fishRTCEngine.onError(ev);
};
fishRTCEngine.signaling.onclose = function (ev) {
fishRTCEngine.onClose(ev);
};
};
FishRTCEngine.prototype.onOpen = function () {
console.log("websocket open");
};
FishRTCEngine.prototype.onMessage = function (event) {
// console.info("websocket onMessage:");
console.log("websocket onMessage:" + event.data);
var jsonMsg = null;
try {
jsonMsg = JSON.parse(event.data);
} catch (e) {
console.warn("onMessage parse Json failed: " + e);
return;
}
switch (jsonMsg.cmd) {
case SIGNAL_TYPE_NEW_PEER:
handleRemoteNewPeer(jsonMsg);
break;
case SIGNAL_TYPE_RESP_JOIN:
handleResponseJoin(jsonMsg);
break;
case SIGNAL_TYPE_PEER_LEAVE:
handleRemotePeerLeave(jsonMsg);
break;
case SIGNAL_TYPE_OFFER:
handleRemoteOffer(jsonMsg);
break;
case SIGNAL_TYPE_ANSWER:
handleRemoteAnswer(jsonMsg);
break;
case SIGNAL_TYPE_CANDIDATE:
handleRemoteCandidate(jsonMsg);
break;
}
};
FishRTCEngine.prototype.onError = function (event) {
console.log("websocket onError" + event.data);
};
FishRTCEngine.prototype.onClose = function (event) {
console.log(
"websocket onClose code:" + event.code + ",reason:" + EventTarget.reason
);
};
FishRTCEngine.prototype.sendMessage = function (message) {
this.signaling.send(message);
};
function handleResponseJoin(message) {
console.info("handleResponseJoin, remoteUid: " + message.remoteUid);
remoteUserId = message.remoteUid;
//doOffer();
}
function handleRemotePeerLeave(message) {
console.info("handleRemotePeerLeave, remoteUid: " + message.remoteUid);
remoteVideo.srcObject = null; //遠(yuǎn)程對(duì)象置空
if (pc != null) {
pc.close();
pc = null;
}
}
//新人加入房間保存userId
function handleRemoteNewPeer(message) {
console.info("handleRemoteNewPeer, remoteUid: " + message.remoteUid);
remoteUserId = message.remoteUid;
doOffer();
}
function handleRemoteOffer(message) {
console.info("handleRemoteOffer");
if (pc == null) {
createPeerConnection();
}
var desc = JSON.parse(message.msg);
pc.setRemoteDescription(desc);
doAnswer();
}
function handleRemoteAnswer(message) {
console.info("handleRemoteAnswer");
var desc = JSON.parse(message.msg);
// console.info("desc: " + desc);
pc.setRemoteDescription(desc);
}
function handleRemoteCandidate(message) {
console.info("handleRemoteCandidate");
var candidate = JSON.parse(message.msg);
pc.addIceCandidate(candidate).catch((e) => {
console.error("addIceCandidate failed: " + e.name);
});
}
function doOffer() {
//創(chuàng)建RCTPeerConnection
if (pc == null) {
createPeerConnection();
}
pc.createOffer()
.then(createOfferAndSendMessage)
.catch(handleCreateOfferError);
}
function doAnswer() {
console.info("doAnswer");
pc.createAnswer()
.then(createAnswerAndSendMessage)
.catch(handleCreateAnswerError);
}
function doJoin(roomId) {
console.info("doJoin roomId:" + roomId);
var jsonMsg = {
cmd: "join",
roomId: roomId,
uid: localUserId,
};
var message = JSON.stringify(jsonMsg);
fishRTCEngine.sendMessage(message);
console.info("doJoin message: " + message);
}
function doLeave() {
var jsonMsg = {
cmd: "leave",
roomId: roomId,
uid: localUserId,
};
var message = JSON.stringify(jsonMsg);
fishRTCEngine.sendMessage(message); //發(fā)信令給服務(wù)器離開
console.info("doLeave message: " + message);
hangup(); //掛斷
}
function hangup() {
localVideo.srcObject = null; //0.關(guān)閉自己的本地顯示
remoteVideo.srcObject = null; //1.關(guān)閉遠(yuǎn)端的流
closeLocalStream(); //2.關(guān)閉本地流,攝像頭關(guān)閉竞思,麥克風(fēng)關(guān)閉
if (pc != null) {
//3.關(guān)閉RTCPeerConnection
pc.close();
pc = null;
}
}
function closeLocalStream() {
if (localStream != null) {
localStream.getTracks().forEach((track) => {
track.stop();
});
}
}
function openLocalStream(stream) {
console.log("Open Local stream");
doJoin(roomId);
localVideo.srcObject = stream;
localStream = stream;
}
function initLocalStream() {
navigator.mediaDevices
.getUserMedia({
audio: true,
// video: true,
video:{
width:640,
height:480
}
})
.then(openLocalStream)
.catch(function (e) {
alert("getUserMedia() error" + e.name);
});
}
fishRTCEngine = new FishRTCEngine("wss://192.168.1.102:8098/ws");
fishRTCEngine.createWebSocket();
document.getElementById("joinBtn").onclick = function () {
roomId = document.getElementById("zero-RoomId").value;
if (roomId == "" || roomId == "請(qǐng)輸入房間ID") {
alert("請(qǐng)輸入房間ID");
return;
}
console.log("加入按鈕被點(diǎn)擊,roomId:" + roomId);
//初始化本地碼流
initLocalStream();
};
document.getElementById("leaveBtn").onclick = function () {
console.log("離開按鈕被點(diǎn)擊");
doLeave();
};
服務(wù)端代碼
var ws = require("nodejs-websocket")
var port = 8099;
// join 主動(dòng)加入房間
// leave 主動(dòng)離開房間
// new-peer 有人加入房間表谊,通知已經(jīng)在房間的人
// peer-leave 有人離開房間,通知已經(jīng)在房間的人
// offer 發(fā)送offer給對(duì)端peer
// answer發(fā)送offer給對(duì)端peer
// candidate 發(fā)送candidate給對(duì)端peer
const SIGNAL_TYPE_JOIN = "join";
const SIGNAL_TYPE_RESP_JOIN = "resp-join"; // 告知加入者對(duì)方是誰(shuí)
const SIGNAL_TYPE_LEAVE = "leave";
const SIGNAL_TYPE_NEW_PEER = "new-peer";
const SIGNAL_TYPE_PEER_LEAVE = "peer-leave";
const SIGNAL_TYPE_OFFER = "offer";
const SIGNAL_TYPE_ANSWER = "answer";
const SIGNAL_TYPE_CANDIDATE = "candidate";
/** ----- ZeroRTCMap ----- */
var ZeroRTCMap = function () {
this._entrys = new Array();
// 插入
this.put = function (key, value) {
if (key == null || key == undefined) {
return;
}
var index = this._getIndex(key);
if (index == -1) {
var entry = new Object();
entry.key = key;
entry.value = value;
this._entrys[this._entrys.length] = entry;
} else {
this._entrys[index].value = value;
}
};
// 根據(jù)key獲取value
this.get = function (key) {
var index = this._getIndex(key);
return (index != -1) ? this._entrys[index].value : null;
};
// 移除key-value
this.remove = function (key) {
var index = this._getIndex(key);
if (index != -1) {
this._entrys.splice(index, 1);
}
};
// 清空map
this.clear = function () {
this._entrys.length = 0;
};
// 判斷是否包含key
this.contains = function (key) {
var index = this._getIndex(key);
return (index != -1) ? true : false;
};
// map內(nèi)key-value的數(shù)量
this.size = function () {
return this._entrys.length;
};
// 獲取所有的key
this.getEntrys = function () {
return this._entrys;
};
// 內(nèi)部函數(shù)
this._getIndex = function (key) {
if (key == null || key == undefined) {
return -1;
}
var _length = this._entrys.length;
for (var i = 0; i < _length; i++) {
var entry = this._entrys[i];
if (entry == null || entry == undefined) {
continue;
}
if (entry.key === key) {// equal
return i;
}
}
return -1;
};
}
//總的房間號(hào)
var roomTableMap = new ZeroRTCMap();
function Client(uid,conn,roomId){
this.uid = uid;//用戶所屬的id
this.conn = conn;//uid對(duì)應(yīng)的websocket連接
this.roomId = roomId;//用戶所在的房間
}
function handleJoin(message,conn){
var roomId = message.roomId;
var uid = message.uid;
console.info("uid" + uid + " try to join roomId: " + roomId);
//查找房間目前是否已經(jīng)存在了
var roomMap = roomTableMap.get(roomId);
if(roomMap == null){//房間不存在
roomMap = new ZeroRTCMap();
roomTableMap.put(roomId,roomMap);
}
//房間已經(jīng)有兩個(gè)人了
if(roomMap.size() >= 2){
console.error("roomId:" + roomId + "已經(jīng)有兩個(gè)人存在,請(qǐng)使用其他房間");
//加信令通知客戶端盖喷,房間已滿
return null;
}
var client = new Client(uid,conn,roomId);
roomMap.put(uid,client);
if(roomMap.size() > 1){
//房間里面已經(jīng)有人了爆办,加上新進(jìn)來的人,那就是>=2了传蹈,所以要通知對(duì)方
var clients = roomMap.getEntrys();
for(var i in clients){
var remoteUid = clients[i].key;
if(remoteUid != uid){
var jsonMsg = {
'cmd':SIGNAL_TYPE_NEW_PEER,
'remoteUid':uid
};
var msg = JSON.stringify(jsonMsg);
var remoteClient = roomMap.get(remoteUid);
console.info("new-peer: " + msg);
//新加入人之后押逼,重新通知遠(yuǎn)程的對(duì)方
remoteClient.conn.sendText(msg);
jsonMsg = {
'cmd':SIGNAL_TYPE_RESP_JOIN,
'remoteUid':remoteUid
};
msg = JSON.stringify(jsonMsg);
console.info("resp-join: " + msg);
//新加入人之后,通知自己惦界,有人加入了
conn.sendText(msg);
}
}
}
return client;
}
function handleLeave(message){
var roomId = message.roomId;
var uid = message.uid;
console.info("handleLeave uid:" + uid + " leave roomId: " + roomId);
//查找房間目前是否已經(jīng)存在了
var roomMap = roomTableMap.get(roomId);
if(roomMap == null){//房間不存在
console.warn("can't find the roomId: " + roomId);
return;
}
roomMap.remove(uid);//刪除發(fā)送者
//退出房間通知其他人
if(roomMap.size() >= 1){
var clients = roomMap.getEntrys();
for(var i in clients){
var jsonMsg = {
'cmd': SIGNAL_TYPE_PEER_LEAVE,
'remoteUid': uid//誰(shuí)離開就填寫誰(shuí)
};
var msg = JSON.stringify(jsonMsg);
var remoteUid = clients[i].key;
var remoteClient = roomMap.get(remoteUid);
if(remoteClient){
//通知此uid離開了房間
console.info("notify peer:" + remoteClient.uid + " , uid: " + uid + " leave");
remoteClient.conn.sendText(msg);
}
}
}
}
function handleForceLeave(client){
var roomId = client.roomId;
var uid = client.uid;
//1.先查找房間號(hào)
var roomMap = roomTableMap.get(roomId);
if(roomMap == null){//房間不存在
console.warn("handleForceLeave can't find the roomId: " + roomId);
return;
}
//2.判斷uid是否在房間
if(!roomMap.contains(uid)){
console.info("uid: " + uid + " have leave roomId: " + roomId);
return;
}
//3.走到這一步挑格,客戶端沒有正常離開,我們要執(zhí)行離開程序
console.info("handleForceLeave uid:" + uid + " force leave roomId: " + roomId);
roomMap.remove(uid);//刪除發(fā)送者
//退出房間通知其他人
if(roomMap.size() >= 1){
var clients = roomMap.getEntrys();
for(var i in clients){
var jsonMsg = {
'cmd': SIGNAL_TYPE_PEER_LEAVE,
'remoteUid': uid//誰(shuí)離開就填寫誰(shuí)
};
var msg = JSON.stringify(jsonMsg);
var remoteUid = clients[i].key;
var remoteClient = roomMap.get(remoteUid);
if(remoteClient){
//通知此uid離開了房間
console.info("notify peer:" + remoteClient.uid + " , uid: " + uid + " leave");
remoteClient.conn.sendText(msg);
}
}
}
}
function handleOffer(message){
var roomId = message.roomId;
var uid = message.uid;
var remoteUid = message.remoteUid;
console.info("handleOffer uid: " + uid + " transfer offer to remoteUid: " + remoteUid);
var roomMap = roomTableMap.get(roomId);
if(roomMap == null){//房間不存在
console.error("handleOffer can't find the roomId: " + roomId);
return;
}
if(roomMap.get(uid) == null){//人不存在
console.error("handleOffer can't find the uid: " + uid);
return;
}
var remoteClient = roomMap.get(remoteUid);
if(remoteClient){
var msg = JSON.stringify(message);
remoteClient.conn.sendText(msg);
}else{
console.error("handleOffer can't find remoteUid: " + remoteUid);
}
}
function handleAnswer(message){
var roomId = message.roomId;
var uid = message.uid;
var remoteUid = message.remoteUid;
console.info("handleAnswer uid: " + uid + " transfer answer to remoteUid: " + remoteUid);
var roomMap = roomTableMap.get(roomId);
if(roomMap == null){//房間不存在
console.error("handleAnswer can't find the roomId: " + roomId);
return;
}
if(roomMap.get(uid) == null){//人不存在
console.error("handleAnswer can't find the uid: " + uid);
return;
}
var remoteClient = roomMap.get(remoteUid);
if(remoteClient){
var msg = JSON.stringify(message);
remoteClient.conn.sendText(msg);
}else{
console.error("handleAnswer can't find remoteUid: " + remoteUid);
}
}
function handleCandidate(message){
var roomId = message.roomId;
var uid = message.uid;
var remoteUid = message.remoteUid;
console.info("handleCandidate uid: " + uid + " transfer candidate to remoteUid: " + remoteUid);
var roomMap = roomTableMap.get(roomId);
if(roomMap == null){//房間不存在
console.error("handleCandidate can't find the roomId: " + roomId);
return;
}
if(roomMap.get(uid) == null){//人不存在
console.error("handleCandidate can't find the uid: " + uid);
return;
}
var remoteClient = roomMap.get(remoteUid);
if(remoteClient){
var msg = JSON.stringify(message);
remoteClient.conn.sendText(msg);
}else{
console.error("handleCandidate can't find remoteUid: " + remoteUid);
}
}
var server = ws.createServer(function(conn){
console.log("創(chuàng)建一個(gè)新的連接---------")
conn.client = null;//對(duì)應(yīng)的客戶端信息
// conn.sendText("我收到你的連接了......");
conn.on("text",function(str){
// console.info("recv msg:" + str);
var jsonMsg = JSON.parse(str);
switch(jsonMsg.cmd){
case SIGNAL_TYPE_JOIN:
conn.client = handleJoin(jsonMsg,conn);
break;
case SIGNAL_TYPE_LEAVE:
handleLeave(jsonMsg);
break;
case SIGNAL_TYPE_OFFER:
handleOffer(jsonMsg);
break;
case SIGNAL_TYPE_ANSWER:
handleAnswer(jsonMsg);
break;
case SIGNAL_TYPE_CANDIDATE:
handleCandidate(jsonMsg);
break;
}
});
conn.on("close",function(code,reason){
console.info("連接關(guān)閉 code: " + code + ", reason: " + reason);
if(conn.client != null){
//強(qiáng)制讓客戶端從房間退出
handleForceLeave(conn.client);
}
});
conn.on("error",function(err){
console.info("監(jiān)聽到錯(cuò)誤:" + err);
});
}).listen(port);
啟動(dòng)coturn
# nohup是重定向命令沾歪,輸出都將附加到當(dāng)前目錄的 nohup.out 文件中漂彤; 命令后加 & ,后臺(tái)執(zhí)行起來后按
ctr+c,不會(huì)停止
sudo nohup turnserver ‐L 0.0.0.0 ‐a ‐u lqf:123456 ‐v ‐f ‐r nort.gov &
# 前臺(tái)啟動(dòng)
sudo turnserver ‐L 0.0.0.0 ‐a ‐u lqf:123456 ‐v ‐f ‐r nort.gov
#然后查看相應(yīng)的端口號(hào)3478是否存在進(jìn)程
sudo lsof ‐i:3478
設(shè)置configuration,先設(shè)置為relay中繼模式,只有relay中繼模式可用的時(shí)候挫望,才能部署到公網(wǎng)去(部署到公網(wǎng)后也先測(cè)試relay)立润。
function createPeerConnection() {
var defaultConfiguration = {
bundlePolicy: "max-bundle",
rtcpMuxPolicy: "require",
iceTransportPolicy: "relay", //relay or all
// 修改ice數(shù)組測(cè)試效果,需要進(jìn)行封裝
iceServers: [
{
urls: [
"turn:192.168.1.102:3478?transport=udp",
"turn:192.168.1.102:3478?transport=tcp", // 可以插入多個(gè)進(jìn)行備選
],
username: "ydy",
credential: "123456",
},
{
urls: ["stun:192.168.1.102:3478"],
},
],
};
pc = new RTCPeerConnection(defaultConfiguration);
pc.onicecandidate = handleIceCandidate;
pc.ontrack = handleRemoteStreamAdd;
pc.oniceconnectionstatechange = handleIceConnectionStateChange;
pc.onconnectionstatechange = handleConnectionStateChange;
localStream.getTracks().forEach((track) => pc.addTrack(track, localStream));
}
all模式:局域網(wǎng)可用優(yōu)先走局域網(wǎng)媳板,通過命令sar -n DEV 1
查看一秒鐘的網(wǎng)絡(luò)傳輸量發(fā)現(xiàn)為0
relay模式:走中繼服務(wù)器模式桑腮,不會(huì)走局域網(wǎng),網(wǎng)絡(luò)傳輸量不為0
編譯和啟動(dòng)nginx
sudo apt‐get update
#安裝依賴:gcc蛉幸、g++依賴庫(kù)
sudo apt‐get install build‐essential libtool
#安裝 pcre依賴庫(kù)(http://www.pcre.org/)
sudo apt‐get install libpcre3 libpcre3‐dev
#安裝 zlib依賴庫(kù)(http://www.zlib.net)
sudo apt‐get install zlib1g‐dev
#安裝ssl依賴庫(kù)
sudo apt‐get install openssl
#下載nginx 1.20.1版本
wget http://nginx.org/download/nginx‐1.15.8.tar.gz
tar xvzf nginx‐1.20.1.tar.gz
cd nginx‐1.15.8/
# 配置破讨,一定要支持https
./configure ‐‐with‐http_ssl_module
# 編譯
make
#安裝
sudo make install
默認(rèn)安裝目錄:/usr/local/nginx
啟動(dòng):sudo /usr/local/nginx/sbin/nginx
停止:sudo /usr/local/nginx/sbin/nginx s
stop
重新加載配置文件:sudo /usr/local/nginx/sbin/nginx s
reload
生成證書
mkdir ‐p ~/cert
cd ~/cert
# CA私鑰
openssl genrsa ‐out key.pem 2048
# 自簽名證書
openssl req ‐new ‐x509 ‐key key.pem ‐out cert.pem ‐days 1095
配置Web服務(wù)器
(1)配置自己的證書
ssl_certificate /root/cert/cert.pem? // 注意證書所在的路徑
ssl_certificate_key /root/cert/key.pem?
(2)配置主機(jī)域名或者主機(jī)IP server_name 192.168.1.103?
(3)web頁(yè)面所在目錄root /mnt/WebRTC/src/04/6.4/client?
完整配置文件:/usr/local/nginx/conf/conf.d/webrtchttps.conf
server {
listen 443 ssl;
ssl_certificate /root/cert/cert.pem;
ssl_certificate_key /root/cert/key.pem;
charset utf‐8;
# ip地址或者域名
server_name 192.168.1.103;
location / {
add_header 'Access‐Control‐Allow‐Origin' '*';
add_header 'Access‐Control‐Allow‐Credentials' 'true';
add_header 'Access‐Control‐Allow‐Methods' '*';
add_header 'Access‐Control‐Allow‐Headers' 'Origin, X‐Requested‐With, Content‐Type,
Accept';
# web頁(yè)面所在目錄
root /mnt/WebRTC/src/04/6.4/client;
index index.php index.html index.htm;
}
}
編輯nginx.conf文件,在末尾}之前添加包含文件include /usr/local/nginx/conf/conf.d/*.conf;
配置websocket代理
ws 不安全的連接 類似http
wss是安全的連接奕纫,類似https
https不能訪問ws提陶,本身是安全的訪問,不能降級(jí)做不安全的訪問匹层。
ws協(xié)議和wss協(xié)議兩個(gè)均是WebSocket協(xié)議的SCHEM,兩者一個(gè)是非安全的,一個(gè)是安全的隙笆。也是統(tǒng)一的資源標(biāo)志
符。就好比HTTP協(xié)議和HTTPS協(xié)議的差別升筏。
Nginx主要是提供wss連接的支持撑柔,https必須調(diào)用wss的連接。
完整配置文件:/usr/local/nginx/conf/conf.d/webrtcwebsocketproxy.
conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream websocket {
server 192.168.1.103:8099;
}
server {
listen 8098 ssl;
#ssl on;
ssl_certificate /root/cert/cert.pem;
ssl_certificate_key /root/cert/key.pem;
server_name 192.168.1.103;
location /ws {
proxy_pass http://websocket;
proxy_http_version 1.1;
keepalive_timeout 6000000000s;
proxy_connect_timeout 400000000s; #配置點(diǎn)1
proxy_read_timeout 60000000s; #配置點(diǎn)2仰冠,如果沒效乏冀,可以考慮這個(gè)時(shí)間配置長(zhǎng)一點(diǎn)
proxy_send_timeout 60000000s; #配置點(diǎn)3
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
wss://192.168.221.134:8098/ws 端口是跟著IP后面
信令服務(wù)器后臺(tái)執(zhí)行nohup node ./signal_server.js &
如果退出終端信令服務(wù)器會(huì)停止蝶糯,需exit退出終端或安裝forever和pm2,才能保持服務(wù)器在后臺(tái)執(zhí)行
解決websocket自動(dòng)斷開(這是重點(diǎn)Q笾弧!V绾础J缎椤!設(shè)置超時(shí)時(shí)間無(wú)效妒茬。担锤。。)
我們?cè)谕ㄔ挄r(shí)乍钻,出現(xiàn)60秒后客戶端自動(dòng)斷開的問題肛循,是因?yàn)榻?jīng)過nginx代理時(shí),如果websocket長(zhǎng)時(shí)間沒有收發(fā)消息
則該websocket將會(huì)被斷開银择。我們這里可以修改收發(fā)消息的時(shí)間間隔多糠。
proxy_connect_timeout :后端服務(wù)器連接的超時(shí)時(shí)間發(fā)起握手等候響應(yīng)超時(shí)時(shí)間
proxy_read_timeout:連接成功后等候后端服務(wù)器響應(yīng)時(shí)間其實(shí)已經(jīng)進(jìn)入后端的排隊(duì)之中等候處理(也可以說是
后端服務(wù)器處理請(qǐng)求的時(shí)間)
proxy_send_timeout :后端服務(wù)器數(shù)據(jù)回傳時(shí)間就是在規(guī)定時(shí)間之內(nèi)后端服務(wù)器必須傳完所有的數(shù)據(jù)
nginx使用proxy模塊時(shí),默認(rèn)的讀取超時(shí)時(shí)間是60s
完整配置文件:/usr/local/nginx/conf/conf.d/webrtcwebsocketproxy.conf
心跳(待補(bǔ)充)維持心跳才能保證WebSocket連接不會(huì)被斷開浩考,前面設(shè)置超時(shí)時(shí)間都無(wú)效夹孔,90秒后WebSocket連接還是會(huì)斷開
客戶端 服務(wù)器 信令:心跳包
keeplive 間隔5秒發(fā)送一次給信令服務(wù)器,說明客戶端一直處于活躍的狀態(tài)。