目录

核心特性

环境准备

1. 浏览器兼容性检查

2. 获取用户媒体权限

基础实现步骤

步骤1:创建本地视频流

步骤2:建立P2P连接

步骤3:信令服务器实现

完整示例代码

高级功能

1. 屏幕共享

2. 录制功能

最佳实践

1. 错误处理

2. 性能优化

常见问题解决

1. 防火墙/NAT穿透问题

2. 移动端适配

引用文章和出处

总结


SWebRTC(Simplified WebRTC)是基于WebRTC技术的简化实现,旨在为开发者提供更便捷的实时音视频通信解决方案。WebRTC(Web Real-Time Communication)是一个开源项目,支持网页浏览器进行实时音视频通信,无需安装任何插件。

核心特性

  • 点对点通信:无需中间服务器转发音视频数据
  • 低延迟:优化的传输协议确保实时性
  • 跨平台:支持Web、移动端和桌面应用
  • 安全加密:内置DTLS和SRTP加密
  • 开源免费:基于开放标准,无需授权费用

环境准备

1. 浏览器兼容性检查

// 检查浏览器是否支持WebRTC
function checkWebRTCSupport() {
    const constraints = {
        audio: true,
        video: true
    };
    
    if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
        console.log('WebRTC支持良好');
        return true;
    } else {
        console.log('当前浏览器不支持WebRTC');
        return false;
    }
}

2. 获取用户媒体权限

async function getUserMedia() {
    try {
        const stream = await navigator.mediaDevices.getUserMedia({
            audio: true,
            video: {
                width: { ideal: 1280 },
                height: { ideal: 720 }
            }
        });
        return stream;
    } catch (error) {
        console.error('获取媒体设备权限失败:', error);
        throw error;
    }
}

基础实现步骤

步骤1:创建本地视频流

<!DOCTYPE html>
<html>
<head>
    <title>SWebRTC示例</title>
</head>
<body>
    <video id="localVideo" autoplay muted></video>
    <video id="remoteVideo" autoplay></video>
    
    <script>
        // 获取本地视频流并显示
        async function setupLocalVideo() {
            try {
                const stream = await navigator.mediaDevices.getUserMedia({
                    video: true,
                    audio: true
                });
                
                const localVideo = document.getElementById('localVideo');
                localVideo.srcObject = stream;
                
                return stream;
            } catch (error) {
                console.error('设置本地视频失败:', error);
            }
        }
        
        // 初始化
        setupLocalVideo();
    </script>
</body>
</html>

步骤2:建立P2P连接

class SWebRTCConnection {
    constructor() {
        this.localStream = null;
        this.remoteStream = null;
        this.peerConnection = null;
        this.configuration = {
            iceServers: [
                { urls: 'stun:stun.l.google.com:19302' },
                { urls: 'stun:stun1.l.google.com:19302' }
            ]
        };
    }
    
    // 创建对等连接
    createPeerConnection() {
        this.peerConnection = new RTCPeerConnection(this.configuration);
        
        // 处理ICE候选
        this.peerConnection.onicecandidate = (event) => {
            if (event.candidate) {
                // 发送ICE候选到远程对等端
                this.sendIceCandidate(event.candidate);
            }
        };
        
        // 处理远程流
        this.peerConnection.ontrack = (event) => {
            this.remoteStream = event.streams[0];
            this.displayRemoteVideo();
        };
        
        // 添加本地流到连接
        if (this.localStream) {
            this.localStream.getTracks().forEach(track => {
                this.peerConnection.addTrack(track, this.localStream);
            });
        }
    }
    
    // 创建offer
    async createOffer() {
        try {
            const offer = await this.peerConnection.createOffer();
            await this.peerConnection.setLocalDescription(offer);
            return offer;
        } catch (error) {
            console.error('创建offer失败:', error);
        }
    }
    
    // 处理answer
    async handleAnswer(answer) {
        try {
            await this.peerConnection.setRemoteDescription(answer);
        } catch (error) {
            console.error('处理answer失败:', error);
        }
    }
}

步骤3:信令服务器实现

// 简单的WebSocket信令服务器
class SignalingServer {
    constructor() {
        this.socket = null;
        this.onMessage = null;
    }
    
    connect(serverUrl) {
        this.socket = new WebSocket(serverUrl);
        
        this.socket.onopen = () => {
            console.log('信令服务器连接成功');
        };
        
        this.socket.onmessage = (event) => {
            const message = JSON.parse(event.data);
            if (this.onMessage) {
                this.onMessage(message);
            }
        };
        
        this.socket.onclose = () => {
            console.log('信令服务器连接关闭');
        };
    }
    
    send(message) {
        if (this.socket && this.socket.readyState === WebSocket.OPEN) {
            this.socket.send(JSON.stringify(message));
        }
    }
}

完整示例代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SWebRTC视频通话</title>
    <style>
        .video-container {
            display: flex;
            gap: 20px;
            margin: 20px;
        }
        
        video {
            width: 400px;
            height: 300px;
            border: 2px solid #ccc;
            border-radius: 8px;
        }
        
        .controls {
            margin: 20px;
            text-align: center;
        }
        
        button {
            padding: 10px 20px;
            margin: 0 10px;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        
        button:disabled {
            background-color: #ccc;
            cursor: not-allowed;
        }
    </style>
</head>
<body>
    <h1>SWebRTC视频通话示例</h1>
    
    <div class="video-container">
        <video id="localVideo" autoplay muted></video>
        <video id="remoteVideo" autoplay></video>
    </div>
    
    <div class="controls">
        <button id="startCall">开始通话</button>
        <button id="endCall" disabled>结束通话</button>
        <button id="toggleAudio">静音</button>
        <button id="toggleVideo">关闭视频</button>
    </div>
    
    <script>
        class SWebRTCCall {
            constructor() {
                this.localStream = null;
                this.remoteStream = null;
                this.peerConnection = null;
                this.signaling = new SignalingServer();
                this.isCallActive = false;
                
                this.initializeElements();
                this.setupEventListeners();
            }
            
            initializeElements() {
                this.localVideo = document.getElementById('localVideo');
                this.remoteVideo = document.getElementById('remoteVideo');
                this.startCallBtn = document.getElementById('startCall');
                this.endCallBtn = document.getElementById('endCall');
                this.toggleAudioBtn = document.getElementById('toggleAudio');
                this.toggleVideoBtn = document.getElementById('toggleVideo');
            }
            
            setupEventListeners() {
                this.startCallBtn.addEventListener('click', () => this.startCall());
                this.endCallBtn.addEventListener('click', () => this.endCall());
                this.toggleAudioBtn.addEventListener('click', () => this.toggleAudio());
                this.toggleVideoBtn.addEventListener('click', () => this.toggleVideo());
                
                // 信令消息处理
                this.signaling.onMessage = (message) => this.handleSignalingMessage(message);
            }
            
            async startCall() {
                try {
                    // 获取媒体流
                    this.localStream = await navigator.mediaDevices.getUserMedia({
                        video: true,
                        audio: true
                    });
                    
                    this.localVideo.srcObject = this.localStream;
                    
                    // 创建对等连接
                    this.createPeerConnection();
                    
                    // 创建offer
                    const offer = await this.peerConnection.createOffer();
                    await this.peerConnection.setLocalDescription(offer);
                    
                    // 发送offer到信令服务器
                    this.signaling.send({
                        type: 'offer',
                        sdp: offer.sdp
                    });
                    
                    this.updateCallStatus(true);
                    
                } catch (error) {
                    console.error('开始通话失败:', error);
                    alert('无法访问摄像头或麦克风,请检查权限设置');
                }
            }
            
            createPeerConnection() {
                const configuration = {
                    iceServers: [
                        { urls: 'stun:stun.l.google.com:19302' }
                    ]
                };
                
                this.peerConnection = new RTCPeerConnection(configuration);
                
                // 添加本地流轨道
                this.localStream.getTracks().forEach(track => {
                    this.peerConnection.addTrack(track, this.localStream);
                });
                
                // 处理远程流
                this.peerConnection.ontrack = (event) => {
                    this.remoteStream = event.streams[0];
                    this.remoteVideo.srcObject = this.remoteStream;
                };
                
                // 处理ICE候选
                this.peerConnection.onicecandidate = (event) => {
                    if (event.candidate) {
                        this.signaling.send({
                            type: 'ice-candidate',
                            candidate: event.candidate
                        });
                    }
                };
            }
            
            handleSignalingMessage(message) {
                switch (message.type) {
                    case 'offer':
                        this.handleOffer(message);
                        break;
                    case 'answer':
                        this.handleAnswer(message);
                        break;
                    case 'ice-candidate':
                        this.handleIceCandidate(message);
                        break;
                }
            }
            
            async handleOffer(message) {
                if (!this.localStream) {
                    this.localStream = await navigator.mediaDevices.getUserMedia({
                        video: true,
                        audio: true
                    });
                    this.localVideo.srcObject = this.localStream;
                }
                
                this.createPeerConnection();
                await this.peerConnection.setRemoteDescription(
                    new RTCSessionDescription({ type: 'offer', sdp: message.sdp })
                );
                
                const answer = await this.peerConnection.createAnswer();
                await this.peerConnection.setLocalDescription(answer);
                
                this.signaling.send({
                    type: 'answer',
                    sdp: answer.sdp
                });
                
                this.updateCallStatus(true);
            }
            
            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 (error) {
                    console.error('添加ICE候选失败:', error);
                }
            }
            
            toggleAudio() {
                if (this.localStream) {
                    const audioTrack = this.localStream.getAudioTracks()[0];
                    if (audioTrack) {
                        audioTrack.enabled = !audioTrack.enabled;
                        this.toggleAudioBtn.textContent = audioTrack.enabled ? '静音' : '取消静音';
                    }
                }
            }
            
            toggleVideo() {
                if (this.localStream) {
                    const videoTrack = this.localStream.getVideoTracks()[0];
                    if (videoTrack) {
                        videoTrack.enabled = !videoTrack.enabled;
                        this.toggleVideoBtn.textContent = videoTrack.enabled ? '关闭视频' : '开启视频';
                    }
                }
            }
            
            endCall() {
                if (this.localStream) {
                    this.localStream.getTracks().forEach(track => track.stop());
                }
                
                if (this.peerConnection) {
                    this.peerConnection.close();
                }
                
                this.localVideo.srcObject = null;
                this.remoteVideo.srcObject = null;
                
                this.updateCallStatus(false);
            }
            
            updateCallStatus(isActive) {
                this.isCallActive = isActive;
                this.startCallBtn.disabled = isActive;
                this.endCallBtn.disabled = !isActive;
            }
        }
        
        // 初始化应用
        const app = new SWebRTCCall();
        
        // 连接信令服务器(需要实际的信令服务器地址)
        app.signaling.connect('ws://localhost:8080');
    </script>
</body>
</html>

高级功能

1. 屏幕共享

async function shareScreen() {
    try {
        const stream = await navigator.mediaDevices.getDisplayMedia({
            video: true,
            audio: true
        });
        return stream;
    } catch (error) {
        console.error('屏幕共享失败:', error);
    }
}

2. 录制功能

class SWebRTCRecorder {
    constructor() {
        this.mediaRecorder = null;
        this.recordedChunks = [];
    }
    
    startRecording(stream) {
        this.mediaRecorder = new MediaRecorder(stream);
        this.recordedChunks = [];
        
        this.mediaRecorder.ondataavailable = (event) => {
            if (event.data.size > 0) {
                this.recordedChunks.push(event.data);
            }
        };
        
        this.mediaRecorder.start();
    }
    
    stopRecording() {
        return new Promise((resolve) => {
            this.mediaRecorder.onstop = () => {
                const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
                resolve(blob);
            };
            this.mediaRecorder.stop();
        });
    }
}

最佳实践

1. 错误处理

// 完善的错误处理机制
function handleWebRTCError(error) {
    switch(error.name) {
        case 'NotAllowedError':
            console.error('用户拒绝了媒体权限请求');
            break;
        case 'NotFoundError':
            console.error('未找到指定的媒体设备');
            break;
        case 'NotReadableError':
            console.error('媒体设备被占用或不可用');
            break;
        case 'OverconstrainedError':
            console.error('无法满足指定的约束条件');
            break;
        default:
            console.error('WebRTC错误:', error);
    }
}

2. 性能优化

// 自适应码率控制
function adaptBitrate(connection, networkCondition) {
    const senders = connection.getSenders();
    senders.forEach(sender => {
        if (sender.track.kind === 'video') {
            const parameters = sender.getParameters();
            if (!parameters.encodings) {
                parameters.encodings = [{}];
            }
            
            // 根据网络状况调整码率
            parameters.encodings[0].maxBitrate = networkCondition === 'good' ? 
                2000000 : 500000; // 2Mbps或500Kbps
            
            sender.setParameters(parameters);
        }
    });
}

常见问题解决

1. 防火墙/NAT穿透问题

// 使用TURN服务器作为备选方案
const advancedConfiguration = {
    iceServers: [
        { urls: 'stun:stun.l.google.com:19302' },
        { 
            urls: 'turn:your-turn-server.com:3478',
            username: 'your-username',
            credential: 'your-password'
        }
    ]
};

2. 移动端适配

// 移动端特定配置
function getMobileConstraints() {
    return {
        video: {
            width: { ideal: 640 },
            height: { ideal: 480 },
            frameRate: { ideal: 24 }
        },
        audio: true
    };
}

引用文章和出处

  1. WebRTC官方文档 - https://webrtc.org/
  2. MDN WebRTC API文档 - https://developer.mozilla.org/zh-CN/docs/Web/API/WebRTC_API
  3. Google WebRTC Samples - https://webrtc.github.io/samples/
  4. WebRTC技术详解 - 《WebRTC权威指南》
  5. 实时通信协议分析 - RFC 7478, RFC 7742

总结

SWebRTC作为WebRTC的简化实现,为开发者提供了强大的实时通信能力。通过本文的指南,您应该能够:

  1. 理解SWebRTC的基本概念和工作原理
  2. 实现基本的音视频通话功能
  3. 处理常见的网络和兼容性问题
  4. 优化应用性能和用户体验

在实际开发中,建议根据具体需求选择合适的信令服务器方案,并充分考虑安全性和性能优化。随着WebRTC技术的不断发展,SWebRTC将在更多场景中发挥重要作用。

Logo

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

更多推荐