WebRTC 实现P2P音视频通话——实现一对一音视频通话

WebRTC 实现P2P音视频通话

WebRTC 实现P2P音视频通话——搭建信令服务器

WebRTC 实现P2P音视频通话——搭建stun/trun P2P穿透和转发服务器

WebRTC 实现P2P音视频通话——实现一对一音视频通话



前言

WebRTC 实现P2P音视频通话系列记录了从零->搭建信令服务器->搭建stun/trun P2P穿透和转发服务器->WebRTC P2P音视频通话。
WebRTC 实现P2P音视频通话——实现一对一音视频通话本文将记录获取摄像头,麦克风的音视频流->连接信令服务器 ->加入房间并创建PeerConnection配置stun/turn服务,设置回调,绑定流媒体 ->对端加入房间后创建offer/answer收集媒体信息,通过信令服务器转发给对端进行媒体协商(同时收集candidate并发送到turn服务进行连通性检测)->turn服务检查完成回调检查结果,将检查结果通过信令服务器转发给对对端 ->双方都收到检查结果,开始进行连通,传输音视频流 ->退出房间,释放资源。
废话不多说,JS实现过程都有注释,看代码👇

一、界面,样式的实现

room.html
界面只是简单的显示了连接服务器,退出房间,显示本地,远端视频组件

<html>
        <head>
                <title> WebRTC  PeerConnection</title>
                <link rel="stylesheet" href="./css/main.css"/>
        </head>
        <body>
                <div>
                        <div>
                                <button id="connserver">Connect Sig Server</button>
                                <button id="leave" disabled>Leave</button>
                        </div>
                        <div id="preview">
                                <div>
                                        <h2>Local:</h2>
                                        <video class="video-local" id="localVideo" autoplay playsinline></video>
                                </div>
                                <div>
                                        <h2>Remote:</h2>
                                        <video class="video-remote" id="remoteVideo" autoplay playsinline></video>
                                </div>
                        </div>

                </div>
                <script src="/socket.io/socket.io.js"></script>
                <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
                <script src="js/main.js"></script>
        </body>
</html>

main.css

button {
  margin: 10px 20px 25px 0;
  vertical-align: top;
  width: 134px;
}

table {
  margin: 200px (50% - 100) 0 0; 
}

textarea {
  color: #444;
  font-size: 0.9em;
  font-weight: 300;
  height: 20.0em;
  padding: 5px;
  width: calc(100% - 10px);
}

div#getUserMedia {
  padding: 0 0 8px 0;
}

div.input {
  display: inline-block;
  margin: 0 4px 0 0;
  vertical-align: top;
  width: 310px;
}

div.input > div {
  margin: 0 0 20px 0;
  vertical-align: top;
}

div.output {
  background-color: #eee;
  display: inline-block;
  font-family: 'Inconsolata', 'Courier New', monospace;
  font-size: 0.9em;
  padding: 10px 10px 10px 25px;
  position: relative;
  top: 10px;
  white-space: pre;
  width: 270px;
}

div#preview {
  border-bottom: 1px solid #eee;
  margin: 0 0 1em 0;
  padding: 0 0 0.5em 0;
}

div#preview > div {
  display: inline-block;
  vertical-align: top;
  width: calc(50% - 12px);
}

section#statistics div {
  display: inline-block;
  font-family: 'Inconsolata', 'Courier New', monospace;
  vertical-align: top;
  width: 308px;
}

section#statistics div#senderStats {
  margin: 0 20px 0 0;
}

section#constraints > div {
  margin: 0 0 20px 0;
}

h2 {
  margin: 0 0 1em 0;
}


section#constraints label {
  display: inline-block;
  width: 156px;
}

section {
  margin: 0 0 20px 0;
  padding: 0 0 15px 0;
}

video {
  background: #222;
  margin: 0 0 0 0;
  --width: 100%;
  width: var(--width);
  height: 225px;
}

@media screen and (max-width: 720px) {
  button {
    font-weight: 500;
    height: 56px;
    line-height: 1.3em;
    width: 90px;
  }

  div#getUserMedia {
    padding: 0 0 40px 0;
  }

  section#statistics div {
    width: calc(50% - 14px);
  }

}

二、音视频通话的实现

main.js
获取流媒体 -> 连接信令服务器 ->创建peerconnection配置stun/turn服务 ->设置回调 ->绑定流媒体 ->获取媒体信息交换协商 ->stun/turn服务检测完成回调 ->交换candidate进行连通,交换媒体数据

'use strict'
/**
* WebRTC 一对一音视频通话
*@author F小志
*@date 2022-02-06
*@copyleft F小志
*/
var localVideo = document.querySelector('video#localVideo');
var remoteVideo = document.querySelector('video#remoteVideo');

var btnConn = document.querySelector('button#connServer');
var btnLeave = document.querySelector('button#leave');

var displayStream = null;
var localStream = null;
var remoteStream = null;
var displayRemoteStream = new MediaStream();

var roomid = '666666';
var offerDesc = null;
var answerDesc = null;

var socket = null;
var state = 'init';

var videoId = null;


var pcConfig = {//turn服务器的相关配置
	
	'iceServers':[{
		'urls':'turn:xxx.xxx.xxx.xxx:3478',//xxx.xxx.xxx.xxx 换成自己的turn服务地址
		'credential':'123456',//换成自己配置的密码
		'username':'webrtc'//换成自己配置的账号
	}]
	
};
var pc = null;

//远端码流回调
function getRemoteStream(e){
	remoteStream = e.streams[0];
	remoteVideo.srcObject = e.streams[0];
}

//侯选者回调
function getICECandidate(e){
	if(e.candidate){
		var candidate = {
			type: 'candidate',
			label:event.candidate.sdpMLineIndex,
			id:event.candidate.sdpMid,
			candidate: event.candidate.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');
	}

}
//创建peerconnection
function createPeerConnection(){
	
		
	if(pc){
		console.warning('the pc have be created!');	
		return;
	}

	console.log('create RTCPeerConnection');

	pc = new  RTCPeerConnection(pcConfig);//创建peerConnection并配置stun服务器

	//pc.onicecandidate = getICECandidate;//设置候选者回调
        
	pc.onicecandidate = (e)=>{
		if(e.candidate){
			var candidate = {

				type: 'candidate',
				label:event.candidate.sdpMLineIndex,
				id:event.candidate.sdpMid,
				candidate: event.candidate.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;//远端媒体流回调
	return;
}

//绑定媒体轨,永远跟在创建完peerconnection之后
function bindTracks(){
	console.log('bind tracks into RTCPeerConnection');

	if(pc === null || pc === undefined ){
		console.log('pc is null or undefined');
		return;
	}
	if(localStream === null || localStream === undefined){
		console.log('localStream is null or undefined');
		return;
	}

	//add all trcak into peer connection
	localStream.getTracks().forEach((track)=>{
		pc.addTrack(track, localStream);
	});

}
//创建answer成功的回调函数
function getAnswer(desc){
	pc.setLocalDescription(desc);
	answerDesc = desc;
	//send answer sdp 将收集到的信息发送给信令服务器,再由信令服务器转发
	sendMessage(roomid,desc);
}

//创建answer失败的回调函数
function handleAnswerError(err){
	console.log('Failed to create answer',err);
}



//创建offer成功的回调函数
function getOffer(desc){

	pc.setLocalDescription(desc);//设置到本地Description中同时开始收集candidate候选者
	offerDesc = desc;

	//send offer sdp 将收集到的信息发送给信令服务器,再由信令服务器转发
	sendMessage(roomid, offerDesc);


}
//处理创建offer失败的回调函数
function handleOfferError(err){
	console.log('Offer create Failed',err);
}

//创建offer,成功后开始收集candidate,在进行媒体协商
function call(){

        if(state === 'joined_conn'){//双方都准备好的状态

		var offerOptions = {

			offerToRecieveVideo: 1,
			offerToRecieveAudio: 1
		}
		pc.createOffer(offerOptions)
			.then(getOffer)
			.catch(handleOfferError);
	}
}


//连接信令服务器的函数
function conn(){

	if(!(socket ===null)){
		console.log('socket not equal to null');
		return;
	}
	socket = io.connect();//连接服务器

	//注册服务器反馈的消息
	socket.on('joined',(roomid,id)=>{//房间加入成功后的反馈消息
		console.log('receive joined message!',roomid, id);
		state = 'joined';

		//进入房间的每一个用户都通过此消息进行创建peerConnection,除已经在房间内的用户且state=joined_unbind 
		//创建连接以及绑定媒体轨
		createPeerConnection();
		bindTracks();
		btnConn.disabled = true;
		btnLeave.disabled = false;
		console.log('receive joined message, state=',state);
	});

	socket.on('otherjoin',(roomid,id)=>{//其他用户加入房间时的反馈消息
		console.log('receive otherjoin  message!',roomid, id);

		if(state === 'joined_unbind'){//已有用户加入并退出房间,此时在房间内的用户状态为unbind,在等其他用户加入时,需重新创建peerconnection
			createPeerConnection();
                	bindTracks();
		}
		//当两个用户都准备好了
		state = 'joined_conn';
		call();
		console.log('receive otherjoin message, state=',state);
	});

	socket.on('full',(roomid,id)=>{//加入房间时,房间已经满了的反馈消息
		console.log('receive full  message!',roomid, id);
		state = 'leaved';
		hangup();
		closeLocalMedia();
		btnConn.disabled = false;
                btnLeave.disabled = true;
		console.log('receive  full message, state=',state);
	});

	socket.on('leaved',(roomid,id)=>{//离开房间成功后的反馈消息
		console.log('receive leaved message!',roomid, id);
		state = 'leaved';
		socket.disconnect();
		btnConn.disabled = false;
                btnLeave.disabled = true;
		console.log('receive leaved message, state=',state);
	});

	socket.on('bye',(roomid,id)=>{//其他用户退出房间时的反馈消息
		console.log('receive bye message!',roomid, id);
		state = 'joined_unbind';
		hangup();
		console.log('receive bye message, state=',state);
	});

	socket.on('disconnect',(roomid,id)=>{//socket连接断开的反馈消息
                console.log('receive disconnect message!',roomid, id);
		if(!(state === 'leaved')){
			hangup();
			closeLocalMedia();
		}
		state = 'leaved';
		btnConn.disabled = false;
                btnLeave.disabled = true;
                console.log('receive disconnect message, state=',state);
        });

	socket.on('message',(roomid,data)=>{//其他端发送过的消息
                console.log('receive  message!',roomid, data);

		if(data === null || data === undefined){
			congsole.log('the message is invalid!');
			return;
		}

		if(data.hasOwnProperty('type') && data.type === 'offer'){//对端的offer媒体信息
		
			if(!pc){
				console.log('ps is to null');
				return;
			}
			pc.setRemoteDescription(new RTCSessionDescription(data));//收到对端的媒体信息,将其设置到RemoteDescription进行协商检查

			//create answer 创建自己媒体信息
			pc.createAnswer()
				.then(getAnswer)
				.catch(handleAnswerError);

		}else if(data.hasOwnProperty('type') && data.type === 'answer'){//对端的answer媒体信息
			if(!pc){
                                console.log('ps is to null');
                                return;
                        }
			pc.setRemoteDescription(new RTCSessionDescription(data));//收到对端的媒体信息,将其设置到RemoteDescription进行协商检查

		}else if(data.hasOwnProperty('type') && data.type === 'candidate'){//对端发送过来的trun服务器检查结果
			var candidate = new RTCIceCandidate({
				
				sdpMLineIndex: data.label,
				candidate: data.candidate
			});
			pc.addIceCandidate(candidate);

      	}else{
			console.log('the message is invalid!',data);
			return;
		}

		console.log('receive  message, state=',state);
        });

//	roomid = getQueryVariable('room');//从跳转页面中获取房间号
	socket.emit('join', roomid);

}

//发送消息的函数
function sendMessage(roomid,data){

	console.log('send message to other end',roomid, data);
	if(!socket){
		console.log('socket is null');
		return;
	}
	socket.emit('message',roomid,data);
}

//获取媒体流出错的回调函数
function handleError(err){
	console.log("Fails to getMediaStream :", err);
}
//获取媒体流成功的回调函数
function getMediaStream(stream){
	localStream = stream;
	localVideo.srcObject = stream;
	
	conn();//获取本地流媒体成功后连接信令服务器
}
//获取屏幕流媒体成功的回调函数
function getDidplayStream(stream){
	displayStream = stream;
	displayVideo.srcObject = stream;
}
//获取屏幕流媒体失败的回调函数
function handleDidplayStreamError(err){
	console.log("Fails to getDidplayStream :", err);
}
/**
 *起始
 *1.先开启本地媒体流
 *2.与信令服务器建立连接并通信
 *3.双方加入房间,开始收集candidate并发送到turn服务进行连通性检查
 *4.同时将媒体信息通过信令服务器转发给对方,进行媒体协商
 *5.turn服务检查完成回调检查结果,将检查结果通过信令服务器转发给对方,
 *6.双方都收到检查结果,开始进行连通,传输音视频流
 */
function connectionSigServer(){

	//开启本地音视频
	start();
	//conn();
	return;

}


function start(){
	
        if(!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia){//媒体设备是否存在
                console.log('getUserMedia is not supported!');
				return;
        }else{
		var constraints = {//配置是否接受视频,音频,可以进一步视频配置分辨率,帧率,音频采样率,单声道双声道
                	video:true,
                	audio:true
		}
		navigator.mediaDevices.getUserMedia(constraints)
                       	        .then(getMediaStream)
                           	.catch(handleError);

	}

}

//释放peerconnection 资源
function hangup(){
	if(pc){
		offerDesc = null;
		pc.close();
		pc =null;
	}
}

//释放流媒体资源
function closeLocalMedia(){
	if(localStream && localStream.getTracks()){
	
		localStream.getTracks().forEach((track)=>{
			track.stop();
		});
	}
	localStream =null;

	if(displayStream && displayStream.getTracks()){

                displayStream.getTracks().forEach((track)=>{
                        track.stop();
                });
        }
        displayStream =null;
}
//退出房间按钮的点击事件
function leave(){
	if(socket){
		socket.emit('leave',roomid);
		btnConn.disabled = false;
		btnLeave.disabled = true;
	}
	hangup();
	closeLocalMedia();

	btnConn.disabled = false;
	btnLeave.disabled = true;
}

//获取页面跳转时 url参数
function getQueryVariable(variable){

	var query = window.location.search.substring(1);
	var vars = query.split('&');
	for (var i=0; i<vars.length; i++){
		var pair = vars[i].split('=');//通过等号进行分割字符
		if(pair[0] == variable){return pair[1];}
	}
	return (false);
}

//连接信令服务器按钮的点击事件
btnConn.onclick = connectionSigServer;
btnLeave.onclick = leave;

效果

在这里插入图片描述

以上使用信令服务器进行管理房间以及媒体信息的交换, 使用stun/trun服务进行穿越检测,连通,转发,最后使用WebRTC实现音视频采集,媒体协商并传输,这样一个非常简单的一对一音视频通话就实现了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值