GENEARE BY SD
Published on

WebRTC - 尝试 - 第二章

上一章我们完成了基本的框架,这章我们让项目跑起来

回顾整体流程

流程梳理

  1. 前端:用户A进入页面,创建一个PeerConnection
  2. 前端:用户B进入页面,创建一个PeerConnection
  3. 前端:A和B都开始采集流
  4. 前端:A和B都监听icecandidateaddTrack事件
  5. 用户A感知到B进入了某个房间
  6. 用户A发起offer
  7. 用户B收到offer回复answer
  8. 用户A收到answer
  9. 双方建立连接完毕,可以开始通话

流程展开

媒体协商

这一部分是用于交换SDP

  1. A利用PeerConnection创建offer,并把offer记录下来,然后发送给B
  2. B收到offer,A利用PeerConnection创建answer,回复answer,并把answer和offer记录下来
  3. 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)
}

解释下setLocalDescriptionsetRemoteDescription,其实很简单,自己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)
})

MediaStreamMediaStreamTrack组成,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