diff --git a/videocall-frontend/src/services/webrtc.js b/videocall-frontend/src/services/webrtc.js index 2521636..d60dcde 100644 --- a/videocall-frontend/src/services/webrtc.js +++ b/videocall-frontend/src/services/webrtc.js @@ -1,25 +1,262 @@ +// // 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 + * 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 + // Добавьте TURN сервер только при необходимости // { // urls: 'turn:your-turn-server.com:3478', // username: 'username', // credential: 'password' // } ], - iceCandidatePoolSize: 10, + 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 */ @@ -32,7 +269,7 @@ export const webrtcService = { }, /** - * Get WebRTC statistics + * Get WebRTC statistics (оптимизировано) */ async getConnectionStats(peerConnection) { if (!peerConnection) return null @@ -48,27 +285,27 @@ export const webrtcService = { 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, + 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, - packetsSent: report.packetsSent, - frameWidth: report.frameWidth, - frameHeight: report.frameHeight, - framesPerSecond: report.framesPerSecond, + 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, - availableOutgoingBitrate: report.availableOutgoingBitrate, - bytesReceived: report.bytesReceived, - bytesSent: report.bytesSent, + currentRoundTripTime: report.currentRoundTripTime || 0, + availableOutgoingBitrate: report.availableOutgoingBitrate || 0, + bytesReceived: report.bytesReceived || 0, + bytesSent: report.bytesSent || 0, } } }) @@ -81,9 +318,9 @@ export const webrtcService = { }, /** - * Monitor connection quality + * Monitor connection quality (увеличен интервал) */ - createQualityMonitor(peerConnection, callback, interval = 5000) { + createQualityMonitor(peerConnection, callback, interval = 10000) { if (!peerConnection || typeof callback !== 'function') { return null } @@ -111,8 +348,8 @@ export const webrtcService = { // 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 + 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 @@ -126,11 +363,40 @@ export const webrtcService = { // 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 + 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'); + } + } +} \ No newline at end of file