@@ -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: 1 0,
// 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 @@
// },
// /**
// * Get WebRTC statistics
// * Get WebRTC statistics (оптимизировано)
// */
// async getConnectionStats(peerConnection) {
// if (!peerConnection) return null
@@ -48,27 +285,27 @@
// 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 @@
// },
// /**
// * Monitor connection quality
// * Monitor connection quality (увеличен интервал)
// */
// createQualityMonitor(peerConnection, callback, interval = 5 000) {
// createQualityMonitor(peerConnection, callback, interval = 10 000) {
// if (!peerConnection || typeof callback !== 'function') {
// return null
// }
@@ -111,8 +348,8 @@
// // 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,53 +363,76 @@
// // 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');
// }
// }
// }
// src/services/webrtc.js - WebRTC utility functions with forced AV1 codec
// 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' } ,
// Добавьте TURN сервер только при необходимости
// {
// urls: 'turn:your-turn-server.com:3478',
// username: 'username',
// credential: 'password'
// }
] ,
iceCandidatePoolSize : 0 , // Уменьшено для экономии ресурсов
bundlePolicy : 'max-bundle' , // Объединение каналов
rtcpMuxPolicy : 'require' // Объединение RTCP и RTP
iceCandidatePoolSize : 0 ,
bundlePolicy : 'max-bundle' ,
rtcpMuxPolicy : 'require'
}
} ,
/**
* Get optimized media constraints
* Get optimized media constraints for AV1
*/
getOptimizedConstraints ( ) {
async getOptimizedConstraints ( ) {
return {
video : {
width : { ideal : 640 , max : 1280 } ,
height : { ideal : 480 , max : 720 } ,
frameRate : { ideal : 15 , max : 30 }
} ,
width : { ideal : 640 , max : 1280 } ,
height : { ideal : 480 , max : 720 } ,
frameRate : { ideal : 15 , max : 30 }
} ,
audio : {
sampleRate : 10000 ,
channelCount : 1 ,
echoCancellation : true ,
noiseSuppression : true
noiseSuppression : true ,
}
}
} ,
@@ -186,13 +446,13 @@ export const webrtcService = {
const presets = {
low : {
maxBitrate : 150_000 ,
scaleResolutionDownBy : 4 .0,
maxFramerate : 10
scaleResolutionDownBy : 2 .0,
maxFramerate : 15
} ,
medium : {
maxBitrate : 400_000 ,
scaleResolutionDownBy : 2.0 ,
maxFramerate : 1 5
scaleResolutionDownBy : 1.5 ,
maxFramerate : 2 5
} ,
high : {
maxBitrate : 1_000_000 ,
@@ -202,61 +462,89 @@ export const webrtcService = {
} ;
const params = sender . getParameters ( ) ;
params . encodings = [ presets [ quality ] || presets . medium ] ;
params . encodings = [ presets [ quality ] || presets . high ] ;
sender . setParameters ( params ) ;
} ,
/**
* Apply simulcast for better quality adaptation
* Force AV1 codec in SDP
*/
enableSimulcast ( sender ) {
if ( ! sender || sender . track ? . kind !== 'video' ) return ;
forceAV1Codec ( sdp ) {
// Найдем AV1 payload type
const av1Payload = this . findAV1Payload ( sdp ) ;
if ( ! av1Payload ) {
console . warn ( 'AV1 codec not found in SDP, falling back to default codecs' ) ;
return sdp ;
}
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
// Переставим AV1 на первое место в m-line
const mLineParts = lines [ mLineIndex ] . split ( ' ' ) ;
const payloads = mLineParts . slice ( 3 ) ;
const newPayloads = [ p ayload, ... payloads . filter ( p => p !== p ayload) ] ;
const newPayloads = [ av1P ayload, ... payloads . filter ( p => p !== av1P ayload) ] ;
lines [ mLineIndex ] = [ ... mLineParts . slice ( 0 , 3 ) , ... newPayloads ] . join ( ' ' ) ;
return lines . join ( '\r\n' ) ;
// Удалим ненужные кодеки (опционально)
const filteredLines = this . removeUnwantedCodecs ( lines , av1Payload ) ;
return filteredLines . join ( '\r\n' ) ;
} ,
/**
* Extract codec information from SDP
* Find AV1 payload type in SDP
*/
extractCodecInfo ( sdp , codecName ) {
findAV1Payload ( sdp ) {
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 ] } ;
for ( const line of lines ) {
if ( line . includes ( ' a=rtpmap:' ) && line . toLowerCase ( ) . includes ( 'av1' ) ) {
const match = line. match ( /a=rtpmap:(\d+) av1/i ) ;
if ( match ) {
return match [ 1 ] ;
}
}
}
return null ;
} ,
/**
* Remove unwanted codecs from SDP (keep only AV1)
*/
removeUnwantedCodecs ( lines , av1Payload ) {
const filteredLines = [ ] ;
let skipNext = false ;
for ( let i = 0 ; i < lines . length ; i ++ ) {
const line = lines [ i ] ;
// Пропускаем описания других кодеков
if ( line . startsWith ( 'a=rtpmap:' ) && ! line . includes ( ` a=rtpmap: ${ av1Payload } ` ) ) {
skipNext = true ;
continue ;
}
if ( line . startsWith ( 'a=rtcp-fb:' ) && ! line . includes ( ` a=rtcp-fb: ${ av1Payload } ` ) ) {
continue ;
}
if ( line . startsWith ( 'a=fmtp:' ) && ! line . includes ( ` a=fmtp: ${ av1Payload } ` ) ) {
continue ;
}
if ( skipNext && line . startsWith ( 'a=' ) ) {
skipNext = false ;
continue ;
}
filteredLines . push ( line ) ;
}
return filteredLines ;
} ,
/**
* Test WebRTC support
*/
@@ -269,7 +557,56 @@ export const webrtcService = {
} ,
/**
* Get WebRTC statistics (оптимизировано)
* Create peer connection with AV1 support
*/
async createPeerConnection ( onIceCandidate , onTrack ) {
const config = this . getIceServerConfig ( ) ;
const peerConnection = new RTCPeerConnection ( config ) ;
peerConnection . onicecandidate = ( event ) => {
if ( event . candidate ) {
onIceCandidate ( event . candidate ) ;
}
} ;
peerConnection . ontrack = ( event ) => {
onTrack ( event ) ;
} ;
// Принудительно применяем AV1 к исходящим потокам
peerConnection . onnegotiationneeded = async ( ) => {
try {
const offer = await peerConnection . createOffer ( ) ;
offer . sdp = this . forceAV1Codec ( offer . sdp ) ;
await peerConnection . setLocalDescription ( offer ) ;
} catch ( error ) {
console . error ( 'Error forcing AV1 codec:' , error ) ;
}
} ;
return peerConnection ;
} ,
/**
* Create offer with forced AV1
*/
async createOfferWithAV1 ( peerConnection ) {
const offer = await peerConnection . createOffer ( ) ;
offer . sdp = this . forceAV1Codec ( offer . sdp ) ;
return offer ;
} ,
/**
* Create answer with forced AV1
*/
async createAnswerWithAV1 ( peerConnection ) {
const answer = await peerConnection . createAnswer ( ) ;
answer . sdp = this . forceAV1Codec ( answer . sdp ) ;
return answer ;
} ,
/**
* Get WebRTC statistics
*/
async getConnectionStats ( peerConnection ) {
if ( ! peerConnection ) return null
@@ -291,6 +628,7 @@ export const webrtcService = {
frameWidth : report . frameWidth || 0 ,
frameHeight : report . frameHeight || 0 ,
framesPerSecond : report . framesPerSecond || 0 ,
codecId : report . codecId || '' ,
}
} else if ( report . type === 'outbound-rtp' && report . mediaType === 'video' ) {
result . video . outbound = {
@@ -299,6 +637,7 @@ export const webrtcService = {
frameWidth : report . frameWidth || 0 ,
frameHeight : report . frameHeight || 0 ,
framesPerSecond : report . framesPerSecond || 0 ,
codecId : report . codecId || '' ,
}
} else if ( report . type === 'candidate-pair' && report . state === 'succeeded' ) {
result . connection = {
@@ -307,6 +646,15 @@ export const webrtcService = {
bytesReceived : report . bytesReceived || 0 ,
bytesSent : report . bytesSent || 0 ,
}
} else if ( report . type === 'codec' ) {
// Получаем информацию о кодеке
if ( report . id === result . video . inbound ? . codecId || report . id === result . video . outbound ? . codecId ) {
result . video . codec = {
mimeType : report . mimeType ,
clockRate : report . clockRate ,
sdpFmtpLine : report . sdpFmtpLine
} ;
}
}
} )
@@ -318,9 +666,9 @@ export const webrtcService = {
} ,
/**
* Monitor connection quality (увеличен интервал)
* Monitor connection quality
*/
createQualityMonitor ( peerConnection , callback , interval = 10 000) {
createQualityMonitor ( peerConnection , callback , interval = 5 000) {
if ( ! peerConnection || typeof callback !== 'function' ) {
return null
}
@@ -346,26 +694,31 @@ export const webrtcService = {
calculateQuality ( stats ) {
let score = 100
// Проверяем, используется ли AV1
if ( stats . video . codec && stats . video . codec . mimeType ) {
if ( stats . video . codec . mimeType . toLowerCase ( ) . includes ( 'av1' ) ) {
score += 10 ; // Бонус за использование AV1
}
}
// 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
score -= Math . min ( 50 , lossRate * 100 )
}
// Reduce score based on round trip time
if ( stats . connection ? . currentRoundTripTime ) {
const rtt = stats . connection . currentRoundTripTime * 1000 // Convert to ms
const rtt = stats . connection . currentRoundTripTime * 1000
if ( rtt > 150 ) {
score -= Math . min ( 30 , ( rtt - 150 ) / 10 ) // Up to 30 points for high latency
score -= Math . min ( 30 , ( rtt - 150 ) / 10 )
}
}
// 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 ) {
if ( fps < 15 ) {
score -= ( 15 - fps ) * 2
}
}
@@ -374,7 +727,7 @@ export const webrtcService = {
} ,
/**
* Adaptive quality control based on network conditions
* Adaptive quality control with AV1
*/
async adaptQuality ( peerConnection , qualityCallback ) {
if ( ! peerConnection ) return ;