基于WebRtc和WebSocket实现视频聊天

前置知识

什么是 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 应用程序中创建实时、双向的通信通道。它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

开始实践

这两个核心技术大致介绍了一下,现在我们就开始实现一个视频聊天!

问题分析

实现之前,我们先来分析一下我们需要解决哪些核心问题:

  1. 如何获取当前用户的视频和音频并且显示?
  2. 两个客户端如何互相建立联系,传递消息?
  3. 如何实时传递对方的音视频流然后显示?

实现思路

如何获取当前用户的视频和音频?

这个问题其实在我介绍 WebRtc 的时候已经给出了答案:这里我们需要使用 WebRTC 中的媒体捕获设备类型的 api ,并且我们这里需要使用到的设备是摄像头和麦克风,所以就是使用navigator.mediaDevices.getUserMedia() 来捕获当前用户的音视频,然后我们再使用video标签展示我们获取到的音视频流,代码如下:

  // 当前视频语音流
  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 是实验属性
基于WebRtc和WebSocket实现视频聊天

两个客户端如何互相建立联系,传递消息?

整体来说就是当客户端1发起通话时,要去发消息告诉服务端并且带上所需数据,然后服务端传达,然后客户端2同意接通之后,也要发送消息给服务端并且带上所需数据,服务端再次传达给客户端1,流程大致如图:这里省去了数据的传递

基于WebRtc和WebSocket实现视频聊天

服务端设计

首先,这里我们肯定需要一个服务,用来接收客户端的信息并且做一些信息的传递,我们可以新建一个 server.js 文件,然后用 express (用啥随意,我这里使用 express 是因为 socket.io 官网示例用的express)新建一个服务,然后这个服务需要用到 websocket(后文统一使用 ws 作为它的简写)来进行客户端和服务端的双向通信。

server.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消息传递逻辑):

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建立的流程如下图:
基于WebRtc和WebSocket实现视频聊天

结合peer之后的客户端细代码如下(含详细注释解释):

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实现视频聊天

已接听状态效果图:
基于WebRtc和WebSocket实现视频聊天

最后总结

本文主要基于WebRtc和WebSocket技术实现了一个可以视频聊天的web网页,其中涉及了WebRtc,WebScoket,node服务以及React相关的知识,希望能给你带来一些帮助~

原文链接:https://juejin.cn/post/7348362217145172006 作者:ZJR

(0)
上一篇 2024年3月21日 下午4:52
下一篇 2024年3月21日 下午5:02

相关推荐

发表回复

登录后才能评论