1. webRTC连接握手概念图解
WebRTC
允许在两个设备之间进行实时的对等媒体交换。通过称为信令的发现和协商过程建立连接. 在这里就有一点像http建立连接握手一样,WebRTC
也必须进行一种发现和媒体格式协商,以使不同网络上的两个设备相互定位。这个过程被称为信令,并涉及两个设备连接到第三个共同商定的服务器。通过这个第三方服务器,这两台设备可以相互定位,并交换协商消息。
为此两个设备直接建立WebRTC
需要一个信令服务器来实现双方通过网络进行连接,一般来说这个信令服务器就是一个WebSocket
服务,也用http/https做信令服务的
1.1 常规握手连接
如图:
一般第一步,需要准备聊天(信令)服务器来处理信令,服务器支持多种消息格式来处理不同的任务,比如注册新用户、设置用户名、发送公共信息等等。
第二步交换会话描述信息,一般来说,连接发起方会通过信令服务器(或者http协议)发送直接的offer(sdp),紧接着接受方收到发起方的信号,并将他提供的offer(sdp)设置到本地,然后创建一个answer(sdp)发送给发起方,让发起方将anwser(sdp)设置到本地,这样各种的网络协议就互相交换完成了
如果有必要(两个节点需要交换 ICE 候选来协商他们自己具体如何连接),还会交换ICE候选,ICE协议内容一般是各自创建offer或者answer的时候,触发ice方法回调中得到的。
- 以上协议交换完成,也就是握手结束
1.2 与流媒体服务器连接
上面介绍的一直常规的一般方式的点对点连接,但是大部分企业服务应用中不是采取的这种客户端的点对点连接,而是使用了一个流媒体服务器作为中转,是客户端用WebRTC与流媒体服务器建立连接,然后推流和拉流
- 一般第一步就是推流,也就是客户端与流媒体服务器建立连接,首先也是发送offer给流媒体服务器
- 然后服务器应答,会发送一个answer给客户端,客户端设置answer即可
- 接着就是拉取流,这要新建立一个rtc连接,发送offer给流媒体服务器
- 然后服务器应答,会发送一个answer给客户端,客户端设置answer即可
从上面步骤来看,也就是这种方式客户端要建立两个RTC连接,分别用于流的推送和流的拉取播放
2. 使用RTCPeerConnection接口建立连接
通过上面基本步骤了描述,web端提供了一个接口RTCPeerConnection
来创建rtc连接,接下来就要使用RTCPeerConnection
来建立webRTC连接
2.1 常规握手连接(伪代码)
这里的信令服务器,我这边假设使用的是ws作为信令服务
import type { Socket } from 'socket.io';
import socket from 'socket.io-client';
type ConnectAtionType = 'call' | 'join' | 'sendoffer' | 'sendanwser' | 'condidate' | 'close';
const socketIns = socket(`wss://${ip}/`);
const player:HTMLVideoElement = document.getElementById('localVideo')
let rtcInsAction = null;
const sedSocktMsg = (option: { action: ConnectAtionType; content?: string }) => {
if (!socketIns) return;
socketIns.emit(
'message',
Object.assign(option, {
target,
username,
tid: new Date().getTime() + '',
socketid: socketIns.id,
}),
);
};
const createConnect = async (transdirect:'recvonly' | 'sendrev',sdp?:RTCSessionDescriptionInit)=>{
const _configuration = {
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
};
const rtcIns = new RTCPeerConnection(_configuration);
rtcIns.addEventListener('track', (e) => {
if (e.streams && e.streams[0]) {
// 这里就是远程发来的媒体流
console.log(e.streams[0])
player?.setAttribute('autoplay', '');
player?.setAttribute('playsinline', '');
player && (player.srcObject = e.streams[0]);
}
});
// 监听触发的icecandidate的事件,一般来说只要使用了RTCPeerConnection.setLocalDescription就能触发
rtcIns.addEventListener('icecandidate', (e) => {
if (e.candidate) {
sedSocktMsg({
action:"condidate",
content:JSON.stringify(e.candidate)
})
}
});
// 监听连接状态
rtcIns.addEventListener('connectionstatechange', (e) => {
const RTCPeerConnection: RTCPeerConnection = e.target as RTCPeerConnection;
const state = RTCPeerConnection.connectionState as ConnectionState;
console.log(state)
});
// 如果是仅仅是接受方,就不需要捕获媒体,直接接受远端的流即可
if (transdirect === 'recvonly') {
rtcIns.addTransceiver('video', { direction: 'recvonly' });
rtcIns.addTransceiver('audio', { direction: 'recvonly' });
} else {
const streams = await getUserMediaStream();
streams.getTracks().forEach((track) => {
rtcIns?.addTrack(track, streams);
});
}
if(sdp){
await rtcIns.setRemoteDescription(sdp);
const answer = await rtcIns.createAnswer();
await rtcIns.setLocalDescription(answer);
// 这里的逻辑是如果是接受方,那么createConnect就要接受一个offer(sdp)
// 创建answer发送
sedSocktMsg({
action:"sendanwser",
content:JSON.stringify(answer)
})
}else{
// 创建offer并发送给远程
const offer = await rtcIns.createOffer();
await rtcIns.setLocalDescription(offer);
sedSocktMsg({
action:"sendoffer",
content:JSON.stringify(offer)
})
}
return {
close:()=>{
rtcIns.close()
},
setRemoteSdp:(sdp: RTCSessionDescriptionInit)=>{
rtcIns.setRemoteDescription(sdp);
},
addIceCandidate: (sdp: RTCIceCandidate) => {
rtcIns.addIceCandidate(sdp);
},
}
}
socketIns.on('message',(message)=>{
const { action, result } = message;
if (result === 'success') {
switch (action) {
case 'applyoffer': {
// 发起了联通的请求建立连接
crateConnect(JSON.parse(message.constent)).then( res =>{
rtcInsAction = res
});
break;
}
case 'addCondidate': {
// 设置对方发过来的ice协议
rtcInsAction.addIceCandidate(JSON.parse(message.constent))
break;
},
case 'applyanwser': {
// 设置对方发过来的answer协议
rtcInsAction.setRemoteSdp(JSON.parse(message.constent));
break;
},
case 'close': {
rtcInsAction.close();
break
}
}
}
})
以上就是对1部分的文字翻译成代码的结构
值得注意的是关闭webrtc的时候,仅仅只把RTCPeerConnection
这个点对点的实例化对象关闭是不完整的,还需要把他里面的tack媒体轨道停止追踪
因此对上面代码close部分还需要获取在这个点对点连接中的流轨道,然后停止流的追踪,再然后关闭点对点连接(补充:关闭本地流的追踪,这部分一般是要对业务情况做判断是否需要关闭本地流追踪,如果视频连接断掉,没有其他业务需要用到捕获的本地流,那么本地流追踪也是要关闭的追踪的),改造如下:
close:()=>{
const receivers = rtcIns.getReceivers().map((item) => {
return item.track;
});
const senders = peerConnection.getSenders().map((item) => {
return item.track;
});
[...receivers,...senders].forEach( track =>{
track?.stop();
})
rtcIns.close()
}
2.2 与流媒体服务器连接
与流媒体服务器建立连接,差别其实不是很大,把流媒体服务器想象成一个客户端就行了。而且在仅仅只有拉流的时候,不需要捕获本地流,所以直接用
rtcIns.addTransceiver('video', { direction: 'recvonly' });
rtcIns.addTransceiver('audio', { direction: 'recvonly' });
★文末名片可以免费领取音视频开发学习资料,内容包括(FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。
见下方!↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓