- Published on
WebRTC - 尝试 - 第一章
整体流程
流程梳理
- 前端:用户A进入页面,创建一个
PeerConnection
- 前端:用户B进入页面,创建一个
PeerConnection
- 前端:A和B都开始采集流
- 前端:A和B都监听
icecandidate
和addTrack
事件 - 用户A感知到B进入了某个房间
- 用户A发起offer
- 用户B收到offer回复answer
- 用户A收到answer
- 双方建立连接完毕,可以开始通话
主要的流程就是上述这些,剩下的结合代码展开 MDN wewbrtc建联 其中6-8步叫做信令交换,主要用于两个客户端进行协商通信协议,需要信令服务器转发
需要什么?
- 前端页面:播放视频、音频
- 后端服务:实现信令服务器
项目搭建
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%" }}
/>
</>
);
}
这一步我们完成了
RTCPeerConnection
的创建- 摄像头的采集以及采集内容的播放
其中:
RTCPeerConnection
的创建 通过new RTCPeerConnection()
完成。其中RTCPeerConnection
是WebRTC的核心,后续音/视频流的发送,信令的交换,ICE的监听都有RTCPeerConnection
的参与。- 摄像头采集到的数据类型为
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房间。我们可以举个例子:
- A用户进入页面点击join,触发
connection
事件,socket进入房间 1,socket广播user-connected
,但是没人收到 - B用户进入页面点击join,触发
connection
事件,socket进入房间 1,socket广播user-connected
- B用户
on('user-connected')
触发知道了A用户了页面,可以向A请求建立webrtc连接
下一章我们完成剩余的工作