8.3 WebRTC 简介

8.3 WebRTC 简介

WebRTC(Web Real-Time Communication)是一套开放的实时通信技术标准,使浏览器和移动应用能够直接进行点对点(P2P)的音视频通信和数据交换,无需中间服务器转发媒体流。

核心架构与关键组件

1. 技术栈组成

组件 功能描述
getUserMedia 访问本地媒体设备(摄像头、麦克风)
RTCPeerConnection 建立点对点连接,处理音视频传输和网络协商
RTCDataChannel 在已建立的连接上传输任意数据
ICE/STUN/TURN 穿透NAT和防火墙的网络地址转换技术

2. 典型通信流程

sequenceDiagram
    participant A as 客户端A
    participant S as 信令服务器
    participant B as 客户端B
    
    A->>S: 加入房间
    B->>S: 加入同一房间
    
    A->>A: 创建本地媒体流
    A->>A: 创建RTCPeerConnection
    
    A->>S: 发送offer
    S->>B: 转发offer
    
    B->>B: 创建RTCPeerConnection
    B->>B: 设置远程描述(offer)
    B->>B: 创建answer
    
    B->>S: 发送answer
    S->>A: 转发answer
    
    A->>A: 设置远程描述(answer)
    
    A->B: ICE候选交换(通过信令服务器)
    
    A-->>B: 建立P2P连接

基础API使用

1. 媒体设备访问

// 获取用户媒体
async function getLocalStream() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: true,
      video: {
        width: { ideal: 1280 },
        height: { ideal: 720 },
        facingMode: 'user' // 或'environment'后置摄像头
      }
    });
    
    document.getElementById('localVideo').srcObject = stream;
    return stream;
  } catch (err) {
    console.error('获取媒体设备失败:', err);
    throw err;
  }
}

// 设备枚举
async function listDevices() {
  const devices = await navigator.mediaDevices.enumerateDevices();
  const cameras = devices.filter(d => d.kind === 'videoinput');
  const mics = devices.filter(d => d.kind === 'audioinput');
  
  console.log('可用摄像头:', cameras);
  console.log('可用麦克风:', mics);
  
  return { cameras, mics };
}

2. 建立P2P连接

// 创建并配置RTCPeerConnection
function createPeerConnection(config) {
  const pcConfig = {
    iceServers: [
      { urls: 'stun:stun.l.google.com:19302' },
      { 
        urls: 'turn:turn.example.com',
        username: 'your_username',
        credential: 'your_password'
      }
    ]
  };
  
  const pc = new RTCPeerConnection(pcConfig);
  
  // ICE候选处理
  pc.onicecandidate = (event) => {
    if (event.candidate) {
      // 通过信令服务器发送候选
      signalingServer.send({
        type: 'ice-candidate',
        candidate: event.candidate
      });
    }
  };
  
  // 远程流到达时处理
  pc.ontrack = (event) => {
    document.getElementById('remoteVideo').srcObject = event.streams[0];
  };
  
  // 连接状态变化
  pc.onconnectionstatechange = () => {
    console.log('连接状态:', pc.connectionState);
  };
  
  return pc;
}

信令服务器实现

1. 基于WebSocket的信令

// 客户端信令处理
class SignalingClient {
  constructor() {
    this.socket = new WebSocket('wss://yourserver.com/signaling');
    this.peerConnection = null;
    this.roomId = null;
  }
  
  initialize() {
    this.socket.onmessage = async (event) => {
      const message = JSON.parse(event.data);
      
      switch (message.type) {
        case 'offer':
          await this.handleOffer(message);
          break;
        case 'answer':
          await this.handleAnswer(message);
          break;
        case 'ice-candidate':
          await this.handleICECandidate(message);
          break;
      }
    };
  }
  
  async joinRoom(roomId) {
    this.roomId = roomId;
    this.socket.send(JSON.stringify({
      type: 'join',
      roomId
    }));
  }
  
  async createOffer() {
    const offer = await this.peerConnection.createOffer();
    await this.peerConnection.setLocalDescription(offer);
    
    this.socket.send(JSON.stringify({
      type: 'offer',
      roomId: this.roomId,
      sdp: offer.sdp
    }));
  }
  
  async handleOffer(message) {
    await this.peerConnection.setRemoteDescription(
      new RTCSessionDescription({ type: 'offer', sdp: message.sdp })
    );
    
    const answer = await this.peerConnection.createAnswer();
    await this.peerConnection.setLocalDescription(answer);
    
    this.socket.send(JSON.stringify({
      type: 'answer',
      roomId: this.roomId,
      sdp: answer.sdp
    }));
  }
  
  async handleAnswer(message) {
    await this.peerConnection.setRemoteDescription(
      new RTCSessionDescription({ type: 'answer', sdp: message.sdp })
    );
  }
  
  async handleICECandidate(message) {
    try {
      await this.peerConnection.addIceCandidate(
        new RTCIceCandidate(message.candidate)
      );
    } catch (err) {
      console.error('添加ICE候选失败:', err);
    }
  }
}

2. 完整连接示例

async function startCall() {
  // 1. 获取本地媒体流
  const localStream = await getLocalStream();
  
  // 2. 创建信令客户端
  const signaling = new SignalingClient();
  signaling.initialize();
  
  // 3. 加入房间
  const roomId = 'demo-room';
  await signaling.joinRoom(roomId);
  
  // 4. 创建RTCPeerConnection
  signaling.peerConnection = createPeerConnection();
  
  // 5. 添加本地流
  localStream.getTracks().forEach(track => {
    signaling.peerConnection.addTrack(track, localStream);
  });
  
  // 6. 创建offer(如果是发起方)
  if (isCaller) {
    await signaling.createOffer();
  }
}

// 处理挂断
function endCall() {
  if (signaling.peerConnection) {
    signaling.peerConnection.close();
    signaling.peerConnection = null;
  }
  
  const localVideo = document.getElementById('localVideo');
  if (localVideo.srcObject) {
    localVideo.srcObject.getTracks().forEach(track => track.stop());
    localVideo.srcObject = null;
  }
}

数据通道(DataChannel)

1. 创建与使用

// 创建数据通道
function setupDataChannel(pc) {
  const dataChannel = pc.createDataChannel('chat', {
    ordered: true,        // 保证消息顺序
    maxRetransmits: 3     // 最大重传次数
  });
  
  dataChannel.onopen = () => {
    console.log('数据通道已打开');
    dataChannel.send('Hello!');
  };
  
  dataChannel.onmessage = (event) => {
    console.log('收到消息:', event.data);
  };
  
  dataChannel.onclose = () => {
    console.log('数据通道已关闭');
  };
  
  return dataChannel;
}

// 接收端处理
pc.ondatachannel = (event) => {
  const dataChannel = event.channel;
  
  dataChannel.onmessage = (event) => {
    console.log('收到数据通道消息:', event.data);
  };
};

2. 文件传输实现

// 发送文件
async function sendFile(dataChannel, file) {
  return new Promise((resolve) => {
    const reader = new FileReader();
    const fileInfo = {
      name: file.name,
      type: file.type,
      size: file.size,
      lastModified: file.lastModified
    };
    
    // 发送文件元数据
    dataChannel.send(JSON.stringify({
      type: 'file-meta',
      data: fileInfo
    }));
    
    let offset = 0;
    const chunkSize = 16 * 1024; // 16KB
    
    reader.onload = (e) => {
      dataChannel.send(e.target.result);
      offset += e.target.result.byteLength;
      
      if (offset < file.size) {
        readNextChunk();
      } else {
        resolve();
      }
    };
    
    function readNextChunk() {
      const slice = file.slice(offset, offset + chunkSize);
      reader.readAsArrayBuffer(slice);
    }
    
    readNextChunk();
  });
}

// 接收文件
function setupFileReceiver(dataChannel) {
  let fileInfo, receivedSize = 0;
  const chunks = [];
  
  dataChannel.onmessage = (event) => {
    if (typeof event.data === 'string') {
      const msg = JSON.parse(event.data);
      if (msg.type === 'file-meta') {
        fileInfo = msg.data;
        console.log(`开始接收文件: ${fileInfo.name}`);
      }
    } else {
      chunks.push(event.data);
      receivedSize += event.data.byteLength;
      
      const progress = (receivedSize / fileInfo.size) * 100;
      updateProgress(progress);
      
      if (receivedSize === fileInfo.size) {
        saveFile(chunks, fileInfo);
      }
    }
  };
  
  function saveFile(chunks, fileInfo) {
    const blob = new Blob(chunks, { type: fileInfo.type });
    const url = URL.createObjectURL(blob);
    
    const a = document.createElement('a');
    a.href = url;
    a.download = fileInfo.name;
    a.click();
    
    URL.revokeObjectURL(url);
  }
}

高级主题与优化

1. 连接监控与统计

// 获取连接统计
async function getConnectionStats(pc) {
  const stats = await pc.getStats();
  let results = {};
  
  stats.forEach(report => {
    if (report.type === 'candidate-pair' && report.nominated) {
      results.candidatePair = report;
    } else if (report.type === 'inbound-rtp') {
      results.inbound = report;
    } else if (report.type === 'outbound-rtp') {
      results.outbound = report;
    }
  });
  
  return {
    rtt: results.candidatePair?.currentRoundTripTime * 1000,
    inboundBitrate: calculateBitrate(results.inbound),
    outboundBitrate: calculateBitrate(results.outbound),
    packetsLost: results.inbound?.packetsLost
  };
}

function calculateBitrate(rtp) {
  if (!rtp) return 0;
  return (8 * rtp.bytesReceived) / (rtp.timestamp - rtp.lastPacketReceivedTimestamp);
}

// 定期监控
setInterval(async () => {
  const stats = await getConnectionStats(pc);
  console.log('当前连接质量:', stats);
}, 5000);

2. 自适应码率控制

// 基于网络条件调整视频质量
function setupBandwidthAdaptation(pc, sender) {
  const MIN_BITRATE = 150000; // 150kbps
  const MAX_BITRATE = 2000000; // 2Mbps
  
  let currentBitrate = MAX_BITRATE;
  
  setInterval(async () => {
    const stats = await getConnectionStats(pc);
    
    // 根据RTT和丢包调整码率
    if (stats.rtt > 300 || stats.packetsLost > 5) {
      currentBitrate = Math.max(currentBitrate * 0.8, MIN_BITRATE);
    } else if (currentBitrate < MAX_BITRATE) {
      currentBitrate = Math.min(currentBitrate * 1.2, MAX_BITRATE);
    }
    
    // 应用新参数
    const parameters = sender.getParameters();
    if (!parameters.encodings) {
      parameters.encodings = [{}];
    }
    parameters.encodings[0].maxBitrate = currentBitrate;
    sender.setParameters(parameters);
  }, 5000);
}

// 使用示例
const videoSender = pc.getSenders().find(s => s.track.kind === 'video');
if (videoSender) {
  setupBandwidthAdaptation(pc, videoSender);
}

实际应用案例

1. 视频会议系统

class VideoConference {
  constructor() {
    this.participants = new Map();
    this.localStream = null;
    this.signaling = new SignalingClient();
  }
  
  async start() {
    try {
      // 初始化本地媒体
      this.localStream = await this.getUserMedia();
      this.displayLocalStream();
      
      // 连接信令服务器
      await this.signaling.connect();
      
      // 处理新参与者
      this.signaling.on('new-peer', async (peerId) => {
        const pc = this.createPeerConnection();
        this.participants.set(peerId, pc);
        
        // 添加本地轨道
        this.localStream.getTracks().forEach(track => {
          pc.addTrack(track, this.localStream);
        });
        
        // 创建offer
        const offer = await pc.createOffer();
        await pc.setLocalDescription(offer);
        
        this.signaling.sendOffer(peerId, offer);
      });
      
      // 加入房间
      this.signaling.joinRoom(this.roomId);
      
    } catch (error) {
      console.error('会议启动失败:', error);
    }
  }
  
  createPeerConnection() {
    const pc = new RTCPeerConnection(this.config);
    
    pc.ontrack = (event) => {
      this.displayRemoteStream(event.streams[0]);
    };
    
    pc.onicecandidate = (event) => {
      if (event.candidate) {
        this.signaling.sendICECandidate(event.candidate);
      }
    };
    
    return pc;
  }
  
  displayRemoteStream(stream) {
    const video = document.createElement('video');
    video.autoplay = true;
    video.playsInline = true;
    video.srcObject = stream;
    document.getElementById('remoteVideos').appendChild(video);
  }
}

2. 远程桌面共享

// 屏幕捕获
async function startScreenShare() {
  try {
    const stream = await navigator.mediaDevices.getDisplayMedia({
      video: {
        width: { max: 1920 },
        height: { max: 1080 },
        frameRate: { ideal: 30 }
      },
      audio: false
    });
    
    const videoTrack = stream.getVideoTracks()[0];
    videoTrack.onended = () => {
      console.log('用户停止了屏幕共享');
      stopScreenShare();
    };
    
    // 替换现有视频轨道
    const sender = pc.getSenders().find(s => s.track.kind === 'video');
    if (sender) {
      sender.replaceTrack(videoTrack);
    }
    
    return stream;
  } catch (err) {
    console.error('屏幕共享失败:', err);
    throw err;
  }
}

// 鼠标键盘事件转发
function setupInputForwarding(dataChannel) {
  document.addEventListener('mousemove', (e) => {
    dataChannel.send(JSON.stringify({
      type: 'mouse',
      x: e.clientX,
      y: e.clientY,
      buttons: e.buttons
    }));
  });
  
  document.addEventListener('keydown', (e) => {
    dataChannel.send(JSON.stringify({
      type: 'key',
      key: e.key,
      code: e.code,
      ctrlKey: e.ctrlKey,
      shiftKey: e.shiftKey
    }));
  });
}

安全与隐私考量

  1. 媒体访问权限

    • 明确向用户解释权限用途
    • 处理权限拒绝情况
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ video: true });
    } catch (err) {
      if (err.name === 'NotAllowedError') {
        showPermissionInstructions();
      }
    }
    
  2. 加密与安全

    • WebRTC强制使用SRTP加密媒体流
    • 数据通道支持SCTP加密
    • 始终使用HTTPS/WSS保护信令
  3. 隐私保护

    // 关闭摄像头/麦克风指示器
    function stopMediaTracks(stream) {
      stream.getTracks().forEach(track => {
        track.enabled = false;
        track.stop();
      });
    }
    

WebRTC技术为Web平台带来了前所未有的实时通信能力,开发者应:

  • 优先考虑用户体验和性能优化
  • 实现完善的错误处理和回退机制
  • 遵循安全最佳实践
  • 针对移动设备进行特别优化

随着WebCodecs和WebTransport等新API的出现,WebRTC的能力边界仍在不断扩展,为开发更丰富的实时应用提供了可能。

#前端开发 分享于 2025-05-20

【 内容由 AI 共享,不代表本站观点,请谨慎参考 】