WebRTC

WebRTC(Web Real-Time Communication)是一种支持浏览器之间实时音视频通信的技术。本章将介绍WebRTC的基本概念和使用方法。

WebRTC概述

什么是WebRTC

const webrtcOverview = {
    definition: 'WebRTC是一种支持浏览器之间点对点实时通信的开放标准',
    
    features: [
        '点对点通信,无需服务器中转',
        '支持音视频实时传输',
        '支持任意数据传输',
        '跨平台支持',
        '内置加密(DTLS/SRTP)'
    ],
    
    components: {
        getUserMedia: '获取本地媒体流(摄像头、麦克风)',
        RTCPeerConnection: '建立点对点连接',
        RTCDataChannel: '数据通道,传输任意数据'
    }
};

获取媒体设备

async function getMediaDevices() {
    const devices = await navigator.mediaDevices.enumerateDevices();
    
    devices.forEach(device => {
        console.log(`${device.kind}: ${device.label} (${device.deviceId})`);
    });
    
    return {
        cameras: devices.filter(d => d.kind === 'videoinput'),
        microphones: devices.filter(d => d.kind === 'audioinput'),
        speakers: devices.filter(d => d.kind === 'audiooutput')
    };
}

async function checkPermissions() {
    const permissions = await navigator.permissions.query({ name: 'camera' });
    console.log('摄像头权限:', permissions.state);
    
    const micPermissions = await navigator.permissions.query({ name: 'microphone' });
    console.log('麦克风权限:', micPermissions.state);
}

获取媒体流

基本用法

async function getLocalStream() {
    try {
        const stream = await navigator.mediaDevices.getUserMedia({
            video: true,
            audio: true
        });
        
        const videoElement = document.querySelector('video');
        videoElement.srcObject = stream;
        videoElement.play();
        
        return stream;
    } catch (error) {
        console.error('获取媒体流失败:', error);
        throw error;
    }
}

async function getSpecificDevice(deviceId) {
    const stream = await navigator.mediaDevices.getUserMedia({
        video: { deviceId: { exact: deviceId } },
        audio: true
    });
    return stream;
}

const constraints = {
    video: {
        width: { ideal: 1920 },
        height: { ideal: 1080 },
        frameRate: { ideal: 30 },
        facingMode: 'user'
    },
    audio: {
        echoCancellation: true,
        noiseSuppression: true,
        autoGainControl: true
    }
};

const stream = await navigator.mediaDevices.getUserMedia(constraints);

媒体流操作

function stopStream(stream) {
    stream.getTracks().forEach(track => {
        track.stop();
    });
}

function toggleVideo(stream, enabled) {
    stream.getVideoTracks().forEach(track => {
        track.enabled = enabled;
    });
}

function toggleAudio(stream, enabled) {
    stream.getAudioTracks().forEach(track => {
        track.enabled = enabled;
    });
}

function getTrackSettings(track) {
    return track.getSettings();
}

function applyConstraints(track, constraints) {
    return track.applyConstraints(constraints);
}

RTCPeerConnection

创建连接

const configuration = {
    iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },
        { urls: 'stun:stun1.l.google.com:19302' },
        {
            urls: 'turn:turn.example.com:3478',
            username: 'username',
            credential: 'password'
        }
    ]
};

const peerConnection = new RTCPeerConnection(configuration);

peerConnection.onicecandidate = (event) => {
    if (event.candidate) {
        console.log('ICE候选:', event.candidate);
        sendToRemote({ candidate: event.candidate });
    }
};

peerConnection.onconnectionstatechange = () => {
    console.log('连接状态:', peerConnection.connectionState);
};

peerConnection.oniceconnectionstatechange = () => {
    console.log('ICE连接状态:', peerConnection.iceConnectionState);
};

peerConnection.ontrack = (event) => {
    console.log('收到远程轨道:', event.track);
    const remoteVideo = document.getElementById('remoteVideo');
    remoteVideo.srcObject = event.streams[0];
};

添加媒体轨道

async function addLocalTracks(peerConnection, stream) {
    stream.getTracks().forEach(track => {
        peerConnection.addTrack(track, stream);
    });
}

const localStream = await getLocalStream();
addLocalTracks(peerConnection, localStream);

信令过程

创建Offer

async function createOffer() {
    const offer = await peerConnection.createOffer({
        offerToReceiveAudio: true,
        offerToReceiveVideo: true
    });
    
    await peerConnection.setLocalDescription(offer);
    
    console.log('本地描述已设置:', offer);
    
    return offer;
}

async function handleOffer(offer) {
    await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
    
    const answer = await peerConnection.createAnswer();
    await peerConnection.setLocalDescription(answer);
    
    return answer;
}

async function handleAnswer(answer) {
    await peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
}

async function addIceCandidate(candidate) {
    try {
        await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
    } catch (error) {
        console.error('添加ICE候选失败:', error);
    }
}

信令服务器

class SignalingClient {
    constructor(serverUrl) {
        this.ws = new WebSocket(serverUrl);
        this.handlers = new Map();
        
        this.ws.onmessage = (event) => {
            const message = JSON.parse(event.data);
            this.handleMessage(message);
        };
    }
    
    on(type, handler) {
        this.handlers.set(type, handler);
    }
    
    handleMessage(message) {
        const handler = this.handlers.get(message.type);
        if (handler) {
            handler(message.data);
        }
    }
    
    send(type, data) {
        this.ws.send(JSON.stringify({ type, data }));
    }
    
    join(roomId) {
        this.send('join', { roomId });
    }
    
    sendOffer(offer) {
        this.send('offer', offer);
    }
    
    sendAnswer(answer) {
        this.send('answer', answer);
    }
    
    sendCandidate(candidate) {
        this.send('candidate', candidate);
    }
}

完整示例

class WebRTCClient {
    constructor(signalingUrl) {
        this.signaling = new SignalingClient(signalingUrl);
        this.peerConnection = null;
        this.localStream = null;
        this.configuration = {
            iceServers: [
                { urls: 'stun:stun.l.google.com:19302' }
            ]
        };
        
        this.setupSignaling();
    }
    
    setupSignaling() {
        this.signaling.on('offer', async (offer) => {
            await this.handleOffer(offer);
        });
        
        this.signaling.on('answer', async (answer) => {
            await this.handleAnswer(answer);
        });
        
        this.signaling.on('candidate', async (candidate) => {
            await this.addIceCandidate(candidate);
        });
    }
    
    async startLocalMedia() {
        this.localStream = await navigator.mediaDevices.getUserMedia({
            video: true,
            audio: true
        });
        
        const localVideo = document.getElementById('localVideo');
        localVideo.srcObject = this.localStream;
        
        return this.localStream;
    }
    
    createPeerConnection() {
        this.peerConnection = new RTCPeerConnection(this.configuration);
        
        this.localStream.getTracks().forEach(track => {
            this.peerConnection.addTrack(track, this.localStream);
        });
        
        this.peerConnection.onicecandidate = (event) => {
            if (event.candidate) {
                this.signaling.sendCandidate(event.candidate);
            }
        };
        
        this.peerConnection.ontrack = (event) => {
            const remoteVideo = document.getElementById('remoteVideo');
            remoteVideo.srcObject = event.streams[0];
        };
    }
    
    async createCall(roomId) {
        this.createPeerConnection();
        
        const offer = await this.peerConnection.createOffer();
        await this.peerConnection.setLocalDescription(offer);
        
        this.signaling.join(roomId);
        this.signaling.sendOffer(offer);
    }
    
    async handleOffer(offer) {
        this.createPeerConnection();
        
        await this.peerConnection.setRemoteDescription(
            new RTCSessionDescription(offer)
        );
        
        const answer = await this.peerConnection.createAnswer();
        await this.peerConnection.setLocalDescription(answer);
        
        this.signaling.sendAnswer(answer);
    }
    
    async handleAnswer(answer) {
        await this.peerConnection.setRemoteDescription(
            new RTCSessionDescription(answer)
        );
    }
    
    async addIceCandidate(candidate) {
        await this.peerConnection.addIceCandidate(
            new RTCIceCandidate(candidate)
        );
    }
    
    hangup() {
        if (this.peerConnection) {
            this.peerConnection.close();
            this.peerConnection = null;
        }
        
        if (this.localStream) {
            this.localStream.getTracks().forEach(track => track.stop());
            this.localStream = null;
        }
    }
}

数据通道

const dataChannel = peerConnection.createDataChannel('chat', {
    ordered: true
});

dataChannel.onopen = () => {
    console.log('数据通道已打开');
};

dataChannel.onclose = () => {
    console.log('数据通道已关闭');
};

dataChannel.onmessage = (event) => {
    console.log('收到消息:', event.data);
};

dataChannel.onerror = (error) => {
    console.error('数据通道错误:', error);
};

function sendMessage(message) {
    if (dataChannel.readyState === 'open') {
        dataChannel.send(JSON.stringify(message));
    }
}

peerConnection.ondatachannel = (event) => {
    const receiveChannel = event.channel;
    
    receiveChannel.onmessage = (event) => {
        console.log('收到消息:', event.data);
    };
};

东巴文小贴士

📞 WebRTC开发建议

  1. STUN/TURN服务器:生产环境需要配置TURN服务器解决NAT穿透问题
  2. 信令服务器:可以使用WebSocket、Socket.io等实现
  3. 错误处理:处理各种连接失败和媒体获取失败的情况
  4. 安全性:WebRTC强制使用加密,确保数据安全

🔗 连接流程

  1. 获取本地媒体流
  2. 创建RTCPeerConnection
  3. 添加本地轨道
  4. 创建并发送Offer/Answer
  5. 交换ICE候选
  6. 建立连接

下一步

下一章将探讨 [Geolocation API](file:///e:/db-w.cn/md_data/javascript/78_Geolocation API.md),学习如何在Web应用中获取地理位置信息。