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实现音视频采集,媒体协商并传输,这样一个非常简单的一对一音视频通话就实现了。