一对一 WebRTC 视频聊天

news/2025/11/16 21:16:52/文章来源:https://www.cnblogs.com/flyingsir/p/19229074

“最小可运行”的一对一 WebRTC 视频聊天示例(前端+信令服务器),完全按照你给出的 8 条信令流程图实现。
技术栈:

  • 前端:原生 JavaScript + socket.io-client
  • 信令服务器:Node.js + socket.io + Express(80 端口同时提供静态文件和信令)
  • STUN/TURN:Google 公共 STUN 即可,生产环境请换成自己的 TURN

代码可直接复制到同一目录运行,局域网两台浏览器打开 http://localhost 即可互拨。


  1. 目录结构
    webrtc-1v1/
    ├─ server.js // 信令服务器
    ├─ public/
    │ ├─ index.html // 页面
    │ └─ client.js // 前端逻辑
    └─ package.json

  1. 安装依赖
npm init -y
npm i express socket.io

  1. 信令服务器 server.js
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const path = require('path');const app = express();
const server = http.createServer(app);
const io = socketIo(server, { cors: { origin: '*' } });// 房间 => Set<socketId>
const rooms = new Map();app.use(express.static(path.join(__dirname, 'public')));io.on('connection', socket => {console.log('connect', socket.id);/* 1. join 加入房间 */socket.on('join', room => {socket.join(room);socket.room = room;const others = rooms.get(room) || new Set();if (others.size >= 1) {// 2. resp-join 返回另一个人的 uidsocket.emit('resp-join', [...others][0]);}others.add(socket.id);rooms.set(room, others);// 4. new-peer 通知房间里其他人socket.to(room).emit('new-peer', socket.id);});/* 3. leave 离开房间 */socket.on('leave', () => leaveRoom(socket));socket.on('disconnect', () => leaveRoom(socket));/* 6. offer 转发 */socket.on('offer', ({ target, sdp }) => {socket.to(target).emit('offer', { from: socket.id, sdp });});/* 7. answer 转发 */socket.on('answer', ({ target, sdp }) => {socket.to(target).emit('answer', { from: socket.id, sdp });});/* 8. candidate 转发 */socket.on('candidate', ({ target, candidate }) => {socket.to(target).emit('candidate', { from: socket.id, candidate });});function leaveRoom(sock) {if (!sock.room) return;const room = sock.room;const others = rooms.get(room);if (others) {others.delete(sock.id);if (others.size === 0) rooms.delete(room);else {// 5. peer-leave 通知剩余的人sock.to(room).emit('peer-leave', sock.id);}}sock.leave(room);delete sock.room;}
});server.listen(80, () => console.log('HTTP+Socket.io on 80'));

  1. 前端 public/index.html
<!doctype html>
<html>
<head><meta charset="utf-8"/><title>WebRTC 1v1</title><style>video{width:240px;height:180px;background:#000;margin:6px}</style>
</head>
<body><h3>WebRTC 一对一</h3><input id="room" placeholder="房间名"/><button id="btnJoin">加入</button><button id="btnLeave" disabled>离开</button><br/><video id="local" autoplay muted></video><video id="remote" autoplay></video><script src="/socket.io/socket.io.js"></script><script src="client.js"></script>
</body>
</html>

  1. 前端 public/client.js
const localVideo = document.getElementById('local');
const remoteVideo = document.getElementById('remote');
const roomInput = document.getElementById('room');
const btnJoin = document.getElementById('btnJoin');
const btnLeave = document.getElementById('btnLeave');let localStream;
let pc;                 // RTCPeerConnection
let socket;
let roomName;
let otherId = null;     // 对端 socket.idbtnJoin.onclick = async () => {roomName = roomInput.value.trim();if (!roomName) return;socket = io();/* 1. join */socket.emit('join', roomName);/* 2. resp-join:如果房间已有人,立即发起 offer */socket.on('resp-join', async uid => {otherId = uid;await createPeerConnection();const offer = await pc.createOffer();await pc.setLocalDescription(offer);socket.emit('offer', { target: otherId, sdp: offer });});/* 4. new-peer:有人后进来,我作为“被叫” */socket.on('new-peer', async uid => {otherId = uid;await createPeerConnection();});/* 5. peer-leave:对方离开 */socket.on('peer-leave', () => {closeCall();});/* 6. offer */socket.on('offer', async ({ from, sdp }) => {if (!pc) await createPeerConnection();await pc.setRemoteDescription(sdp);const answer = await pc.createAnswer();await pc.setLocalDescription(answer);socket.emit('answer', { target: from, sdp: answer });});/* 7. answer */socket.on('answer', async ({ from, sdp }) => {await pc.setRemoteDescription(sdp);});/* 8. candidate */socket.on('candidate', async ({ from, candidate }) => {await pc.addIceCandidate(candidate);});socket.on('connect', async () => {localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });localVideo.srcObject = localStream;});btnJoin.disabled = true;btnLeave.disabled = false;
};btnLeave.onclick = () => {if (socket) {socket.emit('leave');socket.close();}closeCall();btnJoin.disabled = false;btnLeave.disabled = true;
};async function createPeerConnection() {pc = new RTCPeerConnection({iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]});localStream.getTracks().forEach(t => pc.addTrack(t, localStream));pc.ontrack = e => {remoteVideo.srcObject = e.streams[0];};pc.onicecandidate = e => {if (e.candidate && otherId) {socket.emit('candidate', { target: otherId, candidate: e.candidate });}};
}function closeCall() {if (pc) {pc.close();pc = null;}remoteVideo.srcObject = null;otherId = null;
}

  1. 运行
sudo node server.js   # 监听 80 端口需 sudo

浏览器打开 http://localhost,A/B 两台机器输入相同房间名即可互拨。
控制台可看到 8 条信令按顺序打印,完全符合题目流程图。


  1. 生产环境注意
  2. 把 Google STUN 换成自建 TURN(coturn),否则 NAT 穿墙失败率很高。
  3. 信令服务器务必加 HTTPS/WSS,否则最新浏览器禁止 getUserMedia。
  4. 增加异常处理、断线重连、摄像头/麦克风权限错误提示。
  5. 若要做移动端,需加 playsinline、自动播放策略处理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/967358.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2025年11月载冷剂厂家推荐榜:五强真实数据与场景化选型指南

进入2025年冬季,北方冷库、数据中心、新能源电池产线陆续进入满负荷运行,载冷剂作为二次冷媒的“血液”,其稳定性直接决定系统能耗与设备寿命。很多工程师在招标阶段发现:同样标注“食品级”或“低电导”的产品,现…

2025年11月载冷剂厂家榜单:性能参数与口碑综合评测

进入2025年第四季度,北方集中供暖启动、冷链物流旺季叠加新能源电池扩产,载冷剂采购需求集中爆发。很多项目工程师面临“三快一严”场景:快速选型、快速比价、快速交付,同时环保与安全审查趋严。用户普遍担心三点:…

20232313 2025-2026-1 《网络与系统攻防技术》实验五实验报告 - 20232313

1.实验内容基本实验内容如下:学会使用msf编码器,veil-evasion,自己利用shellcode编程等免杀工具或技巧 正确使用msf编码器,使用msfvenom生成如jar之类的其他文件 veil,加壳工具 使用C + shellcode编程 通过组合应…

【第7章 I/O编程与异常】Python文件操作与上下文管理器的深度解析(避坑指南)

从 C 到 Python:文件操作与上下文管理器的深度解析(避坑指南) 对于习惯了 C 语言手动管理资源的学习者,Python 的文件操作和上下文管理器常常带来认知混淆:为什么 C 必须手动 fopen/fclose,而 Python 能用 with …

2025年11月乙二醇厂家对比榜:五家主流厂商真实数据与选型要点

进入2025年第四季度,国内工业温控系统进入年度维保与新建项目并行的高峰期,乙二醇作为防冻液母液、载冷剂溶剂及工业传热介质的核心原料,采购量显著抬升。国家能源局11月最新统计显示,工业温控领域乙二醇月需求环比…

2025年11月乙二醇厂家对比榜:五强产品性能与合规资质全盘点

进入11月,北方工业循环与冷链系统陆续进入防冻液集中更换窗口,乙二醇作为载冷剂母液的需求陡增。用户普遍面临三大痛点:一是原料纯度与批次稳定性差异大,导致换热效率波动;二是资质文件繁杂,难以快速识别合规供应…

工业级时序数据库选型指南:技巧架构与场景化实践

工业级时序数据库选型指南:技巧架构与场景化实践pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas",…

springboot生成前后端接口文档 - f

效果:图片中是访问接口文档的路径 一、基本配置 1.pom.xml中导入依赖:(根据需要的版本导入) <!-- swagger api文档 --><dependency><groupId>io.springfox</groupId><artifactId>sp…

20232429 2025-2026-1 《网络与系统攻防技术》实验五实验报告

1.实验内容 (1)从www.besti.edu.cn、baidu.com、sina.com.cn中选择一个DNS域名进行查询,获取信息。 (2)尝试获取BBS、论坛、QQ、MSN中某一好友的IP地址,并查询获取该好友所在的具体地理位置。 (3)使用nmap开源…

P5797 [SEERC 2019] Max or Min

整点 OI 题做做。 首先将 \(= x\) 的位置拎出来,不难发现剩下的位置都会被操作不到 \(2\) 次(最多取一次最大一次最小必然会变成 \(x\))。 考虑什么地方会操作两次,当 \(0, -1, 1\) 这种地方,我们必须对着中间的 …

完整教程:【论文精读】Latent-Shift:基于时间偏移模块的高效文本生成视频技术

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

20232323 2024-2025-1《网络与系统攻防技术》实验五实验报告

1.实验内容 任务1:DNS域名信息查询 任务2:获取好友IP地址及地理地址 任务3:使用nmap扫描靶机 任务4:使用Nessus扫描靶机 任务5:网络足迹与Google hacking 2.实验目的 通过一系列网络安全工具和技术手段,深入了解…

AI元人文:价值权衡的双模引擎与五维元问——构建人机共生的存在语法

AI元人文:价值权衡的双模引擎与五维元问——构建人机共生的存在语法 笔者:岐金兰 日期:2025年11月16日 摘要 在人工智能深度参与文明决策的元时代,我们面临的核心挑战从技术实现转向价值协调。本文系统论述AI元人文…

make

LOVE Originally, English is also a pictographic script. so English just dont like people are not similar with them.

Spring Cloud - Spring Cloud 注册中心与服务提供者(Spring Cloud Eureka 概述、微服务高效入门、微服务应用实例)

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

DateUtil

点击查看 import org.junit.Test;import java.time.*; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAdjusters; import java.time.tempora…

(链表)找单链表倒数第k个结点

5.找单链表倒数第k个结点 Node* Get_Kth_Node(Node* plist, int k) { Node* p = plist; Node* q = plist;for (int i = 0; i < k; i++)q = q->next;while (q != NULL) {p = p->next;q = q->next; }return …

(链表)判断是否回文

4.判断是否回文 std::stack<Node*> st; int len = GetLength(plist);Node* p = plist->next; for (int i = 0; i < len / 2; i++) {st.push(p);p = p->next; }if (len % 2 != 0)p = p->next;while (…

和为定值的子集数 25-11-16

和为定值的子集数 |递归|二叉树| 本题思维可以扩展到类似的题中,例如:子集和、子集积、排列组合类问题 其中:该题用到未知数组长度的读取,个人认为用stringstream会比之前的string然后一个一个读入方便很多 主要思…

(链表)判断两个单链表是否存在交点

2.判断两个单链表是否存在交点,如果存在交点,则找到相交的一点。 (1)只需判断是否相交 用两个分别跑到单链表的未结点处,然后判断是否是同一个尾结点即可 (2)指出具体的相交的结点是什么 先统计两个单链表长度,然…