前置知识
什么是 WebRtc ?
WebRTC(Real-Time Communication),是一个由 Google 发起的实时通讯解决方案,它既是 API 也是协议。WebRTC 协议是两个 WebRTC Agent 协商双向安全实时通信的一组规则,我们可以通过 WebRTC API 使用 WebRTC 协议,从而实现实时通信。
WebRTC 标准概括介绍了两种不同的技术:媒体捕获设备和点对点连接。
媒体捕获设备就包括了包括摄像机,麦克风,和屏幕捕获设备,对于摄像头和麦克风,我们使用 navigator.mediaDevices.getUserMedia()
来捕获 MediaStreams
。对于屏幕录制,我们改为使用 navigator.mediaDevices.getDisplayMedia()
。
点对点连接就由 RTCPeerConnection
接口处理,它代表一个由本地计算机到远端的 WebRTC 连接。该接口提供了创建,保持,监控,关闭连接的方法的实现。
什么是 WebSocket ?
它是一种协议,用于在 Web 应用程序中创建实时、双向的通信通道。它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
开始实践
这两个核心技术大致介绍了一下,现在我们就开始实现一个视频聊天!
问题分析
实现之前,我们先来分析一下我们需要解决哪些核心问题:
- 如何获取当前用户的视频和音频并且显示?
- 两个客户端如何互相建立联系,传递消息?
- 如何实时传递对方的音视频流然后显示?
实现思路
如何获取当前用户的视频和音频?
这个问题其实在我介绍 WebRtc 的时候已经给出了答案:这里我们需要使用 WebRTC 中的媒体捕获设备类型的 api ,并且我们这里需要使用到的设备是摄像头和麦克风,所以就是使用navigator.mediaDevices.getUserMedia()
来捕获当前用户的音视频,然后我们再使用video标签展示我们获取到的音视频流,代码如下:
jsx
// 当前视频语音流
const [ stream, setStream ] = useState()
// 当前用户使用的video标签的ref
const myVideo = useRef()
useEffect(()=>{
// 获取当前用户音视频
navigator.mediaDevices.getUserMedia({
video: true,
audio: true
}).then((stream) => {
// 将stream通过state存储
setStream(stream)
if(myVideo.current){
// 将stream给video标签的srcObject,用来显示音视频流
myVideo.current.srcObject = stream
}
})
})
这里有个地方可以注意一下: srcObject 是实验属性
两个客户端如何互相建立联系,传递消息?
整体来说就是当客户端1发起通话时,要去发消息告诉服务端并且带上所需数据,然后服务端传达,然后客户端2同意接通之后,也要发送消息给服务端并且带上所需数据,服务端再次传达给客户端1,流程大致如图:这里省去了数据的传递
服务端设计
首先,这里我们肯定需要一个服务,用来接收客户端的信息并且做一些信息的传递,我们可以新建一个 server.js 文件,然后用 express (用啥随意,我这里使用 express 是因为 socket.io 官网示例用的express)新建一个服务,然后这个服务需要用到 websocket(后文统一使用 ws 作为它的简写)来进行客户端和服务端的双向通信。
server.js 文件代码如下:
js
const express = require("express")
const http = require("http")
const app = express()
const server = http.createServer(app)
// 新建一个ws实例
const io = require("socket.io")(server,
{
// 解决跨域问题,3000端口运行的是前端项目
cors: {
origin: "http://localhost:3000",
methods: [ "GET", "POST" ]
}
}
)
// 当ws连接成功时,触发回调
io.on("connection", (socket) => {
// 给连接上的客户端发送其对应的socketId(socketId可以理解成客户端socket的唯一标识)
socket.emit("socketId", socket.id)
// 接收到客户端的请求通话事件
socket.on("callOhter", (data) => {
// 根据传来的ohterId,将信息转发到对应的客户端(告诉对应客户端你被call了!)
io.to(data.to).emit("beCalled", {
// 拨打方peer信号
signal: data.signalData,
// 拨打方id
from: data.from,
// 拨打方名字
name: data.name
})
})
// 接收到用户的同意接通事件
socket.on("answerCall", (data) => {
// 根据传来的ohterId,将信息转发到对应的客户端(告诉对应客户端对方接听了!)
io.to(data.to).emit("otherAccepted", {
// peer信号
signal: data.signalData,
// 接收方id
from: data.from,
// 接收方名字
name: data.name
})
})
// 接到一方挂断电话的事件,通知双方都要关闭连接了
socket.on("endCall",(data)=>{
io.to(data.to).emit("bothCallEnd")
})
})
// 在5000端口开启服务
server.listen(5000, () => console.log("server is running on port 5000"))
客户端设计
按照上述设计,我们客户端在用户发起通话和同意通话时也要做一些处理,主要就是和服务端通信以及带上所需数据,代码如下(我这里只写相关socket消息传递逻辑):
js
import { useState,useRef,useEffect } from 'react'
import { Input,Typography,Button, message } from 'antd';
import { CopyOutlined,WhatsAppOutlined } from '@ant-design/icons'
import io from "socket.io-client"
import Peer from "simple-peer"
import './App.css';
// 建立客户端和5000端口的服务端的ws连接
const socket = io.connect('http://localhost:5000')
function App() {
// ...
useEffect(()=>{
// ...
// 连接上socket时获取socketId
socket.on('socketId',(id)=>{
// ... dosomething
// 当收到服务端传来的通信请求(我被人通信啦!)
socket.on("beCalled",(data)=>{
//... do something
})
})
// 当收到服务端传来的结束通话信息
socket.on("bothCallEnd",()=>{
// ... dosomething
})
},[])
// 接通电话
const answerCall = () =>{
// ... dosomething
// 接通方告诉服务端 "我要接通电话" ,并且传递数据
socket.emit("answerCall", {
// 接通方peer信号
signalData: data,
// 拨打方socketId
to:ohterInfo.ohterId,
// 接通方socketId
from:myId,
// 接通方名字
name:myName,
})
}
// 挂断按钮回调
const endCall = () =>{
// 通知服务端要结束通话了
socket.emit("endCall",{
to:ohterInfo.ohterId
})
// ... dosomething
}
// 打给对方
const callOhter = () =>{
// 向服务端发送通话请求并传递相关数据
socket.emit("callOhter", {
// 接通方socketId
to: otherId,
// 拨打方peer信号
signalData: data,
// 拨打方socketId
from: myId,
// 拨打方名字
name: myName
})
// 对方成功接通事件触发
socket.on("otherAccepted",(data)=>{
setAccepted(true)
// 存储接通方信息
setOhterInfo({
name:data.name,
ohterId:data.from,
otherSignal:data.signal
})
})
}
return ...
}
export default App;
如何实时传递对方的音视频流然后显示?
这里其实我在介绍WebRtc的时候也给出大概的答案,其实我们就是要建立一个点对点连接,然后通过这个连接拿到我们的音视频流(后面就使用 stream 代替),那怎么建立一个点对点连接呢?介绍中也说了WebRtc的点对点连接就由 RTCPeerConnection
接口处理,我们这里直接使用simple-peer这个包来实现,感兴趣的可以看看它的文档。
这地方的核心思路就是,我们总共需要两个peer(拨打方一个peer,接通方一个peer) ,将peer1的signal信号传给peer2,将peer2的信号传给peer1,然后这两个peer建立了的一个点对点连接,然后就可以通过 peer.on("stream", (stream)=>{})拿到远程对方的stream,然后再将stream给到对应的video标签即可。
peer建立的流程如下图:
结合peer之后的客户端细代码如下(含详细注释解释):
js
import { useState,useRef,useEffect } from 'react'
import { Input,Typography,Button, message } from 'antd';
import { CopyOutlined,WhatsAppOutlined } from '@ant-design/icons'
import io from "socket.io-client"
import Peer from "simple-peer"
import './App.css';
// 建立客户端和5000端口的服务端的ws连接
const socket = io.connect('http://localhost:5000')
function App() {
// 当前视频语音流
const [ stream, setStream ] = useState()
// 是否接受通话
const [ accepted, setAccepted ] = useState(false)
// 是否为正在接通状态
const [ panding, setPanding] = useState(false)
// myVideo实例
const myVideo = useRef()
// otherVideo实例
const otherVideo = useRef()
// 我的名字
const [ myName , setMyName] = useState('')
// 拨打方信息
const [ ohterInfo, setOhterInfo] = useState({})
// 我的通话id
const [ myId, setMyId ] = useState('')
// 对方的通话id
const [ otherId, setOtherId ] = useState('')
// peer链接
const webRtcConnectionRef = useRef()
useEffect(()=>{
// ...
// 连接上socket时获取socketId
socket.on('socketId',(id)=>{
// 存储socketId,这是客户端的socket唯一标识,传递信息需要用到
setMyId(id)
// 当收到服务端传来的通信请求(我被人通信啦!)
socket.on("beCalled",(data)=>{
// 设置待接听状态为true
setPanding(true)
// 存储拨打方信息
setOhterInfo({
// 拨打方名字
name:data.name,
// 拨打方socketId
ohterId:data.from,
// 拨打方peer信号
otherSignal:data.signal
})
})
})
// 当收到服务端传来的结束通话信息
socket.on("bothCallEnd",()=>{
// 就做结束通话的处理
handleEndCall()
})
},[])
// 复制自己的通话id(这里的通话id就是socketId)
const onCopyId = () =>{
if(myId){
navigator.clipboard.writeText(myId).then(()=>{
message.success('复制成功')
})
}else{
message.error('通话id不存在')
}
}
// 接通电话
const answerCall = () =>{
// 设置已接受状态
setAccepted(true)
// 新建一个peer实例
const peer2 = new Peer({
initiator: false,
trickle: false,
stream: stream
})
// 用ref存储当前peer实例
webRtcConnectionRef.current = peer2
peer2.on("signal",(data)=>{
// 接通方的signal要传给拨打方的peer(通过我们的server)
socket.emit("answerCall", {
// 接通方peer信号
signalData: data,
// 拨打方socketId
to:ohterInfo.ohterId,
// 接通方socketId
from:myId,
// 接通方名字
name:myName,
})
})
// 获取远程的音视频流
peer2.on("stream",(stream)=>{
// 将音视频流展示在video标签上
if(otherVideo.current){
otherVideo.current.srcObject = stream
}
})
// 将拨打方的信号传给接收方的peer2(建立webrtc连接的过程)
peer2.signal(ohterInfo.otherSignal)
}
// 挂断按钮回调
const endCall = () =>{
// 通知服务端要结束通话了
socket.emit("endCall",{
to:ohterInfo.ohterId
})
// 自己也执行结束通话回调
handleEndCall()
}
// 结束通话处理函数
const handleEndCall = () =>{
// 设置结束的各种状态
setAccepted(false)
setPanding(false)
// 销毁webtrtc连接
if(webRtcConnectionRef.current){
webRtcConnectionRef.current.destroy()
}
}
// 打给对方
const callOhter = () =>{
// 建立一个webrtc链接
const peer = new Peer({
initiator: true,
trickle: false,
stream: stream
})
// 用ref保持这个链接
webRtcConnectionRef.current = peer
// 当peer配置initiator: true时,signal事件会立刻触发
peer.on("signal", (data) => {
// 向服务端发送通话请求
socket.emit("callOhter", {
// 接通方socketId
to: otherId,
// 拨打方peer信号
signalData: data,
// 拨打方socketId
from: myId,
// 拨打方名字
name: myName
})
})
// 获取远程的音视频流
peer.on("stream", (stream) => {
// 将音视频流展示在标签上
if(otherVideo.current){
otherVideo.current.srcObject = stream
}
})
// 解决挂断再次连接之后的cannot signal after peer is destroyed 报错
peer.on('close', () => { socket.off("otherAccepted"); });
// 对方成功接通事件
socket.on("otherAccepted",(data)=>{
setAccepted(true)
// 将接通方peer2的信号给拨打方peer,至此webrtc的通信建立,stream中开始流通数据
peer.signal(data.signal)
// 存储接通方信息
setOhterInfo({
name:data.name,
ohterId:data.from,
otherSignal:data.signal
})
})
}
return ... (jsx代码就省略了,相信这个大家都会写)
}
export default App;
最后大致效果如下图:
待接听状态效果图:
已接听状态效果图:
最后总结
本文主要基于WebRtc和WebSocket技术实现了一个可以视频聊天的web网页,其中涉及了WebRtc,WebScoket,node服务以及React相关的知识,希望能给你带来一些帮助~