402 lines
12 KiB
JavaScript
402 lines
12 KiB
JavaScript
// // 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');
|
|
}
|
|
}
|
|
} |