【WebRTC实现点对点视频通话】

宇豪学习录 2024-08-11 12:03:01 阅读 67

介绍

WebRTC (Web Real-Time Communications) 是一个实时通讯技术,也是实时音视频技术的标准和框架。简单来说WebRTC是一个集大成的实时音视频技术集,包含了各种客户端api、音视频编/解码lib、流媒体传输协议、回声消除、安全传输等。对于开发者来说可以借助webrtc非常方便的实现低延时视频通话能力。目前大多主流的直播系统、会议系统基本都是基于WebRTC来实现。

三种架构

WebRTC针对不同场景以及性能考虑提供了三种架构Mesh架构、MCU、FSU。

在这里插入图片描述

Mesh架构

Mesh架构,需要所有参与连接的peer建立与所有其他peer的媒体连接(两两连接)。该架构需要n-1个上下行,以此带来的带宽消耗(流量)、编/解码消耗(设备性能)成线性增长。该架构只能适用3-4个人的小型会议场景。

MCU架构

所有参与连接的peer将本地媒体流推到远程媒体服务器,由媒体服务器进行混流,然后再推到所有连接的peer端。该架构的优点就是只需要1路上下行,随着peer人数不断增加,依然不会对用户造成带宽、手机性能影响。该架构将压力转嫁到服务端,由专用媒体服务器来完成混流,转推等功能。

SFU架构

相对于MCU来说SFU只做转发,媒体服务器压力有限。与Mesh架构相比,只需要n-1个下行,1个上行,减少了服务器压力。在大规模的场合该架构具有伸缩性。

点对点视频连接

根据上面,我们对基本WebRTC有了最基本的认识,下面就从点对点实际例子来从代码角度进一步了解其原理。

先从下图来看看使用MCU来实现点对点需要哪些东西

在这里插入图片描述

在介绍流程之前,先简单介绍下上图中出现的名词代表什么意思:

Peer:通信双方设备。Signaling Server: 信令服务器,用于交互连接双方的信令数据(SDP、ICE等),以保证通信的对等连接建立。NAT:处理私有网络和公共网络之间的地址转换问题(因为大多数设置都处于内网中,需要转换为公共网络才能进行外网访问)STUN:用于发现设备的公共地址(通过NAT转换的公网地址),辅助穿越NAT进行点对点连接。TURN:在无法建立直接连接时提供数据中继,确保通信的可靠性。对等连接异常时的兜底方案。SDP:会话描述协议,用于描述和协商媒体会话的协议,它定义了会话的所有技术细节,包括媒体格式、编解码器、网络地址等。,ICE:用于发现和选择最优网络路径的框架,确保在各种网络环境下都能成功建立和维持连接。

代码实现

实现点对点连接主要是两点:1、信令数据交互 2、对等连接建立

在代码中使用到了socket.io来将设备和信令服务器通信,使用了simple-peer来建立对等连接,由于该demo在本地运行所以没有使用STUN/TRUN服务器,有兴趣的可以使用Chrome提供的公共服务器<code>stun:stun.l.google.com:19302

主要步骤如下:

1、和信令服务器建立连接,并获取自身的socketId作为唯一标识2、申请方将信令(由simple-peer生成)通过信令服务器到达接受方3、接受方接受,将发起方的信令保存到对等连接peer中,并且将自己的信令通过信令服务器给到发送方4、发送方将接受方的信令数据保存到对等连接peer中,至此发送方-接受方对等连接建立完成5、在发送方和接受方监听peer的stream,来获取视频流,然后展示在页面

和信令服务器建立连接

新建一个server,js使用node+express搭建的简易信令服务器,用于交换双方信令。通过create-react-app来创建一个前端页面。

信令服务器代码如下:

const express = require("express");

const http = require("http");

const cors = require("cors");

const app = express();

const server = http.createServer(app);

app.use(cors);

const io = require("socket.io")(server, { -- -->

cors: {

origin: "*",

methods: ["POST", "GET"],

},

});

server.listen(5001, () => {

console.log("listening on 5000 ...");

});

io.on("connection", (socket) => {

// 分发socket id

socket.emit("offer", socket.id);

// 发送发起方的信令数据别answer

socket.on("callUser", (data) => {

io.to(data.answerId).emit("callUser", data);

});

// 发送接收放信令给申请方

socket.on("answerSignalInfo", (data) => {

io.to(data.to).emit("answerSignalInfo", data);

});

socket.on("disconnect", () => {

socket.broadcast.emit("callEnded", socket);

});

});

// frontend

// 通过socket.io和服务器进行连接

const socket = io("http://localhost:5001");

// 获取自身的socket id

socket.on("offer", (offerId) => {

console.log("offer socket ID", offerId);

setOfferId(offerId);

getLocalStream(); // 获取本地视频流

});

传递信令数据

// 通过simple-peer 交换信令数据 offer -> 信令服务器 -> answer

const peer = new Peer({

initiator: true, // 是否是发起方

stream: localStream, // 传递的视频流

trickle: false, // 点对点传输,获取单个信号

// 设置STUN服务器,Chrome提供的公共服务器

config: {

iceServers: [{ urls: "stun:stun.l.google.com:19302" }],

},

});

peer.on("signal", (data: any) => {

socket.emit("callUser", {

singleData: data, // 发送通话方的信令数据

answerId: answerId, // 需要和谁通话

from: offerId, // 谁申请通话

});

});

接收信令数据

接收方接收发起方的信令数据,并保存到Peer中,然后将自身的信令数据返回给发起方

const peer = new Peer({

initiator: false,

stream: localStream,

trickle: false,

config: {

iceServers: [{ urls: "stun:stun.l.google.com:19302" }],

},

});

peer.on("signal", (data) => {

socket.emit("answerSignalInfo", {

answerSignalInfo: data,

to: offerUserInfo?.id,

from: offerId,

});

});

if (offerUserInfo?.singleData) {

peer.signal(offerUserInfo.singleData);

}

对等连接建立,获取双方视频流

交互信令之后,通过simple-peer成功建立对等连接,监听stream视频流然后显示在页面上

// 监听通过对等连接传递的stream

peer.on("stream", (stream) => {

if (remoteVideoRef.current) {

remoteVideoRef.current.srcObject = stream;

remoteVideoRef.current.play();

}

});

完整页面代码:CSS样式文件省略

import React, { useCallback, useEffect, useRef, useState } from "react";

import { io } from "socket.io-client";

import Peer from "simple-peer";

import "./App.css";

const socket = io("http://localhost:5001");

type UserInfo = {

singleData: any;

id: string;

};

function App() {

// 用于引用 DOM 元素

const localVideoRef = useRef<HTMLVideoElement>(null);

const remoteVideoRef = useRef<HTMLVideoElement>(null);

// 用于管理状态

const [localStream, setLocalStream] = useState<MediaStream | undefined>();

const [offerId, setOfferId] = useState("");

const [answerId, setAnswerId] = useState("");

const [offerUserInfo, setOfferUserInfo] = useState<UserInfo>();

// 获取本地视频流

const getLocalStream = useCallback(async () => {

try {

const stream = await navigator.mediaDevices.getUserMedia({

video: {

width: { ideal: 200 }, // 理想的宽度

height: { ideal: 200 }, // 理想的高度

},

audio: false,

});

console.log("local media", stream);

setLocalStream(stream);

if (localVideoRef.current) {

localVideoRef.current.srcObject = stream;

}

} catch (error) {

console.error("Error accessing media devices.", error);

}

}, []);

// 手动设置通话方id

const onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {

console.log("onChange call id", e);

setAnswerId(e.target.value);

}, []);

// 获取信令牌服务器发送的socket id

const init = useCallback(() => {

socket.on("offer", (offerId) => {

console.log("offer socket ID", offerId);

setOfferId(offerId);

getLocalStream(); // 获取本地视频流

});

// 监听信令服务器发送的通话申请方的信令牌数据

socket.on("callUser", ({ singleData, answerId, from }) => {

console.log(`${ from}发起通话`, from);

setOfferUserInfo({

singleData: singleData,

id: from,

});

});

}, [getLocalStream]);

// 创建和发送 offer

const startCall = useCallback(async () => {

// 通过simple-peer 交换信令数据 offer -> 信令服务器 -> answer

const peer = new Peer({

initiator: true,

stream: localStream,

trickle: false,

// 设置STUN服务器,Chrome提供的公共服务器

config: {

iceServers: [{ urls: "stun:stun.l.google.com:19302" }],

},

});

peer.on("signal", (data: any) => {

socket.emit("callUser", {

singleData: data, // 发送通话方的信令数据

answerId: answerId, // 需要和谁通话

from: offerId, // 谁申请通话

});

});

// 获取到接收方的信令数据

socket.on("answerSignalInfo", (data) => {

console.log(`${ data.from}已经接受通话`, data, peer);

peer.signal(data.answerSignalInfo);

});

// 监听通过对等连接传递的stream

peer.on("stream", (stream) => {

if (remoteVideoRef.current) {

remoteVideoRef.current.srcObject = stream;

remoteVideoRef.current.play();

}

});

// setPeer(peer);

}, [answerId, localStream, offerId, remoteVideoRef]);

const acceptCall = useCallback(() => {

const peer = new Peer({

initiator: false,

stream: localStream,

trickle: false,

config: {

iceServers: [{ urls: "stun:stun.l.google.com:19302" }],

},

});

peer.on("signal", (data) => {

socket.emit("answerSignalInfo", {

answerSignalInfo: data,

to: offerUserInfo?.id,

from: offerId,

});

});

if (offerUserInfo?.singleData) {

peer.signal(offerUserInfo.singleData);

}

// 监听通过对等连接传递的stream

peer.on("stream", (stream) => {

if (remoteVideoRef.current) {

remoteVideoRef.current.srcObject = stream;

remoteVideoRef.current.play();

}

});

}, [localStream, offerUserInfo, offerId, remoteVideoRef]);

useEffect(() => {

init();

}, [init]);

return (

<div className="App">code>

<video autoPlay muted ref={ -- -->localVideoRef} className="video" />code>

<video autoPlay muted ref={ -- -->remoteVideoRef} className="video" />code>

<input value={ -- -->answerId} onChange={ onChange} placeholder="call id" />code>

<button onClick={ -- -->startCall}>发起通话</button>

<button onClick={ acceptCall}>同意通话</button>

</div>

);

}

export default App;

至此可以启动项目,并本地浏览器打开两个tab即可体验点对点视频服务。

总结

点对点通信,主要就是信令数据的交换,知道通信双方具体的配置信息(通信参数、IP地址等)以保证对等连接的成功建立,然后传递视频流在页面展示。

其中信令服务器仅用于对等连接前的信令交换,不会进行数据传输。NAT是将设备内网地址转换为外网公共地址。STUN来获取设置的公网地址。TURN服务器是用于对等连接异常时的兜底方案,可进行数据传输。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。