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.换源与更新系统

bash 复制代码
# 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.安装必备开发工具

bash 复制代码
# 安装 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环境(信令服务器)

bash 复制代码
# 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

bash 复制代码
#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

bash 复制代码
# 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需要配置端口转发)

c 复制代码
获取虚拟机 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 已经成功部署并可以通过端口转发访问了!

端口转发

Text 复制代码
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地址

bash 复制代码
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 :
    • brdBroadcast (广播) 的缩写。
    • 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分别是什么

reStructuredText 复制代码
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, 媒体服务器等)共用一个域名和端口,使系统架构更清晰、更安全、更易于扩展。
reStructuredText 复制代码
总结
组件			角色						   核心作用
基础环境	  地基 & 施工队			  提供编译和安装高性能软件的能力
Node.js	   教务处 & 传达室			处理信令交换和业务逻辑
FFmpeg	  后期制作 & 录播室		   提供后台音视频文件处理和格式转换能力
Nginx	 学校大门 & 总接待处		  作为服务总入口,托管网页并反向代理请求

三.从零构建-手写一个简单的信令服务器,实现P2P

1.创建项目目录

bash 复制代码
mkdir ~/webrtc-signaling-server
cd ~/webrtc-signaling-server

2.初始化 Node.js 项目

bash 复制代码
npm init -y
npm install ws  # 安装最流行的 WebSocket 库

3.编写信令服务器代码 (server.js):

​ 代码逻辑很简单:监听 WebSocket 连接。当收到一个客户端发来的消息时,把这个消息广播给所有其他连接的客户端。它就像一个纯粹的"消息中转站"。

js 复制代码
// '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 文件夹,专门用来存放所有前端文件。这是一个标准的做法。

bash 复制代码
mkdir public

2.创建 HTML 文件

bash 复制代码
nano public/index.html
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 文件

bash 复制代码
nano public/client.js
bash 复制代码
# 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. 立即创建 PeerConnection
        createPeerConnection();
        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 的默认站点配置文件

bash 复制代码
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 配置使其生效

bash 复制代码
sudo systemctl reload nginx

如果没有任何错误提示,说明配置成功。

6.测试

  1. 确认状态 :
    • 你的第一个终端里,node server.js 仍在运行。
    • Nginx 已经重新加载了新配置。
    • VMware 的端口转发规则 主机8080 -> 虚拟机80 已经配置好。
  2. 开始测试 :
    • 在你的 Windows 主机 上,打开 Chrome 浏览器
    • 打开第一个标签页,访问 http://localhost:8080
    • 浏览器会弹出请求,询问是否允许使用摄像头和麦克风。请点击"允许"。你会看到你自己的画面出现在 "Your Video" 框里。
    • 再打开一个"隐身模式"的 Chrome 窗口(或者用另一个不同的浏览器,比如 Edge)。
    • 在第二个窗口里,也访问 http://localhost:8080
    • 同样,**点击"允许"**使用摄像头和麦克风。你也会看到自己的画面。
    • 现在,回到第一个浏览器标签页 ,点击页面上的 "Start Call" 按钮。
  3. 见证奇迹 :
    • 观察你运行 node server.js 的那个终端,你会看到一堆消息飞快地打印出来,显示"收到消息..."。
    • 几秒钟后,你应该能在第一个浏览器窗口的 "Remote Video" 框里,看到第二个窗口的摄像头画面!反之亦然。

7.可能出现的问题

1.root 路径配置错误

在3.5.2时,修改root指令的路径,必须是绝对路径,可进入此路径后,用pwd打印,再复制即可

2.文件权限问题

Nginx是特殊用户,不一定有权限读取文件,默认是没有全选的,

  • 检查权限:
bash 复制代码
ls -ld /home/$USER  # $USER 会自动替换成你的用户名 lxmx
  • 修复权限
bash 复制代码
# 给你的主目录增加 'execute' 权限
chmod o+x /home/$USER

# 为了保险起见,也确保项目目录和 public 目录是可访问的
chmod -R o+rx ~/webrtc-signaling-server

​ chmod o+x: 给 o ther (其他用户) + (增加) execute (执行/进入) 权限。对于目录来说,执行权限就意味着可以进入该目录。

​ chmod -R o+rx: -R (递归地) 给 o ther (其他用户) 增加 r ead (读取) 和 execute (进入) 权限。

执行完权限修改后,不需要重启 Nginx,因为这是文件系统层面的更改。

3.设备被占用:NotReadableError: Device in use

同一台物理电脑,不可访问同一物理摄像头

解决方案:使用虚拟摄像头,OBS Studio

  1. 在你的 Windows 主机上下载并安装 OBS Studio。
  2. 配置 OBS - 捕获真实摄像头 :
    • 打开 OBS。在下方的"来源(Sources)"面板,点击 + 号。
    • 选择"视频采集设备(Video Capture Device)"。
    • 创建一个新的源,在设备下拉列表中,选择你的真实物理摄像头(比如 "Integrated Webcam")。
    • 点击"确定",你应该能在 OBS 的预览窗口里看到你的摄像头画面了。
  3. 启动 OBS 的虚拟摄像头 :
    • 在 OBS 主界面的右下方"控件(Controls)"面板,找到并点击 "启动虚拟摄像机"(Start Virtual Camera) 按钮。
    • 一旦启动,Windows 系统里就会多出一个名为 "OBS Virtual Camera" 的新摄像头设备。
  4. 在浏览器中使用虚拟摄像头 :
    • 在 Edge 浏览器中 : 打开你的 WebRTC 页面 http://localhost:8080。当浏览器询问使用哪个摄像头时(通常地址栏左侧会有一个摄像头图标让你选择),选择 "OBS Virtual Camera" 而不是你的物理摄像头。
    • 在 Chrome 浏览器中 : 打开你的 WebRTC 页面。同样,当它请求摄像头权限时,也选择 "OBS Virtual Camera"
  5. 见证奇迹 :
    • 现在,Edge 和 Chrome 访问的都是 OBS 这个"虚拟"的摄像头。
    • OBS 作为一个中间层,负责从物理摄像头获取一次画面,然后把它分发给所有请求 "OBS Virtual Camera" 的应用程序。
    • 这样,两个浏览器就都可以同时显示画面了,并且 Device in use 错误会彻底消失。

四.关闭nginx服务器

bash 复制代码
# 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
相关推荐
程序员老舅13 小时前
‌NAT穿透技术原理:P2P通信中的打洞机制解析‌
服务器·c++·网络协议·网络编程·p2p·nat·网络穿透
Natsume17101 天前
音视频开发入门:FFmpeg vs GStreamer,新手该如何选择?
c语言·c++·ffmpeg·音视频·webrtc·实时音视频·视频编解码
Antonio9152 天前
【音视频】WebRTC QoS 概述
音视频·webrtc
李姆斯3 天前
数据与直播画面“神同步”——SEI(补充增强信息)
前端·webrtc·音视频开发
撬动未来的支点4 天前
【WebRTC】从入门到忘记
webrtc
mit6.8244 天前
[p2p-Magnet] 队列与处理器 | DHT路由表
网络·网络协议·p2p
子兮曰5 天前
WebRTC实战指南:10个案例让你从入门到精通,别再错过这个实时通信利器!
webrtc
CrystalShaw7 天前
WebRTC音频QoS方法一.1(NetEQ之音频网络延时DelayManager计算补充)
音视频·webrtc
scorpion_V7 天前
WebRTC 结合云手机:释放实时通信与虚拟手机的强大协同效能
vue.js·智能手机·webrtc