// // src/services/webrtc.js - WebRTC utility functions // export const webrtcService = { // /** // * Get STUN/TURN server configuration // */ // getIceServerConfig() { // return { // iceServers: [ // { urls: 'stun:stun.l.google.com:19302' }, // { urls: 'stun:stun1.l.google.com:19302' }, // { urls: 'stun:stun2.l.google.com:19302' }, // // Add TURN servers for production // // { // // urls: 'turn:your-turn-server.com:3478', // // username: 'username', // // credential: 'password' // // } // ], // iceCandidatePoolSize: 10, // } // }, // /** // * Test WebRTC support // */ // isWebRTCSupported() { // return !!( // window.RTCPeerConnection || // window.webkitRTCPeerConnection || // window.mozRTCPeerConnection // ) // }, // /** // * Get WebRTC statistics // */ // async getConnectionStats(peerConnection) { // if (!peerConnection) return null // try { // const stats = await peerConnection.getStats() // const result = { // video: {}, // audio: {}, // connection: {}, // } // stats.forEach((report) => { // if (report.type === 'inbound-rtp' && report.mediaType === 'video') { // result.video.inbound = { // bytesReceived: report.bytesReceived, // packetsReceived: report.packetsReceived, // packetsLost: report.packetsLost, // frameWidth: report.frameWidth, // frameHeight: report.frameHeight, // framesPerSecond: report.framesPerSecond, // } // } else if (report.type === 'outbound-rtp' && report.mediaType === 'video') { // result.video.outbound = { // bytesSent: report.bytesSent, // packetsSent: report.packetsSent, // frameWidth: report.frameWidth, // frameHeight: report.frameHeight, // framesPerSecond: report.framesPerSecond, // } // } else if (report.type === 'candidate-pair' && report.state === 'succeeded') { // result.connection = { // currentRoundTripTime: report.currentRoundTripTime, // availableOutgoingBitrate: report.availableOutgoingBitrate, // bytesReceived: report.bytesReceived, // bytesSent: report.bytesSent, // } // } // }) // return result // } catch (error) { // console.error('Failed to get connection stats:', error) // return null // } // }, // /** // * Monitor connection quality // */ // createQualityMonitor(peerConnection, callback, interval = 5000) { // if (!peerConnection || typeof callback !== 'function') { // return null // } // const monitor = setInterval(async () => { // try { // const stats = await this.getConnectionStats(peerConnection) // if (stats) { // const quality = this.calculateQuality(stats) // callback(quality, stats) // } // } catch (error) { // console.error('Quality monitoring error:', error) // } // }, interval) // return monitor // }, // /** // * Calculate connection quality score (0-100) // */ // calculateQuality(stats) { // let score = 100 // // Reduce score based on packet loss // if (stats.video.inbound?.packetsLost && stats.video.inbound?.packetsReceived) { // const lossRate = stats.video.inbound.packetsLost / stats.video.inbound.packetsReceived // score -= lossRate * 50 // Up to 50 points for packet loss // } // // Reduce score based on round trip time // if (stats.connection?.currentRoundTripTime) { // const rtt = stats.connection.currentRoundTripTime * 1000 // Convert to ms // if (rtt > 150) { // score -= Math.min(30, (rtt - 150) / 10) // Up to 30 points for high latency // } // } // // Reduce score based on low frame rate // if (stats.video.inbound?.framesPerSecond) { // const fps = stats.video.inbound.framesPerSecond // if (fps < 15) { // score -= (15 - fps) * 2 // Up to 30 points for low FPS // } // } // return Math.max(0, Math.min(100, Math.round(score))) // }, // } // src/services/webrtc.js - WebRTC utility functions export const webrtcService = { /** * Get STUN/TURN server configuration (оптимизировано) */ getIceServerConfig() { return { iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, // Добавьте TURN сервер только при необходимости // { // urls: 'turn:your-turn-server.com:3478', // username: 'username', // credential: 'password' // } ], iceCandidatePoolSize: 0, // Уменьшено для экономии ресурсов bundlePolicy: 'max-bundle', // Объединение каналов rtcpMuxPolicy: 'require' // Объединение RTCP и RTP } }, /** * Get optimized media constraints */ getOptimizedConstraints() { return { video: { width: { ideal: 640, max: 1280 }, height: { ideal: 480, max: 720 }, frameRate: { ideal: 15, max: 30 } }, audio: { sampleRate: 10000, channelCount: 1, echoCancellation: true, noiseSuppression: true } } }, /** * Apply optimized parameters to video sender */ optimizeVideoSender(sender, quality = 'medium') { if (!sender || sender.track?.kind !== 'video') return; const presets = { low: { maxBitrate: 150_000, scaleResolutionDownBy: 4.0, maxFramerate: 10 }, medium: { maxBitrate: 400_000, scaleResolutionDownBy: 2.0, maxFramerate: 15 }, high: { maxBitrate: 1_000_000, scaleResolutionDownBy: 1.0, maxFramerate: 30 } }; const params = sender.getParameters(); params.encodings = [presets[quality] || presets.medium]; sender.setParameters(params); }, /** * Apply simulcast for better quality adaptation */ enableSimulcast(sender) { if (!sender || sender.track?.kind !== 'video') return; const params = sender.getParameters(); params.encodings = [ { rid: 'low', active: true, maxBitrate: 150_000, scaleResolutionDownBy: 4.0, maxFramerate: 10 }, { rid: 'medium', active: true, maxBitrate: 400_000, scaleResolutionDownBy: 2.0, maxFramerate: 15 }, { rid: 'high', active: true, maxBitrate: 1_000_000, scaleResolutionDownBy: 1.0, maxFramerate: 30 } ]; sender.setParameters(params); }, /** * Prefer specific codec in SDP */ preferCodec(sdp, codec) { const codecInfo = this.extractCodecInfo(sdp, codec); if (!codecInfo) return sdp; const { payload, rtpmap } = codecInfo; const lines = sdp.split('\r\n'); const mLineIndex = lines.findIndex(line => line.startsWith('m=video')); if (mLineIndex === -1) return sdp; // Изменяем порядок кодеков в m-line const mLineParts = lines[mLineIndex].split(' '); const payloads = mLineParts.slice(3); const newPayloads = [payload, ...payloads.filter(p => p !== payload)]; lines[mLineIndex] = [...mLineParts.slice(0, 3), ...newPayloads].join(' '); return lines.join('\r\n'); }, /** * Extract codec information from SDP */ extractCodecInfo(sdp, codecName) { const lines = sdp.split('\r\n'); for (let i = 0; i < lines.length; i++) { if (lines[i].includes(`a=rtpmap:`) && lines[i].toLowerCase().includes(codecName.toLowerCase())) { const payload = lines[i].split(' ')[0].split(':')[1]; return { payload, rtpmap: lines[i] }; } } return null; }, /** * Test WebRTC support */ isWebRTCSupported() { return !!( window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection ) }, /** * Get WebRTC statistics (оптимизировано) */ async getConnectionStats(peerConnection) { if (!peerConnection) return null try { const stats = await peerConnection.getStats() const result = { video: {}, audio: {}, connection: {}, } stats.forEach((report) => { if (report.type === 'inbound-rtp' && report.mediaType === 'video') { result.video.inbound = { bytesReceived: report.bytesReceived || 0, packetsReceived: report.packetsReceived || 0, packetsLost: report.packetsLost || 0, frameWidth: report.frameWidth || 0, frameHeight: report.frameHeight || 0, framesPerSecond: report.framesPerSecond || 0, } } else if (report.type === 'outbound-rtp' && report.mediaType === 'video') { result.video.outbound = { bytesSent: report.bytesSent || 0, packetsSent: report.packetsSent || 0, frameWidth: report.frameWidth || 0, frameHeight: report.frameHeight || 0, framesPerSecond: report.framesPerSecond || 0, } } else if (report.type === 'candidate-pair' && report.state === 'succeeded') { result.connection = { currentRoundTripTime: report.currentRoundTripTime || 0, availableOutgoingBitrate: report.availableOutgoingBitrate || 0, bytesReceived: report.bytesReceived || 0, bytesSent: report.bytesSent || 0, } } }) return result } catch (error) { console.error('Failed to get connection stats:', error) return null } }, /** * Monitor connection quality (увеличен интервал) */ createQualityMonitor(peerConnection, callback, interval = 10000) { if (!peerConnection || typeof callback !== 'function') { return null } const monitor = setInterval(async () => { try { const stats = await this.getConnectionStats(peerConnection) if (stats) { const quality = this.calculateQuality(stats) callback(quality, stats) } } catch (error) { console.error('Quality monitoring error:', error) } }, interval) return monitor }, /** * Calculate connection quality score (0-100) */ calculateQuality(stats) { let score = 100 // Reduce score based on packet loss if (stats.video.inbound?.packetsLost && stats.video.inbound?.packetsReceived) { const lossRate = stats.video.inbound.packetsLost / Math.max(1, stats.video.inbound.packetsReceived) score -= Math.min(50, lossRate * 100) // Up to 50 points for packet loss } // Reduce score based on round trip time if (stats.connection?.currentRoundTripTime) { const rtt = stats.connection.currentRoundTripTime * 1000 // Convert to ms if (rtt > 150) { score -= Math.min(30, (rtt - 150) / 10) // Up to 30 points for high latency } } // Reduce score based on low frame rate if (stats.video.inbound?.framesPerSecond) { const fps = stats.video.inbound.framesPerSecond if (fps < 10) { score -= (10 - fps) * 3 // Up to 30 points for very low FPS } else if (fps < 15) { score -= (15 - fps) * 2 } } return Math.max(0, Math.min(100, Math.round(score))) }, /** * Adaptive quality control based on network conditions */ async adaptQuality(peerConnection, qualityCallback) { if (!peerConnection) return; const senders = peerConnection.getSenders(); const videoSender = senders.find(sender => sender.track?.kind === 'video'); if (!videoSender) return; const stats = await this.getConnectionStats(peerConnection); if (!stats) return; const quality = this.calculateQuality(stats); qualityCallback(quality); // Адаптивное управление качеством if (quality < 30) { this.optimizeVideoSender(videoSender, 'low'); } else if (quality < 60) { this.optimizeVideoSender(videoSender, 'medium'); } else { this.optimizeVideoSender(videoSender, 'high'); } } }