Nginx实现P2P视频通话
一.搭建虚拟机
1.下载Ubuntu镜像(iso文件)
官方地址:https://ubuntu.com/download
清华源:https://mirrors.tuna.tsinghua.edu.cn/ubuntu-releases/
阿里云:https://mirrors.aliyun.com/ubuntu-releases/
版本为:[ubuntu-22.04.5-desktop-amd64.iso]
2.安装虚拟机
创建新的虚拟机—>自定义(高级)—>Workstation 15.x—>稍后安装操作系统—>Linux(Ubuntu 64位)—>名词、位置—>处理器数量、内核数量—>内存—>使用网络地址转换NAT—>LSO Login(L)—>SCSI---->创建新虚拟磁盘—>最大磁盘大小(拆分)—>磁盘文件名—>自定义硬件如下即可完成:
3.启动虚拟机
开启虚拟机—>Try or install Ubuntu —>English install Ubuntu---->install 内勾选1,3—>Erase dist and install Ubuntu ---->在install 内输入个人机信息即可
注:在VM内部安装时,如果要选择清空直接,可直接清空,因为此处的清空指得的是分配的内存部分,不包括其他部分。
4.网络
由于选取了NAT模式,即为通过主机来连接外部网络,不需要单独设置,IP 由命令行ip addr获取
二.搭建服务器
1.搭建基础环境
1.换源与更新系统
# 1. 备份原始的源列表文件
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak# 2. 使用 sed 命令,一键替换为阿里云的镜像源
# (你也可以选择清华、中科大等其他镜像源)
sudo sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list# 3. 更新软件源信息
sudo apt update# 4. 升级所有已安装的软件包到最新版本
# 期间可能会有提示,一般按回车或输入 'Y' 即可
sudo apt upgrade -y
2.安装必备开发工具
# 安装 build-essential, 它包含了一整套编译工具,如 gcc, g++, make
sudo apt install build-essential -y# 安装 Git, 用于代码版本控制和下载源码
sudo apt install git -y# 安装 CMake, 一个现代化的编译构建工具,很多 C++ 项目会用到
sudo apt install cmake -y# 安装其他常用工具:curl, wget (网络请求), unzip (解压)
sudo apt install curl wget unzip -y
你可以通过 gcc --version, git --version, cmake --version 等命令来验证它们是否已成功安装
2.安装Node.js环境(信令服务器)
# 1. 下载并运行 nvm 的官方安装脚本。
# 这个命令会从 GitHub 下载脚本并直接执行它。
curl -o- https://gitee.com/mirrors/nvm-install/raw/master/install.sh | bash#2. 验证 nvm 是否安装成功:
command -v nvm#3. 安装 Node.js:
nvm install 18
nvm use 18
nvm alias default 18
node -v
npm -v
3.安装FFmpeg
#1. 安装 FFmpeg
sudo apt install ffmpeg -y
ffmpeg -version
#2. 安装编译原生 WebRTC 库所需的依赖 (可选,几百MB,但新手不推荐)
sudo apt install libgtk-3-dev libasound2-dev libxss-dev libxtst-dev libnss3-dev -y
4.部署 Web 服务器 Nginx
# 1. 使用 apt 安装 Nginx
sudo apt install nginx -y# 2. 启动 Nginx 服务
sudo systemctl start nginx# 3. 设置 Nginx 为开机自启动,这样虚拟机重启后它会自动运行
sudo systemctl enable nginx
# 4. 检查服务状态,按 q 键可以退出状态查看。
sudo systemctl status nginx
通过浏览器访问验证安装是否成功(NAT需要配置端口转发)
获取虚拟机 IP: 在终端输入 ip addr,找到 ens33 网卡下的 inet 地址,比如 192.168.128.130。
配置端口转发:
关闭虚拟机。
打开 VMware 中该虚拟机的“设置” -> “网络适配器” -> “高级” -> (或者直接在网络适配器设置里找到) “端口转发”。
添加一条新的规则:
主机端口 (Host Port): 比如 8080 (确保这个端口在你的 Windows 上没被其他程序占用)。
虚拟机 IP 地址 (Virtual machine IP address): 填入你刚才找到的 IP,如 192.168.128.130。
虚拟机端口 (Virtual machine port): 填 80 (这是 Nginx 的默认 HTTP 端口)。
名称可以随便填,比如 nginx-http。
保存所有设置。
启动虚拟机。
测试: 在你Windows 主机的浏览器中,打开地址:http://localhost:8080。
如果你看到了一个标题为 “Welcome to nginx!” 的页面,那么恭喜你,Nginx 已经成功部署并可以通过端口转发访问了!
端口转发
1.首先,关闭这个“虚拟机设置”窗口,回到 VMware 的主界面。2.在 VMware Workstation 的主菜单栏,点击 “编辑(E)”,然后从下拉菜单中选择 “虚拟网络编辑器(N)...”。(如果提示需要管理员权限,请点击“是”或“确定”)3.现在会打开一个名为“虚拟网络编辑器”的新窗口。在这个窗口的上半部分,你会看到一个列表,里面有 VMnet0 (桥接模式)、VMnet1 (仅主机模式) 和 VMnet8 (NAT 模式) 等。4.用鼠标选中列表中的 VMnet8 这一行。5.选中 VMnet8 后,查看窗口下半部分的 “子网 IP” 和 “子网掩码”。你应该能看到192.168.74.0 这样的信息,这确认了 VMnet8 就是你虚拟机当前正在使用的 NAT 网络。6.现在,看 VMnet8 设置区域的右边,你会看到一个 “NAT 设置(S)...” 的按钮。请点击它!7.这时会弹出一个新的“NAT 设置”窗口。在这个窗口的中间,有一个区域叫做 “端口转发” (Port Forwarding)。下面有一个 “添加(A)...” 按钮。这里就是我们最终要找的目标!
8.点击 “添加(A)...”,然后填写信息:主机端口: 8080虚拟机 IP 地址: 192.168.74.128虚拟机端口: 80描述: Nginx-HTTP
9.一路“确定”到底:点击“确定”保存端口转发规则。点击“确定”或“应用”保存 NAT 设置。点击“确定”关闭虚拟网络编辑器。启动你的 Ubuntu 虚拟机,然后在 Windows 浏览器里访问 http://localhost:8080 进行测试。
解释 ip addr的两个IP地址
inet 192.168.74.128/24 brd 192.168.74.255 scope global dynamic noprefixroute ens33
- inet:
- 表示这是一个 IPv4 地址。如果是 IPv6,这里会显示 inet6。
- 192.168.74.128/24:
- 这是最核心的部分,它包含了两个信息:IP 地址 和 子网掩码(255.255.255.0)。
- brd 192.168.74.255:
- brd 是 Broadcast (广播) 的缩写。
- 192.168.74.255 是这个网络的广播地址。
- scope global:
- scope (范围) 定义了这个 IP 地址的有效性范围。global表示全局有效
- dynamic:
- 表示这个 IP 地址是**动态分配(DHCP)**的,重启时可能会改变。
- noprefixroute:
- 这是一个路由相关的选项,对于初学者可以暂时忽略。它大致意味着不会为这个地址所在的整个子网(192.168.74.0/24)自动创建一条路由规则。
- ens33:
- 表示以上所有信息都是属于 ens33 这个网络接口的。
5.小知识
1.基础环境、Node.js、FFmpeg、Nginx分别是什么
1.基础环境
主要包括 gcc/g++ (C/C++编译器)、make (项目构建工具)、CMake (更现代的构建系统)、Git (代码仓库管理工具)
作用: 确保服务器具备从源码构建和安装高性能软件的能力。 对于音视频领域来说,很多核心组件都是 C++ 编写的,所以这个“施工队”至关重要。2.Node.js
一个让 JavaScript 能够脱离浏览器,直接在服务器上运行的环境。它以其异步非阻塞 I/O 的特性而闻名。充当信令服务器的作用
信令服务器: 学生 A 想和学生 B 视频通话,但他们互相不认识。A 会把自己的“名片”(SDP Offer)交给传达室(Node.js 服务器),传达室再把名片转交给 B。B 收到后,也把自己的名片(SDP Answer)通过传达室回传给 A。经过几次这样的信息中转,A 和 B 才能建立直接联系。这个中转“名片”和“地址”(ICE Candidate)的过程,就是信令 (Signaling)。
作用: 负责处理信令交换和业务逻辑。 Node.js 非常擅长处理大量、轻量级的并发连接(如 WebSocket),这正是信令服务器的典型场景。它不处理音视频数据本身,只负责“牵线搭桥”。3.FFmpeg
一个音视频处理的命令行工具集和程序库,能处理几乎所有关于音视频格式转换、编解码、剪辑、加水印、推流、录制等任务。
作用: 为服务器提供强大的后台音视频处理和“杂活”能力。 它是对实时音视频能力的补充,负责处理所有非实时的、文件性的音视频任务。4.Nginx
一个高性能的 HTTP 服务器和反向代理服务器
角色: 【学校大门 & 总接待处】学校大门 (静态 Web 服务器): 任何想参加在线课堂的人,首先需要访问学校的网站。这个网站的页面(HTML)、样式表(CSS)、图片和客户端脚本(JavaScript),都存放在 Nginx 里。Nginx 负责把这些“学校介绍手册”高效地发给来访者(浏览器)。总接待处 (反向代理): 访客来到学校后,有不同的需求:想找“教务处”(Node.js 信令服务),总接待处 Nginx 会给他指路,把请求转发到 Node.js 程序的端口。想看“学校宣传片”(普通的视频文件),总接待处 Nginx 自己就能处理,直接把视频文件给他。未来,如果学校规模大了,开了多个“教务处”,总接待处 Nginx 还能做负载均衡,看哪个教务处比较闲,就把访客引导过去。门禁安保 (SSL/TLS): Nginx 还能负责在学校大门口进行安全检查,部署 HTTPS,确保所有进出的信息都是加密的。
作用: 作为整个服务的前端入口,负责托管网站、分发请求和提供安全防护。 它可以让多个后台服务(如 Node.js, 媒体服务器等)共用一个域名和端口,使系统架构更清晰、更安全、更易于扩展。
总结
组件 角色 核心作用
基础环境 地基 & 施工队 提供编译和安装高性能软件的能力
Node.js 教务处 & 传达室 处理信令交换和业务逻辑
FFmpeg 后期制作 & 录播室 提供后台音视频文件处理和格式转换能力
Nginx 学校大门 & 总接待处 作为服务总入口,托管网页并反向代理请求
三.从零构建-手写一个简单的信令服务器,实现P2P
1.创建项目目录
mkdir ~/webrtc-signaling-server
cd ~/webrtc-signaling-server
2.初始化 Node.js 项目
npm init -y
npm install ws # 安装最流行的 WebSocket 库
3.编写信令服务器代码 (server.js):
代码逻辑很简单:监听 WebSocket 连接。当收到一个客户端发来的消息时,把这个消息广播给所有其他连接的客户端。它就像一个纯粹的“消息中转站”。
// 'use strict'; 开启严格模式,是一种良好的编程习惯// 1. 引入我们刚刚安装的 'ws' 库,并从中获取 WebSocketServer 类
const { WebSocketServer } = require('ws');// 2. 创建一个新的 WebSocketServer 实例,并让它在 8080 端口上监听连接
const wss = new WebSocketServer({ port: 8080 });console.log('信令服务器已启动,正在监听 8080 端口...');// 3. 监听 'connection' 事件,当有新的客户端连接进来时,这个函数就会被触发
// 'ws' 参数就是代表这个新连接的 WebSocket 对象
wss.on('connection', function connection(ws) {console.log('一个新的客户端已连接!');// 4. 监听这个新连接的 'message' 事件,当这个客户端发来消息时,此函数触发ws.on('message', function message(data) {console.log('收到消息 => %s', data);// 5. 核心逻辑:广播消息// 遍历所有已连接的客户端wss.clients.forEach(function each(client) {// 判断一下,不要把消息发回给发送者自己,并且对方处于连接状态if (client !== ws && client.readyState === ws.OPEN) {// 将收到的消息原封不动地转发给其他客户端client.send(data.toString());}});});// 6. 监听 'close' 事件,当客户端断开连接时触发ws.on('close', () => {console.log('一个客户端已断开连接。');});});
运行后出现:信令服务器已启动,正在监听 8080 端口…即为成功
4.编写前端页面 (index.html 和 client.js)
1.创建一个 public 文件夹,专门用来存放所有前端文件。这是一个标准的做法。
mkdir public
2.创建 HTML 文件。
nano public/index.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Simple WebRTC P2P Call</title><style>body { font-family: sans-serif; }video { border: 2px solid black; width: 480px; height: 360px; }.video-container { display: flex; gap: 20px; }</style>
</head>
<body><h1>Simple WebRTC P2P Call</h1><div class="video-container"><div><h2>Your Video</h2><video id="localVideo" autoplay muted playsinline></video></div><div><h2>Remote Video</h2><video id="remoteVideo" autoplay playsinline></video></div></div><button id="startButton">Start Call</button><script src="client.js"></script>
</body>
</html>
3.创建 JavaScript 文件
nano public/client.js
# client.js
'use strict';// 新增函数:停止已存在的媒体轨道
// ===================================================================
function stopExistingTracks() {if (localStream) {console.log("正在停止旧的媒体流轨道...");localStream.getTracks().forEach(track => {track.stop();});}
}const startButton = document.getElementById('startButton');
const localVideo = document.getElementById('localVideo');
const remoteVideo = document.getElementById('remoteVideo');let localStream;
let peerConnection;
let isCaller = false; // 用一个变量来标记谁是发起方// ===================================================================
// 再次确认这里的 IP 地址是你虚拟机的 IP 地址!
// ===================================================================
const serverIp = '192.168.74.128';
const signalingServer = new WebSocket(`ws://${serverIp}:8080`);// 页面加载后立即执行初始化
initialize();async function initialize() {// 在所有操作之前,先调用清理函数stopExistingTracks();try {// 1. 立即获取本地媒体流localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });localVideo.srcObject = localStream;console.log("本地媒体流获取成功");// 2. 立即创建 PeerConnectioncreatePeerConnection();console.log("PeerConnection 已创建");} catch (e) {console.error("初始化失败:", e);// 如果错误是 Device in use,给出友好提示if (e.name === 'NotReadableError') {alert('摄像头或麦克风正在被占用。请尝试关闭其他使用摄像头的程序或标签页,然后刷新页面。');}}
}signalingServer.onopen = () => {console.log("成功连接到信令服务器!");
};signalingServer.onmessage = async (msg) => {const message = JSON.parse(msg.data);console.log("收到信令消息:", message);if (message.offer && !isCaller) {// 如果是 offer 且自己不是发起方,则处理peerConnection.setRemoteDescription(new RTCSessionDescription(message.offer));const answer = await peerConnection.createAnswer();await peerConnection.setLocalDescription(answer);signalingServer.send(JSON.stringify({ 'answer': peerConnection.localDescription }));console.log("已发送 Answer");} else if (message.answer) {// 如果是 answer,设置远端描述peerConnection.setRemoteDescription(new RTCSessionDescription(message.answer));console.log("远端描述(Answer)已设置");} else if (message.candidate) {// 如果是 ICE candidate,添加到连接中try {await peerConnection.addIceCandidate(new RTCIceCandidate(message.candidate));} catch (e) {console.error('添加 ICE Candidate 失败:', e);}}
};startButton.addEventListener('click', () => {isCaller = true; // 标记自己是发起方startCall();
});async function startCall() {console.log("开始呼叫,创建 Offer...");const offer = await peerConnection.createOffer();await peerConnection.setLocalDescription(offer);signalingServer.send(JSON.stringify({ 'offer': peerConnection.localDescription }));console.log("已发送 Offer");
}function createPeerConnection() {peerConnection = new RTCPeerConnection(null);peerConnection.onicecandidate = (event) => {if (event.candidate) {signalingServer.send(JSON.stringify({ 'candidate': event.candidate }));}};peerConnection.ontrack = (event) => {console.log("收到远程媒体流!");remoteVideo.srcObject = event.streams[0];};// 将本地流的轨道添加到连接中localStream.getTracks().forEach(track => {peerConnection.addTrack(track, localStream);});
}
5.配置 Nginx
1.编辑 Nginx 的默认站点配置文件
sudo nano /etc/nginx/sites-available/default
2.修改 root 指令
在打开的文件里,找到这样一行:root /var/www/html;
把它修改为指向我们刚刚创建的 public 文件夹的绝对路径。请注意,要把 lxmx 换成你自己的用户名:root /home/lxmx/webrtc-signaling-server/public;
3.保存并退出 (Ctrl+O, Enter, Ctrl+X)
4.重新加载 Nginx 配置使其生效
sudo systemctl reload nginx
如果没有任何错误提示,说明配置成功。
6.测试
- 确认状态:
- 你的第一个终端里,node server.js 仍在运行。
- Nginx 已经重新加载了新配置。
- VMware 的端口转发规则 主机8080 -> 虚拟机80 已经配置好。
- 开始测试:
- 在你的 Windows 主机上,打开 Chrome 浏览器。
- 打开第一个标签页,访问 http://localhost:8080。
- 浏览器会弹出请求,询问是否允许使用摄像头和麦克风。请点击“允许”。你会看到你自己的画面出现在 “Your Video” 框里。
- 再打开一个“隐身模式”的 Chrome 窗口(或者用另一个不同的浏览器,比如 Edge)。
- 在第二个窗口里,也访问 http://localhost:8080。
- 同样,**点击“允许”**使用摄像头和麦克风。你也会看到自己的画面。
- 现在,回到第一个浏览器标签页,点击页面上的 “Start Call” 按钮。
- 见证奇迹:
- 观察你运行 node server.js 的那个终端,你会看到一堆消息飞快地打印出来,显示“收到消息…”。
- 几秒钟后,你应该能在第一个浏览器窗口的 “Remote Video” 框里,看到第二个窗口的摄像头画面!反之亦然。
7.可能出现的问题
1.root 路径配置错误
在3.5.2时,修改root指令的路径,必须是绝对路径,可进入此路径后,用pwd打印,再复制即可
2.文件权限问题
Nginx是特殊用户,不一定有权限读取文件,默认是没有全选的,
- 检查权限:
ls -ld /home/$USER # $USER 会自动替换成你的用户名 lxmx
- 修复权限
# 给你的主目录增加 'execute' 权限
chmod o+x /home/$USER# 为了保险起见,也确保项目目录和 public 目录是可访问的
chmod -R o+rx ~/webrtc-signaling-server
chmod o+x: 给 other (其他用户) + (增加) execute (执行/进入) 权限。对于目录来说,执行权限就意味着可以进入该目录。
chmod -R o+rx: -R (递归地) 给 other (其他用户) 增加 read (读取) 和 execute (进入) 权限。
执行完权限修改后,不需要重启 Nginx,因为这是文件系统层面的更改。
3.设备被占用:NotReadableError: Device in use
同一台物理电脑,不可访问同一物理摄像头
解决方案:使用虚拟摄像头,OBS Studio
- 在你的 Windows 主机上下载并安装 OBS Studio。
- 官方网站: https://obsproject.com/
- 配置 OBS - 捕获真实摄像头:
- 打开 OBS。在下方的“来源(Sources)”面板,点击 + 号。
- 选择“视频采集设备(Video Capture Device)”。
- 创建一个新的源,在设备下拉列表中,选择你的真实物理摄像头(比如 “Integrated Webcam”)。
- 点击“确定”,你应该能在 OBS 的预览窗口里看到你的摄像头画面了。
- 启动 OBS 的虚拟摄像头:
- 在 OBS 主界面的右下方“控件(Controls)”面板,找到并点击 “启动虚拟摄像机”(Start Virtual Camera) 按钮。
- 一旦启动,Windows 系统里就会多出一个名为 “OBS Virtual Camera” 的新摄像头设备。
- 在浏览器中使用虚拟摄像头:
- 在 Edge 浏览器中: 打开你的 WebRTC 页面 http://localhost:8080。当浏览器询问使用哪个摄像头时(通常地址栏左侧会有一个摄像头图标让你选择),选择 “OBS Virtual Camera” 而不是你的物理摄像头。
- 在 Chrome 浏览器中: 打开你的 WebRTC 页面。同样,当它请求摄像头权限时,也选择 “OBS Virtual Camera”。
- 见证奇迹:
- 现在,Edge 和 Chrome 访问的都是 OBS 这个“虚拟”的摄像头。
- OBS 作为一个中间层,负责从物理摄像头获取一次画面,然后把它分发给所有请求 “OBS Virtual Camera” 的应用程序。
- 这样,两个浏览器就都可以同时显示画面了,并且 Device in use 错误会彻底消失。
四.关闭nginx服务器
# 1. 停用 WebRTC 的 Nginx
sudo systemctl stop nginx
sudo systemctl disable nginx# 2. 启动 RTMP 的 Nginx
sudo /usr/local/nginx/sbin/nginx# 3. 查看状态
sudo systemctl status nginx