- Published on
WebRTC - 尝试 - 第二章
上一章我们完成了基本的框架,这章我们让项目跑起来
回顾整体流程
流程梳理
前端:用户A进入页面,创建一个PeerConnection
前端:用户B进入页面,创建一个PeerConnection
前端:A和B都开始采集流- 前端:A和B都监听
icecandidate
和addTrack
事件 用户A感知到B进入了某个房间- 用户A发起offer
- 用户B收到offer回复answer
- 用户A收到answer
- 双方建立连接完毕,可以开始通话
流程展开
媒体协商
这一部分是用于交换SDP
- A利用
PeerConnection
创建offer,并把offer记录下来,然后发送给B - B收到offer,A利用
PeerConnection
创建answer,回复answer,并把answer和offer记录下来 - A收到answer,记录answer
网络寻址
而为了在当前网络环境中找到对端的连接地址,我们还需要引入一个概念叫做ICE。
ICE用于描述连接的网络信息,抹平了不同协议的差异。 在WebRTC中我们通过onicecandidate
来获取到自己的网络环境, 然后通过信令服务器交换,最后由对端通过addIceCandidate
把自己加为候选人
书接上回
上一篇我们完成了AB两个用户进入页面时能够通知对方,在A感知到B时,我们创建offer
ioRef.current.on('user-connected', async ({ userid }) => {
console.log('user-connected:', userid)
const offer = await createOffer()
ioRef.current?.emit('client-offer', offer)
})
const createOffer = async () => {
const peerConnection = somePeerConnection
//创建offer
const offer = await peerConnection.createOffer()
//记录offer
await peerConnection.setLocalDescription(offer)
return offer
}
在服务端我们把这个offer转发给B
socket.on('client-offer', (offer) => {
console.log('🚀 ~ socket.on client-offer~', offer)
socket.broadcast.emit('forward-offer', offer)
})
然后B在页面上接收offer,记录offer,回复answer,记录answer
ioRef.current?.on('forward-offer', (offer) => {
//接收offer
console.log('forward-offer:', offer)
createAnswer(offer)
})
const createAnswer = async (offer: RTCSessionDescriptionInit) => {
const peerConnection = somePeerConnection
//记录offer
await peerConnection.setRemoteDescription(offer)
//创建answer
const answer = await peerConnection.createAnswer()
//记录answer
await peerConnection.setLocalDescription(answer)
//回复answer
ioRef.current?.emit('client-answer', answer)
}
解释下setLocalDescription
和setRemoteDescription
,其实很简单,自己createOffer就用setLocalDescription
,别人来的就用setRemoteDescription`
服务端转发answer
socket.on('client-answer', (answer) => {
console.log('🚀 ~ socket.on client-answer~', answer)
socket.broadcast.emit('forward-answer', answer)
})
再到A收到answer,记录answer
ioRef.current?.on('forward-answer', (answer) => {
console.log('forward-answer:', answer)
addAnswer(answer)
})
const addAnswer = (answer: RTCSessionDescriptionInit) => {
if (!rtcPeerconnectionRef.current?.currentRemoteDescription) {
rtcPeerconnectionRef.current?.setRemoteDescription(answer)
}
}
其中setLocalDescription
会收集自身的ICE信息,我们需要把这个信息也给到对方,让对方知道怎么和自己连接
(注:setLocalDescription
本身不会直接触发ICE的收集,需要调用PeerConnection
上的addTrack
方法后才会触发下面的onicecandidate
回调)
peerConnection.onicecandidate = (e) => {
console.log('🚀 ~ initPeerConnectionCallback ~ e:', e)
if (!e.candidate) return
//发送自己的ice
ioRef.current?.emit('new-ice-candidate', e.candidate)
}
服务端转发
socket.on('new-ice-candidate', (offer) => {
console.log('🚀 ~ socket.on new-ice-candidate~', offer)
socket.broadcast.emit('forward-candidate', offer)
})
另一端接受后设置为自己连接的候选
ioRef.current?.on('forward-candidate', (candidate) => {
rtcPeerconnectionRef.current.addIceCandidate(candidate)
})
至此我们完成了连接了建立
那么,画面呢?
上面这些完成后我们还是看不到画面的,因为我们采集到的MediaStream
没有发送到对方的电脑上,所以先发送一下,把音频和视频加到自己的peerConnection
上。
问了下GPT为什么addTrack之后才触发onicecandidate
媒体协商的需求:在WebRTC中,ICE候选者的收集和交换是建立连接的一部分,但这个连接是为了传输特定的媒体流(如视频或音频)。如果没有通过addTrack(或addStream)指定要传输的媒体类型,那么开始ICE过程(即收集和交换网络候选者)可能没有实际的意义,因为对端还不知道要准备接收什么类型的媒体。
SDP的完整性:addTrack对生成的SDP影响很大,因为它决定了SDP中将包含哪些媒体描述。只有在添加了媒体轨道后,生成的SDP才是完整的,这时开始ICE候选者收集和交换才有意义,因为这时双方都清楚要交换什么类型的媒体数据。
yourStream?.getTracks().forEach((track) => {
peerConnection.addTrack(track)
})
MediaStream
由MediaStreamTrack
组成,peerConnection
无法直接发送MediaStream
,所以我们拿到所有的track添加到peerConnection上
如果我们同时采集视频和音频,那么
getTracks
应该可以获取两个track分别对应了视频和音频
而对端则可以拿到一个MediaStream
用于播放
peerConnection.ontrack = (e) => {
const stream = new MediaStream()
console.log('🚀 ~ initPeerConnectionCallback ~ e:', e)
e.streams.forEach((stream) => {
stream.getTracks().forEach((track) => stream.addTrack(track))
})
someVideoElement!.srcObject = stream
}
DONE