add av1 codec

This commit is contained in:
2025-09-07 14:08:11 +00:00
parent 8324597a84
commit 0e95d01786
4 changed files with 452 additions and 100 deletions

View File

@@ -15,6 +15,7 @@ pycparser==2.22
pypng==0.20220715.0
python-decouple==3.8
pytz==2025.2
psutil==7.0.0
qrcode==7.4.2
redis==5.0.1
sqlparse==0.5.3

View File

@@ -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,
},
}

View File

@@ -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;

View File

@@ -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