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. 双方建立连接完毕,可以开始通话
    主要的流程就是上述这些,剩下的结合代码展开 MDN wewbrtc建联 其中6-8步叫做信令交换,主要用于两个客户端进行协商通信协议,需要信令服务器转发

需要什么?

  1. 前端页面:播放视频、音频
  2. 后端服务:实现信令服务器

项目搭建

pnpm + monorepo workspace

初始化项目

前端

先用vite创建个React新项目 pnpm create vite

function App() {
  const videoDomRef = useRef<HTMLVideoElement | null>(null);
  const localStreamRef = useRef<MediaStream>();
  //创建peerconnection实例
  const rtcPeerconnectionRef = useRef<RTCPeerConnection>(
    new RTCPeerConnection(CONNECTION_CONFIG)
  );


  useEffect(() => {
      const capture = async () => {
        //采集
        const cameraStream = await navigator.mediaDevices.getUserMedia({
          video: true,
        });
        localStreamRef.current = cameraStream;
        //播放
        videoDomRef.current!.srcObject = cameraStream;
      };
      capture()
    }, []);
  };

  return (
    <>
      <video
        width='800px'
        height='450px'
        controls
        autoPlay
        muted
        ref={videoDomRef}
        style={{ width: "100%", height: "100%" }}
      />
    </>
  );
}

这一步我们完成了

  1. RTCPeerConnection的创建
  2. 摄像头的采集以及采集内容的播放

其中:

  1. RTCPeerConnection的创建 通过new RTCPeerConnection()完成。其中RTCPeerConnection是WebRTC的核心,后续音/视频流的发送,信令的交换,ICE的监听都有RTCPeerConnection的参与。
  2. 摄像头采集到的数据类型为MediaStream,通过给<video/>直接设置srcObject即可播放。

现在我们完成了步骤1、2和3

后端

用express + websock.io实现信令服务器

//index.ts
import { Server } from 'socket.io'
import express from 'express'
import { createServer } from 'node:http'

const app = express()
const server = createServer(app)
const io = new Server(server)

server.listen(3000, () => {
  console.log('server running at http://localhost:3000')
})

使用ts-node就可以执行这个文件

ts-node ./index.ts

执行后会在3000端口启动一个http服务

初始化信令服务器相关

这一部分完成 5-8 这几步 先完成第五步,我们通过 socket.io 来完成两个端之间的通信

后端
import { Server } from 'socket.io'

//....
const io = new Server(server)

server.listen(3000, () => {
  console.log('server running at http://localhost:3000')
})

io.on('connection', (socket) => {
  socket.on('join-room', ({ roomid, userid }) => {
    console.log('🚀 ~ socket.on join-room ~', roomid, userid)
    socket.join(roomid)
    socket.broadcast.emit('user-connected', { userid, roomid })
  })
})

其中 connection 是socket.io内置的事件,当一个用户连接上webscocket服务器后就会触发这个事件。
在这个事件中,我们能够获取到socket的实例。

socket.on('join-room' /* ... */)

join-room是我们自定义的事件,在前端emit时会触发,参数我们这里比较简单,只有房间ID和用户ID,当两个用户进入到同一个房间才可以进行通信。

socket.broadcast.emit('user-connected', { userid, roomid })

当一个用户进入房间后,会通过emit("user-connected", { userid, roomid })进行广播通知其他用户。表示某某用户进入了房间,这时候就可以继续后面的6-8步,也就是offer相关的操作。

前端

前端使用socket.io-client创建websocket连接。

import { useEffect, useRef } from 'react'
import { io, Socket } from 'socket.io-client'

const CONNECTION_CONFIG = {
  iceServers: [
    {
      urls: ['stun:stun1.l.google.com:19302', 'stun:stun2.l.google.com:19302'],
    },
  ],
}

function App() {
  //增加下面三行
  const ioRef = useRef<Socket>()
  const roomIDRef = useRef('')
  const userIDRef = useRef('')

  const videoDomRef = useRef<HTMLVideoElement | null>(null)
  const localStreamRef = useRef<MediaStream>()
  const rtcPeerconnectionRef = useRef<RTCPeerConnection>(new RTCPeerConnection(CONNECTION_CONFIG))

  useEffect(() => {
    const capture = async () => {
      const cameraStream = await navigator.mediaDevices.getUserMedia({
        video: true,
      })
      localStreamRef.current = cameraStream
      videoDomRef.current!.srcObject = cameraStream
    }
    capture()
  }, [])

  // 创建websocket连接
  const initWS = () => {
    const instance = io()
    ioRef.current = instance

    //触发 join-room 并传递参数,对应了后端的 on 代码
    ioRef.current?.emit('join-room', {
      roomid: roomIDRef.current,
      userid: userIDRef.current,
    })

    //有用户进房了
    ioRef.current.on('user-connected', async ({ userid }) => {
      console.log('user-connected:', userid)
      //这里执行一些webrtc开始建立连接相关的逻辑
      //。。。。
    })
  }

  return (
    <>
      <div
        style={{
          display: 'flex',
          flexWrap: 'wrap',
        }}
      >
        <div
          style={{
            flexGrow: 1,
            aspectRatio: '16/9',
          }}
        >
          <video
            controls
            autoPlay
            muted
            ref={videoDomRef}
            style={{ width: '100%', height: '100%' }}
          />
        </div>
        <div
          style={{
            flexGrow: 1,
            aspectRatio: '16/9',
          }}
        >
          <video
            autoPlay
            controls
            muted
            ref={remoteVideoDomRef}
            style={{ width: '100%', height: '100%' }}
          />
        </div>
      </div>
      <input placeholder="roomid" onChange={(e) => (roomIDRef.current = e.target.value)} />
      <input placeholder="userid" onChange={(e) => (userIDRef.current = e.target.value)} />

      <button
        onClick={() => {
          initWS()
        }}
      >
        join
      </button>
    </>
  )
}

export default App

在这里,用户点击join后即可进入某个websocket房间。我们可以举个例子:

  1. A用户进入页面点击join,触发connection事件,socket进入房间 1,socket广播user-connected,但是没人收到
  2. B用户进入页面点击join,触发connection事件,socket进入房间 1,socket广播user-connected
  3. B用户on('user-connected')触发知道了A用户了页面,可以向A请求建立webrtc连接

下一章我们完成剩余的工作