柚子快報(bào)邀請(qǐng)碼778899分享:1V1音視頻實(shí)時(shí)互動(dòng)直播系統(tǒng)
柚子快報(bào)邀請(qǐng)碼778899分享:1V1音視頻實(shí)時(shí)互動(dòng)直播系統(tǒng)
李超老師的項(xiàng)目
先肯定分為兩個(gè)兩個(gè)端,一個(gè)是服務(wù)器端一個(gè)是客戶端??蛻舳擞糜赨I界面的顯示,服務(wù)器端用于處理客戶端發(fā)來(lái)的消息。
我們先搭建stun和turn服務(wù)器
首先介紹一下什么是stun協(xié)議,
它是用來(lái)干什么的?
stun協(xié)議存在的目的就是進(jìn)行NAT穿越。stun是典型的客戶端、服務(wù)器模式??蛻舳税l(fā)送請(qǐng)求,服務(wù)器進(jìn)行響應(yīng)。
那么是什么NAT穿越呢?
首先我們先了解一下為什么要進(jìn)行NAT穿越。下面舉個(gè)例子,在兩個(gè)瀏覽器之間進(jìn)行實(shí)時(shí)的音視頻互動(dòng),對(duì)于底層來(lái)說(shuō),這就是兩個(gè)端點(diǎn)之間進(jìn)行高效的網(wǎng)絡(luò)傳輸。
為了解決音視頻網(wǎng)絡(luò)傳輸?shù)膯?wèn)題,webrtc引入了一些網(wǎng)絡(luò)傳輸協(xié)議。
1.NAT:那么此時(shí)我們介紹NAT,簡(jiǎn)單理解為將內(nèi)網(wǎng)的地址轉(zhuǎn)換為公網(wǎng)的地址,內(nèi)網(wǎng)地址無(wú)法通訊,通過(guò)NAT轉(zhuǎn)換為公網(wǎng)之后,才有通信的可能。
2.說(shuō)到這里那么順便介紹一下stun,這個(gè)stun充當(dāng)?shù)氖侵薪榈淖饔?,在NAT的基礎(chǔ)上,交換兩個(gè)公網(wǎng)的信息,使得;兩個(gè)公網(wǎng)之間可以建立連接。
3.turn,stun是有一定幾率是不成功的,因此turn會(huì)在云端架設(shè)一個(gè)服務(wù)器,在p2p連接不成功的情況下,保證音視頻的互通,它就相當(dāng)于一個(gè)中轉(zhuǎn)站。
4.ICE, 羅列所有通信可能性,選擇最優(yōu)解。
NAT又分為四種類型:
完全錐型
地址限制錐型
端口限制錐型
對(duì)稱型
根據(jù)圖中所示很好理解,caller與信令服務(wù)器連接,同時(shí)callee也跟信令服務(wù)器連接,第二個(gè)callee也跟信令服務(wù)器連接;caller向信令服務(wù)器發(fā)出join請(qǐng)求,信令服務(wù)器響應(yīng)返回joined信息,第一個(gè)callee同理,但是第二個(gè)callee發(fā)送完join之后服務(wù)器發(fā)現(xiàn)成員已滿,因此返回一個(gè)full信息,該callee不能join。然后caller和callee進(jìn)行媒體協(xié)商,協(xié)商成功之后進(jìn)行媒體流的數(shù)據(jù)傳輸。之后callee主動(dòng)發(fā)出leave請(qǐng)求,服務(wù)器響應(yīng)跟caller發(fā)出bye信息并返回callee leaved信息。
我們來(lái)看一下服務(wù)器端的代碼。主要就是處理客戶端發(fā)來(lái)的信令消息。也就是以上的流程轉(zhuǎn)換為代碼。
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 user number of room is: ' + users);
if(users < USERCOUNT){
socket.emit('joined', room, socket.id); //發(fā)給自己
if(users > 1){
socket.to(room).emit('otherjoin', room, socket.id);//發(fā)給除自己之外的房間內(nèi)的所有人
}
}else{
socket.leave(room);
socket.emit('full', room, socket.id);
}
//socket.emit('joined', room, socket.id); //發(fā)給自己
//socket.broadcast.emit('joined', room, socket.id); //發(fā)給除自己之外的這個(gè)節(jié)點(diǎn)上的所有人
//io.in(room).emit('joined', room, socket.id); //發(fā)給房間內(nèi)的所有人
});
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.emit('leaved', room, socket.id);
//socket.broadcast.emit('leaved', room, socket.id);
socket.to(room).emit('bye', room, socket.id);
socket.emit('leaved', room, socket.id);
//io.in(room).emit('leaved', room, socket.id);
});
});
iceRestart是一個(gè)很好的方案,能夠幫助我們重新選擇數(shù)據(jù)傳輸?shù)木€路
下面了解一下?tīng)顟B(tài)機(jī)是什么這點(diǎn)蠻重要
下面介紹一下客戶端代碼編寫(xiě)的流程圖
首先寫(xiě)函數(shù)start
start函數(shù)用于采集音視頻數(shù)據(jù)
采集成功則與信令服務(wù)器連接,并注冊(cè)信令函數(shù)
注冊(cè)以上這些消息的處理函數(shù)
如果是joined則設(shè)置狀態(tài)為joined,創(chuàng)建PC并綁定媒體流。
如果是otherjoin,則判斷自身的狀態(tài)是否為joined_unbind,如果是則需要重新創(chuàng)建PC并綁定媒體流并將狀態(tài)設(shè)置為joined_conn,如果一開(kāi)始狀態(tài)為joined則直接將狀態(tài)轉(zhuǎn)換為joined_conn,接著開(kāi)始媒體協(xié)商。
如果是full則狀態(tài)設(shè)置為full并關(guān)閉PC,并關(guān)閉本地媒體流
客戶端的實(shí)現(xiàn)需要注意的幾點(diǎn)是
1.網(wǎng)絡(luò)連接要在音視頻數(shù)據(jù)獲取到之后,否則可能導(dǎo)致綁定音視頻流失敗
2.當(dāng)一端退出房間之后,另一端的PeerConnection要關(guān)閉重建,否則與新用戶互通時(shí)媒體協(xié)商會(huì)失敗。
3.所有的處理流程為異步處理
這里要了解一下什么是異步處理:
異步事件處理:要等待收到一個(gè)消息或事件后,才能做下一步的操作
同步處理:做完一步,直接做下一步
接下來(lái)我們介紹一下客戶端的代碼
首先先了解一個(gè)api
其中較為關(guān)鍵的是iceServers
第二個(gè)api
根據(jù)流程寫(xiě)以下代碼:
首先start函數(shù)獲取音視頻數(shù)據(jù)
function start(){
if(!navigator.mediaDevices ||
!navigator.mediaDevices.getUserMedia){
console.error('the getUserMedia is not supported!');
return;
}else {
var constraints;
if( shareDeskBox.checked && shareDesk()){
constraints = {
video: false,
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
}
}else{
constraints = {
video: true,
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
}
}
navigator.mediaDevices.getUserMedia(constraints)
.then(getMediaStream)
.catch(handleError);
}
}
與信令服務(wù)器連接,注冊(cè)處理函數(shù)
function conn(){
socket = io.connect();
socket.on('joined', (roomid, id) => {
console.log('receive joined message!', roomid, id);
state = 'joined'
//如果是多人的話,第一個(gè)人不該在這里創(chuàng)建peerConnection
//都等到收到一個(gè)otherjoin時(shí)再創(chuàng)建
//所以,在這個(gè)消息里應(yīng)該帶當(dāng)前房間的用戶數(shù)
//
//create conn and bind media track
createPeerConnection();
bindTracks();
btnConn.disabled = true;
btnLeave.disabled = false;
console.log('receive joined message, state=', state);
});
socket.on('otherjoin', (roomid) => {
console.log('receive joined message:', roomid, state);
//如果是多人的話,每上來(lái)一個(gè)人都要?jiǎng)?chuàng)建一個(gè)新的 peerConnection
//
if(state === 'joined_unbind'){
createPeerConnection();
bindTracks();
}
state = 'joined_conn';
call();
console.log('receive other_join message, state=', state);
});
socket.on('full', (roomid, id) => {
console.log('receive full message', roomid, id);
hangup();
closeLocalMedia();
state = 'leaved';
console.log('receive full message, state=', state);
alert('the room is full!');
});
socket.on('leaved', (roomid, id) => {
console.log('receive leaved message', roomid, id);
state='leaved'
socket.disconnect();
console.log('receive leaved message, state=', state);
btnConn.disabled = false;
btnLeave.disabled = true;
});
socket.on('bye', (room, id) => {
console.log('receive bye message', roomid, id);
//state = 'created';
//當(dāng)是多人通話時(shí),應(yīng)該帶上當(dāng)前房間的用戶數(shù)
//如果當(dāng)前房間用戶不小于 2, 則不用修改狀態(tài)
//并且,關(guān)閉的應(yīng)該是對(duì)應(yīng)用戶的peerconnection
//在客戶端應(yīng)該維護(hù)一張peerconnection表,它是
//一個(gè)key:value的格式,key=userid, value=peerconnection
state = 'joined_unbind';
hangup();
offer.value = '';
answer.value = '';
console.log('receive bye message, state=', state);
});
socket.on('disconnect', (socket) => {
console.log('receive disconnect message!', roomid);
if(!(state === 'leaved')){
hangup();
closeLocalMedia();
}
state = 'leaved';
});
socket.on('message', (roomid, data) => {
console.log('receive 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));
//create answer
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.log('the message is invalid!', data);
}
});
roomid = getQueryVariable('room');
socket.emit('join', roomid);
return true;
}
媒體協(xié)商call函數(shù)
function call(){
if(state === 'joined_conn'){
var offerOptions = {
offerToRecieveAudio: 1,
offerToRecieveVideo: 1
}
pc.createOffer(offerOptions)
.then(getOffer)
.catch(handleOfferError);
}
}
function getOffer(desc){
pc.setLocalDescription(desc);
offer.value = desc.sdp;
offerdesc = desc;
//send offer sdp
sendMessage(roomid, offerdesc);
}
function getAnswer(desc){
pc.setLocalDescription(desc);
answer.value = desc.sdp;
//send answer sdp
sendMessage(roomid, desc);
}
每個(gè)端都維護(hù)一個(gè)自己的peerconnection
var pcConfig = {
'iceServers': [{
'urls': 'turn:stun.al.learningrtc.cn:3478',
'credential': "mypasswd",
'username': "garrylea"
}]
};
function createPeerConnection(){
//如果是多人的話,在這里要?jiǎng)?chuàng)建一個(gè)新的連接.
//新創(chuàng)建好的要放到一個(gè)map表中。
//key=userid, value=peerconnection
console.log('create RTCPeerConnection!');
if(!pc){
pc = new RTCPeerConnection(pcConfig);
pc.onicecandidate = (e)=>{
if(e.candidate) {
sendMessage(roomid, {
type: 'candidate',
label:event.candidate.sdpMLineIndex,
id:event.candidate.sdpMid,
candidate: event.candidate.candidate
});
}else{
console.log('this is the end candidate');
}
}
pc.ontrack = getRemoteStream;
}else {
console.warning('the pc have be created!');
}
return;
}
柚子快報(bào)邀請(qǐng)碼778899分享:1V1音視頻實(shí)時(shí)互動(dòng)直播系統(tǒng)
相關(guān)閱讀
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場(chǎng)。
轉(zhuǎn)載請(qǐng)注明,如有侵權(quán),聯(lián)系刪除。