WebRTC 初探
背景
我正在实现一个 FC 游戏网站, PC 用户仅需要配置键盘便能实现小伙伴们一起玩, 但是手机用户就比较麻烦了
传统的网页游戏都是通过 HTTP/WS 的方式实现联机, 对于服务器的负担还是比较重的. 实际上需要一起玩的小伙伴一般都在一块, 也没必要使用远端的服务器转发.
任意一个小伙伴的设备起一个服务也是一个好办法, 但是暂时还没考虑做 APP, 想要用户打开就能玩耍, 所以我坚持仅使用浏览器的功能
有小伙伴喜欢用手柄操作, 我考虑使用蓝牙联机, 但是 Web Bluetooth API 主要用于浏览器与蓝牙设备之间的通信(如智能手表、蓝牙耳机等), 而非直接实现浏览器之间的通信
最后我了解到了 WebRTC, 那就是我要的滑板鞋
什么是 WebRTC ?
WebRTC (Web Real-Time Communication), 网页及时交流
WebRTC 是一项开源技术, 旨在通过网页或移动应用程序实现点对点(P2P)的实时音视频、数据传输。WebRTC 允许用户无需通过中介服务器, 直接在浏览器之间进行音视频通信、文件共享、屏幕共享和实时数据传输, 广泛用于视频通话、在线会议、直播等场景。
简而言之, 这是一项网页之间直接通信的技术
WebRTC 的核心功能
WebRTC 提供了以下核心功能:
音频、视频通信:
WebRTC 能够通过点对点连接传输高质量的音频和视频数据, 支持实时视频通话和音频通话。它支持多种音视频编码器, 如 Opus 和 VP8、VP9 等。
数据传输:
除了音视频, WebRTC 还支持任意数据的传输。通过 RTCDataChannel, 可以进行低延迟的任意格式的数据传输, 如文件传输、聊天信息等。
安全性:
WebRTC 使用强大的加密技术, 所有数据传输都通过 SRTP(安全实时传输协议)和 DTLS(数据报传输层安全协议)加密, 确保通信的安全性。
如何使用 WebRTC ?
浏览器主要提供了 3 个 API
getUserMedia
这个 API 允许从用户的摄像头和麦克风中获取音视频流, 并将其捕获在 MediaStream 对象中。该对象可以通过 WebRTC 传输到远程浏览器, 也可以直接在本地页面播放。
navigator.mediaDevices
.getUserMedia({ video: true, audio: true })
.then((stream) => {
// 使用本地视频播放流
document.getElementById("localVideo").srcObject = stream;
})
.catch((error) => {
console.error("Error accessing media devices.", error);
});
RTCPeerConnection
RTCPeerConnection 是 WebRTC 的核心, 用于在两端建立音视频通信和数据通道。它支持网络协商(包括 SDP 会话描述协议)和处理网络中的 NAT(网络地址转换)穿透, 使得两个浏览器即使在不同网络下也可以建立直接连接。
const peerConnection = new RTCPeerConnection();
// 添加本地流
stream.getTracks().forEach((track) => peerConnection.addTrack(track, stream));
// 监听远端流
peerConnection.ontrack = (event) => {
const remoteStream = event.streams[0];
document.getElementById("remoteVideo").srcObject = remoteStream;
};
RTCDataChannel
RTCDataChannel 允许两个浏览器之间的任意数据传输, 适合传输文本、文件、游戏状态、实时聊天消息等内容, 支持低延迟和高性能。
const dataChannel = peerConnection.createDataChannel("chat");
dataChannel.onmessage = (event) => {
console.log("Received message:", event.data);
};
dataChannel.send("Hello!");
WebRTC 连接建立流程
创建 RTCPeerConnection
两个端点(浏览器)各自创建 RTCPeerConnection 实例, 用于管理 P2P 连接。
信令交换(SDP)
WebRTC 本身不定义信令机制, 需要借助第三方信令服务器(如 WebSocket、HTTP)来交换 SDP(Session Description Protocol)。SDP 描述了端点的音视频格式、网络信息等, 确保两个端点能够互相理解。
一方创建 offer, 发送给另一方, 另一方回复 answer。
// 创建 offer 并发送给远端
peerConnection.createOffer().then((offer) => {
return peerConnection.setLocalDescription(offer);
}); // 接收 answer 并设置为远端描述
peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
ICE(Interactive Connectivity Establishment)候选项交换
使用 ICE 来发现并交换每个浏览器的候选网络路径(如本地 IP、公共 IP 等), 以帮助浏览器之间建立 P2P 连接。ICE 通过 STUN/TURN 服务器帮助穿透 NAT 和防火墙。
建立 P2P 连接并传输数据
一旦 SDP 和 ICE 协商完成, 浏览器间建立 P2P 连接, 音视频和数据可以开始实时传输。
简单的案例
这里只调用基本的 API, 不做过多的介绍
创建 2 个 html 文件, 1.html和2.html, 用浏览器打开, 咱们直接控制台撸代码体验流程
创建 RTCPeerConnection
// 1.html
const p1 = new RTCPeerConnection();
// 2.html
const p2 = new RTCPeerConnection();
信令交换 SDP(Session Description Protocol)
这个过程通常是通过 WS 服务转发, 咱们这里主要体验流程, 所以手动操作
创建 offer 并设置为本地描述
// 1.html
p1.createOffer().then((offer) => {
// 设置为本地描述, 手动复制offer对象
p1.setLocalDescription(offer);
});
接收 offer 并设置为远端描述
// 2.html
// 将刚刚的offer设置为远端描述
p2.setRemoteDescription(offer);
创建 answer 并设置为本地描述
// 2.html
// 创建应答answer, 将answer设置为本地描述, 复制answer
p2.createAnswer().then((answer) => {
p2.setLocalDescription(answer);
});
接收 answer 并设置为远端描述
// 1.html
// 将刚刚的answer设置为远端描述
p1.setRemoteDescription(answer);
ICE(Interactive Connectivity Establishment)候选项交换
监听 icecandidate 事件
监听 icecandidate 事件, 获取 candidate
// 1.html
p1.onicecandidate = (event) => {
if (event.candidate) {
// 复制candidate
}
};
添加到对端
// 2.html
p2.addIceCandidate(candidate);
同理
// 2.html
p2.onicecandidate = (event) => {
if (event.candidate) {
// 复制candidate
}
};
// 1.html
p1.addIceCandidate(event.candidate);
这样, 基本的连接流程就完成了
一个简易聊天室
这个流程手动操作起来也挺麻烦的, 这里简化一下操作
打开a.html会生成带参数的链接打开b.html, 复制b.html生成的信息填入a.html, 这就是交换 SDP 和 ice 的过程
相关代码
a.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div class="config">
<a id="open" href="" target="_blank">打开新页面</a>
<p>打开页面复制sdp相关信息填入</p>
<textarea id="sdp" style="width: 100%; height: 200px"></textarea>
<button id="add">add sdp</button>
</div>
<div class="chat" style="display: none">
<div class="chat-box"></div>
<textarea class="chat-input"></textarea>
<button id="send">send</button>
</div>
<script>
const p1 = new RTCPeerConnection();
function send(msg) {
dataChannel.send(msg);
const div = document.createElement("div");
div.innerText = "我: " + msg;
const chat = document.querySelector(".chat-box");
chat.appendChild(div);
}
const dataChannel = p1.createDataChannel("chatChannel");
p1.ondatachannel = (event) => {
console.log(event);
};
dataChannel.onopen = () => {
console.log("DataChannel 已打开,可以发送消息");
const chat = document.querySelector(".chat");
const config = document.querySelector(".config");
chat.style.display = "block";
config.style.display = "none";
const btn = document.querySelector("#send");
btn.addEventListener("click", () => {
const input = document.querySelector(".chat-input");
send(input.value);
input.value = "";
});
};
dataChannel.onmessage = (event) => {
console.log("收到消息:", event.data);
const div = document.createElement("div");
div.innerText = "对方: " + event.data;
const chat = document.querySelector(".chat-box");
chat.appendChild(div);
};
p1.createOffer().then((offer) => {
p1.setLocalDescription(offer);
p1.onicecandidate = (event) => {
if (event.candidate) {
console.log(offer);
console.log(event.candidate);
const url = `${location.origin}/b.html?offer=${encodeURIComponent(
JSON.stringify(offer)
)}&candidate=${encodeURIComponent(
JSON.stringify(event.candidate)
)}`;
const open = document.querySelector("#open");
open.href = url;
}
};
});
const add = document.querySelector("#add");
add.addEventListener("click", () => {
const { answer, candidate } = JSON.parse(
document.querySelector("#sdp").value
);
p1.setRemoteDescription(answer);
p1.addIceCandidate(candidate);
});
</script>
</body>
</html>
b.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div class="config">
<button id="copy" disabled title="复制SDP ice信息">复制信息</button>
</div>
<div class="chat" style="display: none">
<div class="chat-box"></div>
<textarea class="chat-input"></textarea>
<button id="send">send</button>
</div>
<script>
const query = new URLSearchParams(location.search);
const offer = JSON.parse(decodeURIComponent(query.get("offer")));
const candidate = JSON.parse(decodeURIComponent(query.get("candidate")));
const p2 = new RTCPeerConnection();
p2.setRemoteDescription(offer);
p2.addIceCandidate(candidate);
p2.createAnswer().then((answer) => {
p2.setLocalDescription(answer);
p2.onicecandidate = (event) => {
if (event.candidate) {
console.log("生成的 ICE 候选者:", event.candidate);
const json = JSON.stringify({ candidate: event.candidate, answer });
const copy = document.querySelector("#copy");
copy.addEventListener("click", () => {
const input = document.createElement("input");
document.body.appendChild(input);
input.value = json;
input.select();
document.execCommand("copy");
document.body.removeChild(input);
});
copy.disabled = false;
}
};
});
let receiveChannel;
p2.ondatachannel = (event) => {
receiveChannel = event.channel;
receiveChannel.onopen = () => {
const config = document.querySelector(".config");
config.style.display = "none";
const chat = document.querySelector(".chat");
chat.style.display = "block";
console.log("DataChannel 已打开,可以接收消息");
const send = document.querySelector("#send");
send.addEventListener("click", () => {
const input = document.querySelector(".chat-input");
receiveChannel.send(input.value);
const div = document.createElement("div");
div.textContent = "我: " + input.value;
document.querySelector(".chat-box").appendChild(div);
input.value = "";
});
};
receiveChannel.onmessage = (event) => {
const div = document.createElement("div");
div.textContent = "对方: " + event.data;
document.querySelector(".chat-box").appendChild(div);
};
};
</script>
</body>
</html>
体验案例: https://webrtcchat.surge.sh/a.html
简易流媒体通信
既然 RTCPeerConnection 是个对象, 咱们可以一个页面创建两个对象来体验功能, 这样 SDP 和 ice 交换就简单了, 机智如我啊
相关代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<canvas width="500" height="400" id="canvas"></canvas>
<video id="video" autoplay muted></video>
<script>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let x = 50; // 圆的初始 x 坐标
let y = 50; // 圆的初始 y 坐标
let radius = 30; // 圆的半径
let dx = 2; // 圆在 x 方向上的增量
let dy = 2; // 圆在 y 方向上的增量
// 定义动画的绘制函数
function draw() {
// 清空 canvas,防止绘制的图形叠加
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绘制圆
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = "blue";
ctx.fill();
ctx.closePath();
// 更新圆的坐标
x += dx;
y += dy;
// 碰撞检测,使圆在边缘反弹
if (x + radius > canvas.width || x - radius < 0) {
dx = -dx; // 水平方向反弹
}
if (y + radius > canvas.height || y - radius < 0) {
dy = -dy; // 垂直方向反弹
}
// 请求下一帧动画
requestAnimationFrame(draw);
}
// 启动动画
draw();
const p1 = new RTCPeerConnection();
const stream = canvas.captureStream(60);
stream.getTracks().forEach((track) => {
p1.addTrack(track, stream);
console.log(track, stream);
});
const p2 = new RTCPeerConnection();
p2.ontrack = (event) => {
console.log("event", event);
const video = document.querySelector("#video");
video.srcObject = event.streams[0];
video.muted = true;
video.autoplay = true;
};
let receiveChannel;
p2.ondatachannel = (event) => {
receiveChannel = event.channel;
receiveChannel.onopen = () => {
console.log("DataChannel 已打开,可以接收消息");
};
receiveChannel.onmessage = (event) => {
console.log("收到消息:", event.data);
receiveChannel.send("Hello from Browser B");
};
};
const channel = p1.createDataChannel("channel");
channel.onopen = () => {
console.log("DataChannel 已打开,可以发送消息");
channel.send("Hello from Browser A");
};
channel.onmessage = (event) => {
console.log("收到消息:", event.data);
};
p1.onicecandidate = (event) => {
if (event.candidate) {
p2.addIceCandidate(event.candidate);
}
};
p2.onicecandidate = (event) => {
if (event.candidate) {
p1.addIceCandidate(event.candidate);
}
};
p1.createOffer().then((offer) => {
p1.setLocalDescription(offer);
p2.setRemoteDescription(offer);
p2.createAnswer().then((answer) => {
p2.setLocalDescription(answer);
p1.setRemoteDescription(answer);
console.log(offer, answer);
});
});
</script>
</body>
</html>
体验案例: https://webrtcchat.surge.sh/
参考文献
WebRTC API
实现 WebRTC 群聊会议室
WebRTC 浅谈(一)概述与架构
WebRTC 初探的更多相关文章
- 下周二推出“音视频技术WebRTC初探”公开课,欢迎捧场!
下周二推出"音视频技术WebRTC初探"公开课,欢迎捧场! 公开课课程链接:http://edu.csdn.net/huiyiCourse/detail/90 课程的解说资料 ...
- webrtc初探之一对一的连接过程(一)
说明,我研究的是muan-khan的一个github项目,针对的是chrome对chrome,也就是pc对pc的一对一,一对多通话,感兴趣的可以继续往下看. github地址:https://gith ...
- webrtc初探
0.闲来无事,想研究webrtc,看了一些网上的文章之后,觉得谬误较多,以讹传讹的比较多,自己试验了一把,记录一下. 官网的写的教程在实践中也觉得不用那么复杂,有种落伍与繁冗的感觉. 1.我想看的是w ...
- freeswitch编译安装,初探, 以及联合sipgateway, webrtc server的使用场景。
本文主要记录freeswitch学习过程. 一 安装freeswitch NOTE 以下两种安装方式,再安装的过程中遇到了不少问题,印象比较深刻的就是lua库找到不到这个问题.这个问题发生在make ...
- WebRTC手记之初探
转载请注明出处:http://www.cnblogs.com/fangkm/p/4364553.html WebRTC是HTML5支持的重要特性之一,有了它,不再需要借助音视频相关的客户端,直接通过浏 ...
- (一)WebRTC手记之初探
转自:http://www.cnblogs.com/fangkm/p/4364553.html WebRTC是HTML5支持的重要特性之一,有了它,不再需要借助音视频相关的客户端,直接通过浏览器的We ...
- 初探Electron,从入门到实践
本文由葡萄城技术团队于博客园原创并首发 转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 在开始之前,我想您一定会有这样的困惑:标题里的Electron ...
- 初探领域驱动设计(2)Repository在DDD中的应用
概述 上一篇我们算是粗略的介绍了一下DDD,我们提到了实体.值类型和领域服务,也稍微讲到了DDD中的分层结构.但这只能算是一个很简单的介绍,并且我们在上篇的末尾还留下了一些问题,其中大家讨论比较多的, ...
- CSharpGL(8)使用3D纹理渲染体数据 (Volume Rendering) 初探
CSharpGL(8)使用3D纹理渲染体数据 (Volume Rendering) 初探 2016-08-13 由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了.CSharpGL源码 ...
- 从273二手车的M站点初探js模块化编程
前言 这几天在看273M站点时被他们的页面交互方式所吸引,他们的首页是采用三次加载+分页的方式.也就说分为大分页和小分页两种交互.大分页就是通过分页按钮来操作,小分页是通过下拉(向下滑动)时异步加载数 ...
随机推荐
- linux date格式化获取时间
转载请注明出处: 在编写shell脚本时,需要在shell脚本中格式化时间,特此整理下date命令相关参数的应用 root@controller1:~# date --help 用法:date [选项 ...
- 玄机-第二章日志分析-mysql应急响应
目录 前言 简介 应急开始 准备工作 日志分析 步骤 1 步骤 2 步骤 3 步骤 4 总结 补充mysql中的/var/log/mysql/erro.log 记录上传文件信息的原因 前言 这里应急需 ...
- Mysql函数1-IFNULL
IFNULL函数用于判断参数值是null时则返回指定内容. 原本 select goods_base_name,goods_id from goods where goods_id in (6,7,8 ...
- 【Java】 Void 类型
void 也算一个类型,而且是基本数据类型 和其它数据类型一样提供了对应的包装类Void 每个包装类都提供一个TYPE字节实例,返回对应的原型类实例 public static void main(S ...
- 【Linux】11 RPM & YUM 管理工具 介绍
rpm包的管理 介绍: 一种用于互联网下载包的打包及安装工具,它包含在某些Linux分发版中. 它生成具有.RPM扩展名的文件.RPM是RedHat Package Manager(RedHat软件包 ...
- 【Tutorial C】08 函数 Function
函数的定义 C源程序是由函数组成的. 最简单的程序有一个主函数 main(),但实用程序往往由多个函数组成, 由主函数调用其他函数,其他函数也可以互相调用. 函数是C源程序的基本模块,程序的许多功能是 ...
- 【Vue】Re11 Vue 与 Webpack
一.案例环境前置准备: 创建一个空目录用于案例演示 mkdir vue-sample 初始化案例和安装webpack cd vue-sample npm install webpack@3.6.0 - ...
- baselines算法库run.py模块分析
baselines算法库地址: https://gitee.com/devilmaycry812839668/baselines =================================== ...
- Android网页投屏控制从入门到放弃
背景 业务需要采集在app上执行任务的整个过程,原始方案相对复杂,修改需要协调多方人员,因而考虑是否有更轻量级的方案. 原始需求: 记录完成任务的每一步操作(点击.滑动.输入等) 记录操作前后的截图和 ...
- FlashAttention简介
前置知识 在GPU进行矩阵运算的时候,内部的运算单元具有和CPU类似的存储金字塔. 如果采用经典的Attention的计算方式,需要保存中间变量S和注意力矩阵O,这样子会产生很大的现存占用,并且这些数 ...