fix webRTC
This commit is contained in:
@@ -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
|
// src/services/webrtc.js - WebRTC utility functions
|
||||||
export const webrtcService = {
|
export const webrtcService = {
|
||||||
/**
|
/**
|
||||||
* Get STUN/TURN server configuration
|
* Get STUN/TURN server configuration (оптимизировано)
|
||||||
*/
|
*/
|
||||||
getIceServerConfig() {
|
getIceServerConfig() {
|
||||||
return {
|
return {
|
||||||
iceServers: [
|
iceServers: [
|
||||||
{ urls: 'stun:stun.l.google.com:19302' },
|
{ urls: 'stun:stun.l.google.com:19302' },
|
||||||
{ urls: 'stun:stun1.l.google.com:19302' },
|
// Добавьте TURN сервер только при необходимости
|
||||||
{ urls: 'stun:stun2.l.google.com:19302' },
|
|
||||||
// Add TURN servers for production
|
|
||||||
// {
|
// {
|
||||||
// urls: 'turn:your-turn-server.com:3478',
|
// urls: 'turn:your-turn-server.com:3478',
|
||||||
// username: 'username',
|
// username: 'username',
|
||||||
// credential: 'password'
|
// 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
|
* Test WebRTC support
|
||||||
*/
|
*/
|
||||||
@@ -32,7 +269,7 @@ export const webrtcService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get WebRTC statistics
|
* Get WebRTC statistics (оптимизировано)
|
||||||
*/
|
*/
|
||||||
async getConnectionStats(peerConnection) {
|
async getConnectionStats(peerConnection) {
|
||||||
if (!peerConnection) return null
|
if (!peerConnection) return null
|
||||||
@@ -48,27 +285,27 @@ export const webrtcService = {
|
|||||||
stats.forEach((report) => {
|
stats.forEach((report) => {
|
||||||
if (report.type === 'inbound-rtp' && report.mediaType === 'video') {
|
if (report.type === 'inbound-rtp' && report.mediaType === 'video') {
|
||||||
result.video.inbound = {
|
result.video.inbound = {
|
||||||
bytesReceived: report.bytesReceived,
|
bytesReceived: report.bytesReceived || 0,
|
||||||
packetsReceived: report.packetsReceived,
|
packetsReceived: report.packetsReceived || 0,
|
||||||
packetsLost: report.packetsLost,
|
packetsLost: report.packetsLost || 0,
|
||||||
frameWidth: report.frameWidth,
|
frameWidth: report.frameWidth || 0,
|
||||||
frameHeight: report.frameHeight,
|
frameHeight: report.frameHeight || 0,
|
||||||
framesPerSecond: report.framesPerSecond,
|
framesPerSecond: report.framesPerSecond || 0,
|
||||||
}
|
}
|
||||||
} else if (report.type === 'outbound-rtp' && report.mediaType === 'video') {
|
} else if (report.type === 'outbound-rtp' && report.mediaType === 'video') {
|
||||||
result.video.outbound = {
|
result.video.outbound = {
|
||||||
bytesSent: report.bytesSent,
|
bytesSent: report.bytesSent || 0,
|
||||||
packetsSent: report.packetsSent,
|
packetsSent: report.packetsSent || 0,
|
||||||
frameWidth: report.frameWidth,
|
frameWidth: report.frameWidth || 0,
|
||||||
frameHeight: report.frameHeight,
|
frameHeight: report.frameHeight || 0,
|
||||||
framesPerSecond: report.framesPerSecond,
|
framesPerSecond: report.framesPerSecond || 0,
|
||||||
}
|
}
|
||||||
} else if (report.type === 'candidate-pair' && report.state === 'succeeded') {
|
} else if (report.type === 'candidate-pair' && report.state === 'succeeded') {
|
||||||
result.connection = {
|
result.connection = {
|
||||||
currentRoundTripTime: report.currentRoundTripTime,
|
currentRoundTripTime: report.currentRoundTripTime || 0,
|
||||||
availableOutgoingBitrate: report.availableOutgoingBitrate,
|
availableOutgoingBitrate: report.availableOutgoingBitrate || 0,
|
||||||
bytesReceived: report.bytesReceived,
|
bytesReceived: report.bytesReceived || 0,
|
||||||
bytesSent: report.bytesSent,
|
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') {
|
if (!peerConnection || typeof callback !== 'function') {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -111,8 +348,8 @@ export const webrtcService = {
|
|||||||
|
|
||||||
// Reduce score based on packet loss
|
// Reduce score based on packet loss
|
||||||
if (stats.video.inbound?.packetsLost && stats.video.inbound?.packetsReceived) {
|
if (stats.video.inbound?.packetsLost && stats.video.inbound?.packetsReceived) {
|
||||||
const lossRate = stats.video.inbound.packetsLost / stats.video.inbound.packetsReceived
|
const lossRate = stats.video.inbound.packetsLost / Math.max(1, stats.video.inbound.packetsReceived)
|
||||||
score -= lossRate * 50 // Up to 50 points for packet loss
|
score -= Math.min(50, lossRate * 100) // Up to 50 points for packet loss
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reduce score based on round trip time
|
// Reduce score based on round trip time
|
||||||
@@ -126,11 +363,40 @@ export const webrtcService = {
|
|||||||
// Reduce score based on low frame rate
|
// Reduce score based on low frame rate
|
||||||
if (stats.video.inbound?.framesPerSecond) {
|
if (stats.video.inbound?.framesPerSecond) {
|
||||||
const fps = stats.video.inbound.framesPerSecond
|
const fps = stats.video.inbound.framesPerSecond
|
||||||
if (fps < 15) {
|
if (fps < 10) {
|
||||||
score -= (15 - fps) * 2 // Up to 30 points for low FPS
|
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)))
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user