别再手搓WebRTC了,试试PeerJS
·
一、 简介
PeerJS 是一个 WebRTC 的封装库,简化了浏览器之间的点对点通信。它提供了简单的 API 来实现音视频通话、数据传输等功能。
peerjs官网

二、基础概念
2.1 核心组件
- Peer: 代表一个连接实例
- DataConnection: 用于传输二进制数据
- MediaConnection: 用于传输音视频流
2.2 工作原理
- 每个 Peer 都有一个唯一的 ID
- 通过 PeerServer 进行信令( signaling)
- 建立 P2P 连接后直接通信
三、安装和设置
3.1 直接引入 CDN
<script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js"></script>
3.2 npm 安装
npm install peerjs
3.3 启动 PeerServer
# 全局安装 peer
npm install -g peer
# 启动默认服务器(端口 9000)
peerjs --port 9000
# 或使用自定义配置
peerjs --port 9001 --key peerjs --path /myapp
四、基础用法
4.1 创建 Peer 实例
// 方式1:自动生成 ID
const peer = new Peer();
// 方式2:指定 ID
const peer = new Peer('my-unique-id');
// 方式3:完整配置
const peer = new Peer('my-id', {
host: 'localhost',
port: 9000,
path: '/myapp',
secure: false, // 是否使用 HTTPS
debug: 2, // 调试级别:0-3
config: { // WebRTC 配置
iceServers: [
{ url: 'stun:stun.l.google.com:19302' },
{
url: 'turn:turn.example.com',
credential: 'password',
username: 'username'
}
]
}
});
4.2 事件监听
// 打开连接(获取 ID)
peer.on('open', (id) => {
console.log('我的 ID 是:', id);
});
// 接收连接
peer.on('connection', (conn) => {
console.log('收到连接请求');
handleConnection(conn);
});
// 接收通话
peer.on('call', (call) => {
console.log('收到通话请求');
handleCall(call);
});
// 错误处理
peer.on('error', (err) => {
console.error('发生错误:', err);
switch(err.type) {
case 'peer-unavailable':
console.log('对方不在线');
break;
case 'network':
console.log('网络错误');
break;
// ... 其他错误类型
}
});
五、数据传输
5.1 建立数据连接
// 主动连接
const conn = peer.connect('目标-peer-id');
// 处理传入连接
peer.on('connection', (conn) => {
setupDataConnection(conn);
});
// 通用设置函数
function setupDataConnection(conn) {
console.log('已连接到:', conn.peer);
// 监听数据
conn.on('data', (data) => {
console.log('收到数据:', data);
handleReceivedData(data);
});
// 连接打开
conn.on('open', () => {
console.log('连接已打开');
conn.send({ type: 'greeting', message: '你好!' });
});
// 连接关闭
conn.on('close', () => {
console.log('连接已关闭');
});
// 错误处理
conn.on('error', (err) => {
console.error('连接错误:', err);
});
return conn;
}
5.2 发送数据
// 发送字符串
conn.send('Hello World!');
// 发送对象
conn.send({
type: 'message',
content: '这是一条消息',
timestamp: Date.now()
});
// 发送二进制数据
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setInt32(0, 42);
conn.send(buffer);
// 发送 Blob
const blob = new Blob(['Hello'], {type: 'text/plain'});
conn.send(blob);
// 发送文件
document.getElementById('fileInput').addEventListener('change', (e) => {
const file = e.target.files[0];
conn.send({
type: 'file',
name: file.name,
size: file.size,
data: file
});
});
5.3 处理接收的数据
function handleReceivedData(data) {
// 检测数据类型
if (typeof data === 'string') {
displayMessage(data);
} else if (data instanceof ArrayBuffer) {
handleBinaryData(data);
} else if (data instanceof Blob) {
handleBlobData(data);
} else if (data.type === 'file') {
handleFileData(data);
} else {
handleObjectData(data);
}
}
function handleFileData(fileInfo) {
const { name, size, data } = fileInfo;
console.log(`收到文件: ${name} (${size} bytes)`);
// 创建下载链接
const url = URL.createObjectURL(data);
const a = document.createElement('a');
a.href = url;
a.download = name;
a.click();
// 清理
URL.revokeObjectURL(url);
}
六、音视频通话
6.1 获取本地媒体流
async function getLocalStream() {
try {
// 获取音视频
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
// 显示本地视频
const localVideo = document.getElementById('localVideo');
localVideo.srcObject = stream;
localVideo.play();
return stream;
} catch (err) {
console.error('获取媒体流失败:', err);
throw err;
}
}
// 只获取音频
async function getAudioOnly() {
return await navigator.mediaDevices.getUserMedia({
video: false,
audio: true
});
}
// 屏幕共享
async function getScreenStream() {
try {
return await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: true
});
} catch (err) {
console.error('屏幕共享失败:', err);
throw err;
}
}
6.2 发起通话
// 发起视频通话
async function callPeer(peerId) {
const localStream = await getLocalStream();
// 发起通话
const call = peer.call(peerId, localStream);
setupCall(call);
}
// 处理传入通话
peer.on('call', async (call) => {
console.log('收到来自', call.peer, '的通话');
// 可以选择接受或拒绝
const accept = confirm(`接受来自 ${call.peer} 的通话?`);
if (accept) {
const localStream = await getLocalStream();
call.answer(localStream);
setupCall(call);
} else {
call.close();
}
});
function setupCall(call) {
// 对方媒体流到达
call.on('stream', (remoteStream) => {
console.log('收到远程流');
const remoteVideo = document.getElementById('remoteVideo');
remoteVideo.srcObject = remoteStream;
remoteVideo.play();
});
// 通话关闭
call.on('close', () => {
console.log('通话已关闭');
document.getElementById('remoteVideo').srcObject = null;
});
// 错误处理
call.on('error', (err) => {
console.error('通话错误:', err);
});
// 获取通话统计
call.on('stats', (stats) => {
console.log('通话统计:', stats);
});
}
七、高级功能
7.1 多房间实现
class ChatRoom {
constructor(roomId) {
this.roomId = roomId;
this.peer = null;
this.connections = new Map(); // peerId -> connection
this.users = new Set();
}
async join(username) {
return new Promise((resolve, reject) => {
this.peer = new Peer();
this.peer.on('open', (id) => {
this.userId = id;
this.username = username;
this.broadcast({ type: 'join', user: { id, username } });
resolve(id);
});
this.peer.on('connection', (conn) => {
this.setupConnection(conn);
});
this.peer.on('error', reject);
});
}
setupConnection(conn) {
this.connections.set(conn.peer, conn);
conn.on('data', (data) => {
this.handleMessage(data, conn);
});
conn.on('close', () => {
this.connections.delete(conn.peer);
this.users.delete(conn.peer);
this.broadcast({
type: 'leave',
userId: conn.peer
});
});
// 发送现有用户列表
conn.send({
type: 'users',
users: Array.from(this.users)
});
}
handleMessage(data, conn) {
switch(data.type) {
case 'join':
this.users.add(data.user);
this.broadcast({
type: 'user-joined',
user: data.user
}, conn.peer);
break;
case 'message':
this.broadcast({
type: 'chat',
message: data.message,
from: data.from,
timestamp: Date.now()
});
break;
}
}
broadcast(message, excludePeerId = null) {
this.connections.forEach((conn, peerId) => {
if (peerId !== excludePeerId && conn.open) {
conn.send(message);
}
});
}
sendMessage(text) {
const message = {
type: 'chat',
message: text,
from: {
id: this.userId,
username: this.username
}
};
this.broadcast(message);
}
leave() {
this.broadcast({ type: 'leave', userId: this.userId });
this.connections.forEach(conn => conn.close());
this.peer.destroy();
}
}
7.2 文件传输进度
class FileTransfer {
constructor(conn) {
this.conn = conn;
this.chunkSize = 16384; // 16KB
this.transfers = new Map();
this.setupListeners();
}
setupListeners() {
this.conn.on('data', (data) => {
if (data.type === 'file-meta') {
this.receiveFileMeta(data);
} else if (data.type === 'file-chunk') {
this.receiveFileChunk(data);
}
});
}
sendFile(file) {
const transferId = Date.now().toString();
const chunks = Math.ceil(file.size / this.chunkSize);
// 发送文件元数据
this.conn.send({
type: 'file-meta',
transferId,
name: file.name,
size: file.size,
chunks,
mimeType: file.type
});
// 读取并发送文件块
const reader = new FileReader();
let offset = 0;
reader.onload = (e) => {
this.conn.send({
type: 'file-chunk',
transferId,
data: e.target.result,
index: offset / this.chunkSize,
total: chunks
});
offset += this.chunkSize;
if (offset < file.size) {
readNextChunk();
} else {
console.log('文件发送完成');
}
};
const readNextChunk = () => {
const slice = file.slice(offset, offset + this.chunkSize);
reader.readAsArrayBuffer(slice);
};
readNextChunk();
}
receiveFileMeta(meta) {
this.transfers.set(meta.transferId, {
...meta,
received: 0,
chunks: new Array(meta.chunks),
progress: 0
});
this.updateProgress(meta.transferId, 0);
}
receiveFileChunk(chunk) {
const transfer = this.transfers.get(chunk.transferId);
if (!transfer) return;
transfer.chunks[chunk.index] = chunk.data;
transfer.received += chunk.data.byteLength;
transfer.progress = (transfer.received / transfer.size) * 100;
this.updateProgress(chunk.transferId, transfer.progress);
// 检查是否完成
if (transfer.received >= transfer.size) {
this.assembleFile(chunk.transferId);
}
}
assembleFile(transferId) {
const transfer = this.transfers.get(transferId);
const blob = new Blob(transfer.chunks, { type: transfer.mimeType });
// 创建下载链接
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = transfer.name;
a.click();
URL.revokeObjectURL(url);
this.transfers.delete(transferId);
console.log(`文件 ${transfer.name} 接收完成`);
}
updateProgress(transferId, progress) {
// 更新 UI
const progressBar = document.getElementById(`progress-${transferId}`);
if (progressBar) {
progressBar.style.width = `${progress}%`;
}
}
}
7.3 重连机制
class ReliableConnection {
constructor(peerId, options = {}) {
this.peerId = peerId;
this.maxRetries = options.maxRetries || 5;
this.retryDelay = options.retryDelay || 2000;
this.connection = null;
this.retryCount = 0;
this.messageQueue = [];
this.connect();
}
connect() {
this.connection = peer.connect(this.peerId);
this.connection.on('open', () => {
console.log('连接已建立');
this.retryCount = 0;
// 发送队列中的消息
while (this.messageQueue.length > 0) {
const msg = this.messageQueue.shift();
this.send(msg);
}
});
this.connection.on('close', () => {
console.log('连接关闭,尝试重连...');
this.reconnect();
});
this.connection.on('error', (err) => {
console.error('连接错误:', err);
this.reconnect();
});
}
reconnect() {
if (this.retryCount < this.maxRetries) {
this.retryCount++;
console.log(`重连尝试 ${this.retryCount}/${this.maxRetries}`);
setTimeout(() => {
this.connect();
}, this.retryDelay * this.retryCount);
} else {
console.error('重连失败');
}
}
send(data) {
if (this.connection && this.connection.open) {
this.connection.send(data);
} else {
this.messageQueue.push(data);
}
}
}
这个教程涵盖了 PeerJS 的主要功能和使用场景。通过实践这些示例,你应该能够构建出功能完善的 P2P 应用。有什么问题欢迎留言。
火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。
更多推荐
所有评论(0)