一、 简介

PeerJS 是一个 WebRTC 的封装库,简化了浏览器之间的点对点通信。它提供了简单的 API 来实现音视频通话、数据传输等功能。
peerjs官网
在这里插入图片描述

二、基础概念

2.1 核心组件

  • Peer: 代表一个连接实例
  • DataConnection: 用于传输二进制数据
  • MediaConnection: 用于传输音视频流

2.2 工作原理

  1. 每个 Peer 都有一个唯一的 ID
  2. 通过 PeerServer 进行信令( signaling)
  3. 建立 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 应用。有什么问题欢迎留言。

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐