efd697a9d3
Composants créés : - CoreAudioBackend.js : Backend audio macOS natif (naudiodon/PortAudio) - Énumération et sélection devices audio - Capture audio 48kHz mono/stereo - Lecture audio avec buffer circulaire - Gestion underrun/overrun - OpusCodec.js : Encodeur/décodeur Opus - Support 32-320 kbps configurable - Présets voix (économique, standard, HD) et musique - Frame 20ms (960 samples à 48kHz) - Statistiques encode/decode - JitterBuffer.js : Buffer FIFO adaptatif - Cible 40ms (2 frames) - Détection underrun/overrun - Mode adaptatif pour conditions réseau variables - Statistiques latence et santé buffer - LiveKitClient.js : Client LiveKit pour bridge - Connexion room en tant que participant "AudioBridge" - Publication/souscription tracks audio - Reconnexion automatique - Gestion événements participants - AudioBridge.js : Classe principale orchestration - Détection automatique backend (CoreAudio macOS) - Routing bidirectionnel CoreAudio ↔ Opus ↔ LiveKit - Configuration via présets ou custom - Logs détaillés et statistiques temps réel Dépendances ajoutées : - opusscript : Codec Opus JavaScript - naudiodon : Bindings natifs PortAudio/CoreAudio - livekit-client : SDK LiveKit côté serveur TODO.md mis à jour avec tâches Phase 1.3 complétées. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
294 lines
7.5 KiB
JavaScript
294 lines
7.5 KiB
JavaScript
/**
|
|
* OpusCodec.js
|
|
* Wrapper pour encoder/décoder audio avec Opus
|
|
*
|
|
* Gère :
|
|
* - Encodage PCM 16-bit → Opus
|
|
* - Décodage Opus → PCM 16-bit
|
|
* - Configuration bitrate (32-320 kbps)
|
|
* - Frame size flexible (20ms par défaut)
|
|
*/
|
|
|
|
import OpusScript from 'opusscript';
|
|
|
|
export class OpusCodec {
|
|
constructor(options = {}) {
|
|
this.options = {
|
|
sampleRate: options.sampleRate || 48000,
|
|
channels: options.channels || 1,
|
|
bitrate: options.bitrate || 96000, // 96kbps par défaut (voix standard)
|
|
frameSize: options.frameSize || 960, // 20ms à 48kHz
|
|
application: options.application || 'voip', // 'voip' | 'audio' | 'restricted_lowdelay'
|
|
...options
|
|
};
|
|
|
|
// Validation
|
|
this._validateOptions();
|
|
|
|
// Création des encodeurs/décodeurs Opus
|
|
this.encoder = null;
|
|
this.decoder = null;
|
|
|
|
this._initCodecs();
|
|
|
|
// Statistiques
|
|
this.stats = {
|
|
encoded: 0,
|
|
decoded: 0,
|
|
encodeErrors: 0,
|
|
decodeErrors: 0
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Valide les options
|
|
* @private
|
|
*/
|
|
_validateOptions() {
|
|
const validSampleRates = [8000, 12000, 16000, 24000, 48000];
|
|
if (!validSampleRates.includes(this.options.sampleRate)) {
|
|
throw new Error(`Sample rate invalide : ${this.options.sampleRate}. Valeurs acceptées : ${validSampleRates.join(', ')}`);
|
|
}
|
|
|
|
if (this.options.channels < 1 || this.options.channels > 2) {
|
|
throw new Error(`Nombre de canaux invalide : ${this.options.channels}. Doit être 1 (mono) ou 2 (stereo)`);
|
|
}
|
|
|
|
if (this.options.bitrate < 6000 || this.options.bitrate > 510000) {
|
|
throw new Error(`Bitrate invalide : ${this.options.bitrate}. Doit être entre 6000 et 510000 bps`);
|
|
}
|
|
|
|
const validApplications = ['voip', 'audio', 'restricted_lowdelay'];
|
|
if (!validApplications.includes(this.options.application)) {
|
|
throw new Error(`Application invalide : ${this.options.application}. Valeurs acceptées : ${validApplications.join(', ')}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialise les codecs Opus
|
|
* @private
|
|
*/
|
|
_initCodecs() {
|
|
try {
|
|
// Mapping des applications
|
|
const appMapping = {
|
|
'voip': OpusScript.Application.VOIP,
|
|
'audio': OpusScript.Application.AUDIO,
|
|
'restricted_lowdelay': OpusScript.Application.RESTRICTED_LOWDELAY
|
|
};
|
|
|
|
// Création encoder
|
|
this.encoder = new OpusScript(
|
|
this.options.sampleRate,
|
|
this.options.channels,
|
|
appMapping[this.options.application]
|
|
);
|
|
|
|
// Configuration bitrate
|
|
this.encoder.setBitrate(this.options.bitrate);
|
|
|
|
// Création decoder
|
|
this.decoder = new OpusScript(
|
|
this.options.sampleRate,
|
|
this.options.channels,
|
|
appMapping[this.options.application]
|
|
);
|
|
|
|
console.log(`✓ Opus codec initialisé : ${this.options.sampleRate}Hz, ${this.options.channels}ch, ${this.options.bitrate / 1000}kbps`);
|
|
} catch (error) {
|
|
console.error('Erreur initialisation codec Opus:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Encode des données PCM en Opus
|
|
* @param {Buffer} pcmData - Données PCM 16-bit signed
|
|
* @returns {Buffer|null} Données Opus encodées ou null en cas d'erreur
|
|
*/
|
|
encode(pcmData) {
|
|
if (!this.encoder) {
|
|
console.error('Encoder non initialisé');
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
// Conversion Buffer → Int16Array pour OpusScript
|
|
const pcmInt16 = new Int16Array(
|
|
pcmData.buffer,
|
|
pcmData.byteOffset,
|
|
pcmData.byteLength / 2
|
|
);
|
|
|
|
// Vérification taille frame
|
|
const expectedSamples = this.options.frameSize * this.options.channels;
|
|
if (pcmInt16.length !== expectedSamples) {
|
|
console.warn(`Taille frame incorrecte : ${pcmInt16.length} samples (attendu ${expectedSamples})`);
|
|
// Padding ou truncate si nécessaire
|
|
const adjusted = new Int16Array(expectedSamples);
|
|
adjusted.set(pcmInt16.slice(0, expectedSamples));
|
|
const opusData = this.encoder.encode(adjusted, this.options.frameSize);
|
|
this.stats.encoded++;
|
|
return Buffer.from(opusData);
|
|
}
|
|
|
|
// Encodage
|
|
const opusData = this.encoder.encode(pcmInt16, this.options.frameSize);
|
|
this.stats.encoded++;
|
|
|
|
return Buffer.from(opusData);
|
|
} catch (error) {
|
|
console.error('Erreur encodage Opus:', error);
|
|
this.stats.encodeErrors++;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Décode des données Opus en PCM
|
|
* @param {Buffer} opusData - Données Opus
|
|
* @returns {Buffer|null} Données PCM 16-bit ou null en cas d'erreur
|
|
*/
|
|
decode(opusData) {
|
|
if (!this.decoder) {
|
|
console.error('Decoder non initialisé');
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
// Décodage
|
|
const pcmInt16 = this.decoder.decode(opusData, this.options.frameSize);
|
|
this.stats.decoded++;
|
|
|
|
// Conversion Int16Array → Buffer
|
|
const pcmBuffer = Buffer.from(pcmInt16.buffer);
|
|
return pcmBuffer;
|
|
} catch (error) {
|
|
console.error('Erreur décodage Opus:', error);
|
|
this.stats.decodeErrors++;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Change le bitrate de l'encodeur
|
|
* @param {number} bitrate - Nouveau bitrate en bps (6000-510000)
|
|
*/
|
|
setBitrate(bitrate) {
|
|
if (bitrate < 6000 || bitrate > 510000) {
|
|
console.error(`Bitrate invalide : ${bitrate}`);
|
|
return;
|
|
}
|
|
|
|
if (this.encoder) {
|
|
this.encoder.setBitrate(bitrate);
|
|
this.options.bitrate = bitrate;
|
|
console.log(`✓ Bitrate Opus mis à jour : ${bitrate / 1000}kbps`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obtient les statistiques du codec
|
|
* @returns {Object}
|
|
*/
|
|
getStats() {
|
|
return {
|
|
...this.stats,
|
|
config: {
|
|
sampleRate: this.options.sampleRate,
|
|
channels: this.options.channels,
|
|
bitrate: this.options.bitrate,
|
|
frameSize: this.options.frameSize,
|
|
application: this.options.application
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Réinitialise les statistiques
|
|
*/
|
|
resetStats() {
|
|
this.stats = {
|
|
encoded: 0,
|
|
decoded: 0,
|
|
encodeErrors: 0,
|
|
decodeErrors: 0
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Détruit le codec et libère les ressources
|
|
*/
|
|
destroy() {
|
|
this.encoder = null;
|
|
this.decoder = null;
|
|
console.log('✓ OpusCodec détruit');
|
|
}
|
|
|
|
/**
|
|
* Calcule la taille d'une frame en millisecondes
|
|
* @returns {number} Durée en ms
|
|
*/
|
|
getFrameDuration() {
|
|
return (this.options.frameSize / this.options.sampleRate) * 1000;
|
|
}
|
|
|
|
/**
|
|
* Calcule le nombre de bytes PCM pour une frame
|
|
* @returns {number} Taille en bytes
|
|
*/
|
|
getFrameSizeBytes() {
|
|
// PCM 16-bit = 2 bytes par sample
|
|
return this.options.frameSize * this.options.channels * 2;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Présets de configuration Opus selon le cas d'usage
|
|
*/
|
|
export const OpusPresets = {
|
|
// Voix économique (WiFi limité, faible bande passante)
|
|
VOICE_LOW: {
|
|
bitrate: 32000, // 32 kbps
|
|
application: 'voip'
|
|
},
|
|
|
|
// Voix économique améliorée
|
|
VOICE_ECONOMY: {
|
|
bitrate: 64000, // 64 kbps
|
|
application: 'voip'
|
|
},
|
|
|
|
// Voix standard (défaut, bon compromis)
|
|
VOICE_STANDARD: {
|
|
bitrate: 96000, // 96 kbps
|
|
application: 'voip'
|
|
},
|
|
|
|
// Voix HD (qualité maximale voix)
|
|
VOICE_HD: {
|
|
bitrate: 128000, // 128 kbps
|
|
application: 'voip'
|
|
},
|
|
|
|
// Voix ultra HD
|
|
VOICE_ULTRA: {
|
|
bitrate: 192000, // 192 kbps
|
|
application: 'audio'
|
|
},
|
|
|
|
// Musique/monitoring (si besoin événementiel)
|
|
MUSIC: {
|
|
bitrate: 256000, // 256 kbps
|
|
application: 'audio'
|
|
},
|
|
|
|
// Musique haute qualité
|
|
MUSIC_HQ: {
|
|
bitrate: 320000, // 320 kbps
|
|
application: 'audio'
|
|
}
|
|
};
|
|
|
|
export default OpusCodec;
|