add av1 codec
This commit is contained in:
@@ -55,14 +55,13 @@ export const mediaService = {
|
||||
// Default constraints
|
||||
let constraints = {
|
||||
video: {
|
||||
width: { ideal: 1280, max: 1920 },
|
||||
height: { ideal: 720, max: 1080 },
|
||||
frameRate: { ideal: 30, max: 60 },
|
||||
},
|
||||
width: { ideal: 640, max: 1280 },
|
||||
height: { ideal: 480, max: 720 },
|
||||
frameRate: { ideal: 15, max: 30 }
|
||||
},
|
||||
audio: {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
// },
|
||||
|
||||
// /**
|
||||
// * 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 = 5000) {
|
||||
// createQualityMonitor(peerConnection, callback, interval = 10000) {
|
||||
// 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: 15
|
||||
scaleResolutionDownBy: 1.5,
|
||||
maxFramerate: 25
|
||||
},
|
||||
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 = [payload, ...payloads.filter(p => p !== payload)];
|
||||
const newPayloads = [av1Payload, ...payloads.filter(p => p !== av1Payload)];
|
||||
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 = 10000) {
|
||||
createQualityMonitor(peerConnection, callback, interval = 5000) {
|
||||
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;
|
||||
|
||||
@@ -21,10 +21,10 @@ export const useWebRTCStore = defineStore('webrtc', () => {
|
||||
// Media constraints
|
||||
const mediaConstraints = ref({
|
||||
video: {
|
||||
width: { ideal: 1280, max: 1920 },
|
||||
height: { ideal: 720, max: 1080 },
|
||||
frameRate: { ideal: 30, max: 60 },
|
||||
},
|
||||
width: { ideal: 640, max: 1280 },
|
||||
height: { ideal: 480, max: 720 },
|
||||
frameRate: { ideal: 15, max: 30 }
|
||||
},
|
||||
audio: {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
@@ -43,10 +43,9 @@ export const useWebRTCStore = defineStore('webrtc', () => {
|
||||
const rtcConfiguration = {
|
||||
iceServers: [
|
||||
{ urls: 'stun:stun.l.google.com:19302' },
|
||||
{ urls: 'stun:stun1.l.google.com:19302' },
|
||||
// Add TURN servers here for production
|
||||
],
|
||||
iceCandidatePoolSize: 10,
|
||||
iceCandidatePoolSize: 0,
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
Reference in New Issue
Block a user