feat: ajout support Linux avec backends JACK et PipeWire

Phase 3.1 - Support Linux professionnel

Nouveaux backends audio:
- JACKBackend.js : support JACK Audio Connection Kit pour audio pro
- PipeWireBackend.js : support PipeWire (standard moderne Linux)
- Detection automatique dans AudioBridge (PipeWire > JACK > erreur)

Script installation:
- install/linux.sh pour Ubuntu/Debian/Arch/Fedora
- Installation automatique dependencies (Node.js, PipeWire/JACK)
- Telechargement LiveKit Server pour Linux (amd64/arm64)

Fonctionnalites:
- Detection serveur audio (PipeWire/JACK)
- Enumeration devices audio via pactl/jack_lsp
- Capture et lecture audio basse latence (pw-cat, jack_rec/play)
- Messages d'erreur detailles pour troubleshooting
- Compatibilite Ubuntu 22.04+, Debian 11+, Arch Linux, Fedora

TODO.md mis a jour: Phase 3.1 en cours
This commit is contained in:
2026-05-26 13:37:18 +02:00
parent 9654c7f421
commit 37205f0409
5 changed files with 1166 additions and 20 deletions
+49 -11
View File
@@ -13,6 +13,8 @@
import { EventEmitter } from 'events';
import { platform } from 'os';
import CoreAudioBackend from './backends/CoreAudioBackend.js';
import JACKBackend from './backends/JACKBackend.js';
import PipeWireBackend from './backends/PipeWireBackend.js';
import OpusCodec, { OpusPresets } from './OpusCodec.js';
import JitterBuffer, { JitterBufferPresets } from './JitterBuffer.js';
import LiveKitClient from './LiveKitClient.js';
@@ -123,27 +125,52 @@ export class AudioBridge extends EventEmitter {
*/
async _initAudioBackend() {
const os = platform();
let BackendClass = null;
let devices = [];
// macOS : CoreAudio prioritaire
if (os === 'darwin') {
if (CoreAudioBackend.isAvailable()) {
this.backendType = 'CoreAudio';
this.audioBackend = new CoreAudioBackend({
sampleRate: this.options.sampleRate,
channels: this.options.channels,
framesPerBuffer: this.options.frameSize,
inputDeviceId: this.options.inputDeviceId,
outputDeviceId: this.options.outputDeviceId
});
BackendClass = CoreAudioBackend;
console.log('✓ Backend audio : CoreAudio (macOS natif)');
} else {
throw new Error('CoreAudio non disponible sur ce système');
}
}
// Linux : JACK ou PipeWire (Phase 3)
// Linux : PipeWire > JACK (ordre de préférence)
else if (os === 'linux') {
throw new Error('Support Linux non encore implémenté (Phase 3)');
// Détection automatique : préfère PipeWire (moderne) puis JACK (pro)
if (PipeWireBackend.isAvailable() && PipeWireBackend.isServerRunning()) {
this.backendType = 'PipeWire';
BackendClass = PipeWireBackend;
console.log('✓ Backend audio : PipeWire (Linux moderne)');
} else if (JACKBackend.isAvailable() && JACKBackend.isServerRunning()) {
this.backendType = 'JACK';
BackendClass = JACKBackend;
console.log('✓ Backend audio : JACK (Linux professionnel)');
} else {
// Aucun backend disponible
const pipewireInstalled = PipeWireBackend.isAvailable();
const jackInstalled = JACKBackend.isAvailable();
let errorMsg = 'Aucun backend audio disponible sur Linux.\n';
if (!pipewireInstalled && !jackInstalled) {
errorMsg += 'Installez PipeWire (recommandé) ou JACK :\n';
errorMsg += ' Ubuntu/Debian : sudo apt install pipewire pipewire-pulse\n';
errorMsg += ' Arch Linux : sudo pacman -S pipewire pipewire-pulse\n';
errorMsg += ' JACK : sudo apt install jackd2 jack-tools';
} else if (pipewireInstalled && !PipeWireBackend.isServerRunning()) {
errorMsg += 'PipeWire installé mais non démarré.\n';
errorMsg += 'Démarrez-le : systemctl --user start pipewire pipewire-pulse';
} else if (jackInstalled && !JACKBackend.isServerRunning()) {
errorMsg += 'JACK installé mais serveur non démarré.\n';
errorMsg += 'Démarrez-le : jackd -d alsa -r 48000';
}
throw new Error(errorMsg);
}
}
// Windows : WASAPI (futur)
else if (os === 'win32') {
@@ -153,8 +180,19 @@ export class AudioBridge extends EventEmitter {
throw new Error(`Plateforme non supportée : ${os}`);
}
// Initialisation du backend sélectionné
this.audioBackend = new BackendClass({
sampleRate: this.options.sampleRate,
channels: this.options.channels,
framesPerBuffer: this.options.frameSize,
inputDeviceId: this.options.inputDeviceId,
outputDeviceId: this.options.outputDeviceId,
// Options spécifiques PipeWire
latency: this.options.latency || 20
});
// Liste des devices disponibles
const devices = CoreAudioBackend.getDevices();
devices = BackendClass.getDevices();
console.log(`📻 Devices audio détectés : ${devices.length}`);
devices.forEach(d => {
console.log(` - ${d.name} (in:${d.maxInputChannels}, out:${d.maxOutputChannels})`);
+404
View File
@@ -0,0 +1,404 @@
/**
* JACKBackend.js
* Backend audio pour Linux utilisant JACK Audio Connection Kit
*
* Gère :
* - Connexion au serveur JACK
* - Ports audio input/output
* - Capture et lecture audio temps réel
* - Détection automatique du serveur JACK
*/
import { spawn, execSync } from 'child_process';
import { EventEmitter } from 'events';
export class JACKBackend extends EventEmitter {
constructor(options = {}) {
super();
this.options = {
sampleRate: options.sampleRate || 48000,
channels: options.channels || 1,
framesPerBuffer: options.framesPerBuffer || 960, // 20ms à 48kHz
clientName: options.clientName || 'PTTLive',
autoConnect: options.autoConnect !== false,
inputPorts: options.inputPorts || [],
outputPorts: options.outputPorts || [],
...options
};
this.jackProcess = null;
this.isCapturing = false;
this.isPlaying = false;
this.playbackBuffer = [];
this.maxBufferSize = 10;
// Ports JACK créés
this.capturePort = null;
this.playbackPort = null;
}
/**
* Vérifie si JACK est installé et disponible
* @returns {boolean}
*/
static isAvailable() {
try {
execSync('which jackd', { stdio: 'ignore' });
return true;
} catch (error) {
return false;
}
}
/**
* Vérifie si le serveur JACK est en cours d'exécution
* @returns {boolean}
*/
static isServerRunning() {
try {
execSync('jack_lsp', { stdio: 'ignore' });
return true;
} catch (error) {
return false;
}
}
/**
* Liste tous les ports JACK disponibles
* @returns {Array} Liste des ports
*/
static getPorts() {
try {
const output = execSync('jack_lsp', { encoding: 'utf8' });
const ports = output.trim().split('\n').filter(p => p.length > 0);
return ports.map(port => {
const isOutput = port.includes('capture') || port.includes('output');
const isInput = port.includes('playback') || port.includes('input');
return {
name: port,
type: isOutput ? 'output' : (isInput ? 'input' : 'unknown'),
isPhysical: port.includes('system:')
};
});
} catch (error) {
console.error('Erreur listage ports JACK:', error);
return [];
}
}
/**
* Liste les devices audio via JACK (ports système)
* @returns {Array} Liste des devices
*/
static getDevices() {
if (!this.isServerRunning()) {
console.warn('Serveur JACK non démarré');
return [];
}
try {
const ports = this.getPorts();
const systemPorts = ports.filter(p => p.isPhysical);
// Grouper par device (system:capture_*, system:playback_*)
const devices = [];
// Ports d'entrée (capture)
const capturePorts = systemPorts.filter(p => p.name.includes('capture'));
if (capturePorts.length > 0) {
devices.push({
id: 'jack-input',
name: 'JACK System Capture',
maxInputChannels: capturePorts.length,
maxOutputChannels: 0,
defaultSampleRate: this._getServerSampleRate(),
hostAPIName: 'JACK',
ports: capturePorts.map(p => p.name)
});
}
// Ports de sortie (playback)
const playbackPorts = systemPorts.filter(p => p.name.includes('playback'));
if (playbackPorts.length > 0) {
devices.push({
id: 'jack-output',
name: 'JACK System Playback',
maxInputChannels: 0,
maxOutputChannels: playbackPorts.length,
defaultSampleRate: this._getServerSampleRate(),
hostAPIName: 'JACK',
ports: playbackPorts.map(p => p.name)
});
}
return devices;
} catch (error) {
console.error('Erreur énumération devices JACK:', error);
return [];
}
}
/**
* Récupère le sample rate du serveur JACK
* @returns {number}
* @private
*/
static _getServerSampleRate() {
try {
const output = execSync('jack_samplerate', { encoding: 'utf8' });
return parseInt(output.trim()) || 48000;
} catch (error) {
return 48000;
}
}
/**
* Récupère la taille du buffer du serveur JACK
* @returns {number}
* @private
*/
static _getServerBufferSize() {
try {
const output = execSync('jack_bufsize', { encoding: 'utf8' });
return parseInt(output.trim()) || 1024;
} catch (error) {
return 1024;
}
}
/**
* Trouve le device par défaut pour l'entrée
* @returns {Object|null}
*/
static getDefaultInputDevice() {
const devices = this.getDevices();
return devices.find(d => d.maxInputChannels > 0) || null;
}
/**
* Trouve le device par défaut pour la sortie
* @returns {Object|null}
*/
static getDefaultOutputDevice() {
const devices = this.getDevices();
return devices.find(d => d.maxOutputChannels > 0) || null;
}
/**
* Démarre la capture audio
* @returns {Promise<void>}
*/
async startCapture() {
if (this.isCapturing) {
console.warn('Capture JACK déjà active');
return;
}
if (!JACKBackend.isServerRunning()) {
throw new Error('Serveur JACK non démarré. Lancez jackd avant de continuer.');
}
try {
// Utilisation de jack_rec pour capturer l'audio
const portName = this.options.inputPorts[0] || 'system:capture_1';
this.jackProcess = spawn('jack_rec', [
'-f', '-', // Sortie vers stdout
'-d', String(this.options.framesPerBuffer),
'-b', '16', // 16-bit PCM
portName
]);
this.jackProcess.stdout.on('data', (audioData) => {
// Émet les données audio capturées (Buffer PCM 16-bit)
this.emit('audioData', audioData);
});
this.jackProcess.stderr.on('data', (data) => {
console.error('JACK stderr:', data.toString());
});
this.jackProcess.on('error', (error) => {
console.error('Erreur processus JACK:', error);
this.emit('error', error);
});
this.jackProcess.on('close', () => {
console.log('Processus JACK capture fermé');
this.isCapturing = false;
});
this.isCapturing = true;
console.log(`✓ Capture JACK démarrée : ${this.options.sampleRate}Hz, ${this.options.channels}ch`);
console.log(` Port: ${portName}`);
} catch (error) {
console.error('Erreur démarrage capture JACK:', error);
throw error;
}
}
/**
* Arrête la capture audio
*/
stopCapture() {
if (this.jackProcess && this.isCapturing) {
this.jackProcess.kill('SIGTERM');
this.jackProcess = null;
this.isCapturing = false;
console.log('✓ Capture JACK arrêtée');
}
}
/**
* Démarre la lecture audio
* @returns {Promise<void>}
*/
async startPlayback() {
if (this.isPlaying) {
console.warn('Lecture JACK déjà active');
return;
}
if (!JACKBackend.isServerRunning()) {
throw new Error('Serveur JACK non démarré');
}
try {
const portName = this.options.outputPorts[0] || 'system:playback_1';
this.playbackProcess = spawn('jack_play', [
'-f', '-', // Lecture depuis stdin
'-b', '16', // 16-bit PCM
portName
]);
this.playbackProcess.on('error', (error) => {
console.error('Erreur processus JACK playback:', error);
this.emit('error', error);
});
this.playbackProcess.stderr.on('data', (data) => {
console.error('JACK playback stderr:', data.toString());
});
this.playbackProcess.on('close', () => {
console.log('Processus JACK playback fermé');
this.isPlaying = false;
});
this.isPlaying = true;
this._startPlaybackLoop();
console.log(`✓ Lecture JACK démarrée : ${this.options.sampleRate}Hz, ${this.options.channels}ch`);
console.log(` Port: ${portName}`);
} catch (error) {
console.error('Erreur démarrage lecture JACK:', error);
throw error;
}
}
/**
* Arrête la lecture audio
*/
stopPlayback() {
if (this.playbackProcess && this.isPlaying) {
this.playbackProcess.kill('SIGTERM');
this.playbackProcess = null;
this.isPlaying = false;
this.playbackBuffer = [];
console.log('✓ Lecture JACK arrêtée');
}
}
/**
* Ajoute des données audio au buffer de lecture
* @param {Buffer} audioData - Données PCM 16-bit
*/
queueAudio(audioData) {
if (!this.isPlaying) {
console.warn('Tentative ajout audio alors que lecture JACK inactive');
return;
}
if (this.playbackBuffer.length < this.maxBufferSize) {
this.playbackBuffer.push(audioData);
} else {
this.emit('bufferOverrun');
}
}
/**
* Boucle de lecture du buffer circulaire
* @private
*/
_startPlaybackLoop() {
const playNextChunk = () => {
if (!this.isPlaying) return;
if (this.playbackBuffer.length > 0) {
const chunk = this.playbackBuffer.shift();
this.playbackProcess.stdin.write(chunk);
} else {
// Buffer vide : underrun (silence)
const silenceBuffer = Buffer.alloc(this.options.framesPerBuffer * 2 * this.options.channels);
this.playbackProcess.stdin.write(silenceBuffer);
this.emit('bufferUnderrun');
}
const intervalMs = (this.options.framesPerBuffer / this.options.sampleRate) * 1000;
setTimeout(playNextChunk, intervalMs);
};
playNextChunk();
}
/**
* Arrête tous les streams
*/
destroy() {
this.stopCapture();
this.stopPlayback();
this.removeAllListeners();
console.log('✓ JACKBackend détruit');
}
/**
* Obtient les statistiques du backend
* @returns {Object}
*/
getStats() {
return {
capturing: this.isCapturing,
playing: this.isPlaying,
playbackBufferSize: this.playbackBuffer.length,
sampleRate: this.options.sampleRate,
channels: this.options.channels,
framesPerBuffer: this.options.framesPerBuffer,
jackServerRunning: JACKBackend.isServerRunning(),
jackSampleRate: JACKBackend._getServerSampleRate(),
jackBufferSize: JACKBackend._getServerBufferSize()
};
}
/**
* Obtient les informations du serveur JACK
* @returns {Object}
*/
static getServerInfo() {
if (!this.isServerRunning()) {
return { running: false };
}
return {
running: true,
sampleRate: this._getServerSampleRate(),
bufferSize: this._getServerBufferSize(),
ports: this.getPorts().length
};
}
}
export default JACKBackend;
+407
View File
@@ -0,0 +1,407 @@
/**
* PipeWireBackend.js
* Backend audio pour Linux moderne utilisant PipeWire
*
* PipeWire est le nouveau standard audio sur Linux (remplace PulseAudio + JACK)
* Compatible avec : Fedora 34+, Ubuntu 22.10+, Arch Linux
*
* Gère :
* - Connexion au serveur PipeWire
* - Capture et lecture audio via pw-cat
* - Détection automatique des devices
* - Mode basse latence (compatible JACK)
*/
import { spawn, execSync } from 'child_process';
import { EventEmitter } from 'events';
export class PipeWireBackend extends EventEmitter {
constructor(options = {}) {
super();
this.options = {
sampleRate: options.sampleRate || 48000,
channels: options.channels || 1,
framesPerBuffer: options.framesPerBuffer || 960,
targetDevice: options.targetDevice || null,
latency: options.latency || 20, // ms
...options
};
this.captureProcess = null;
this.playbackProcess = null;
this.isCapturing = false;
this.isPlaying = false;
this.playbackBuffer = [];
this.maxBufferSize = 10;
}
/**
* Vérifie si PipeWire est installé et disponible
* @returns {boolean}
*/
static isAvailable() {
try {
execSync('which pw-cat', { stdio: 'ignore' });
return true;
} catch (error) {
return false;
}
}
/**
* Vérifie si le serveur PipeWire est en cours d'exécution
* @returns {boolean}
*/
static isServerRunning() {
try {
execSync('pw-cli info 0', { stdio: 'ignore' });
return true;
} catch (error) {
return false;
}
}
/**
* Liste tous les devices audio PipeWire
* @returns {Array} Liste des devices
*/
static getDevices() {
if (!this.isServerRunning()) {
console.warn('Serveur PipeWire non démarré');
return [];
}
try {
// Utilise pactl (compatible PipeWire) pour lister les devices
const sourcesOutput = execSync('pactl list sources short', { encoding: 'utf8' });
const sinksOutput = execSync('pactl list sinks short', { encoding: 'utf8' });
const devices = [];
// Parse sources (entrées)
const sources = sourcesOutput.trim().split('\n').filter(l => l.length > 0);
sources.forEach(line => {
const parts = line.split(/\s+/);
if (parts.length >= 2) {
devices.push({
id: `pw-input-${parts[0]}`,
name: parts[1],
maxInputChannels: 2, // Assume stéréo par défaut
maxOutputChannels: 0,
defaultSampleRate: 48000,
hostAPIName: 'PipeWire',
type: 'source'
});
}
});
// Parse sinks (sorties)
const sinks = sinksOutput.trim().split('\n').filter(l => l.length > 0);
sinks.forEach(line => {
const parts = line.split(/\s+/);
if (parts.length >= 2) {
devices.push({
id: `pw-output-${parts[0]}`,
name: parts[1],
maxInputChannels: 0,
maxOutputChannels: 2,
defaultSampleRate: 48000,
hostAPIName: 'PipeWire',
type: 'sink'
});
}
});
return devices;
} catch (error) {
console.error('Erreur énumération devices PipeWire:', error);
return [];
}
}
/**
* Trouve le device par défaut pour l'entrée
* @returns {Object|null}
*/
static getDefaultInputDevice() {
try {
const output = execSync('pactl get-default-source', { encoding: 'utf8' });
const defaultName = output.trim();
const devices = this.getDevices();
return devices.find(d => d.name === defaultName && d.maxInputChannels > 0) ||
devices.find(d => d.maxInputChannels > 0);
} catch (error) {
const devices = this.getDevices();
return devices.find(d => d.maxInputChannels > 0) || null;
}
}
/**
* Trouve le device par défaut pour la sortie
* @returns {Object|null}
*/
static getDefaultOutputDevice() {
try {
const output = execSync('pactl get-default-sink', { encoding: 'utf8' });
const defaultName = output.trim();
const devices = this.getDevices();
return devices.find(d => d.name === defaultName && d.maxOutputChannels > 0) ||
devices.find(d => d.maxOutputChannels > 0);
} catch (error) {
const devices = this.getDevices();
return devices.find(d => d.maxOutputChannels > 0) || null;
}
}
/**
* Démarre la capture audio
* @returns {Promise<void>}
*/
async startCapture() {
if (this.isCapturing) {
console.warn('Capture PipeWire déjà active');
return;
}
if (!PipeWireBackend.isServerRunning()) {
throw new Error('Serveur PipeWire non démarré');
}
try {
// Utilise pw-cat pour capturer l'audio
const args = [
'--record',
'--format=s16', // 16-bit signed PCM
`--rate=${this.options.sampleRate}`,
`--channels=${this.options.channels}`,
`--latency=${this.options.latency}ms`,
'-' // Sortie vers stdout
];
// Ajoute le device cible si spécifié
if (this.options.targetDevice) {
args.push(`--target=${this.options.targetDevice}`);
}
this.captureProcess = spawn('pw-cat', args);
this.captureProcess.stdout.on('data', (audioData) => {
this.emit('audioData', audioData);
});
this.captureProcess.stderr.on('data', (data) => {
const msg = data.toString();
if (!msg.includes('stream state changed')) {
console.error('PipeWire capture stderr:', msg);
}
});
this.captureProcess.on('error', (error) => {
console.error('Erreur processus PipeWire capture:', error);
this.emit('error', error);
});
this.captureProcess.on('close', (code) => {
console.log(`Processus PipeWire capture fermé (code ${code})`);
this.isCapturing = false;
});
this.isCapturing = true;
console.log(`✓ Capture PipeWire démarrée : ${this.options.sampleRate}Hz, ${this.options.channels}ch`);
console.log(` Latence: ${this.options.latency}ms`);
} catch (error) {
console.error('Erreur démarrage capture PipeWire:', error);
throw error;
}
}
/**
* Arrête la capture audio
*/
stopCapture() {
if (this.captureProcess && this.isCapturing) {
this.captureProcess.kill('SIGTERM');
this.captureProcess = null;
this.isCapturing = false;
console.log('✓ Capture PipeWire arrêtée');
}
}
/**
* Démarre la lecture audio
* @returns {Promise<void>}
*/
async startPlayback() {
if (this.isPlaying) {
console.warn('Lecture PipeWire déjà active');
return;
}
if (!PipeWireBackend.isServerRunning()) {
throw new Error('Serveur PipeWire non démarré');
}
try {
const args = [
'--playback',
'--format=s16',
`--rate=${this.options.sampleRate}`,
`--channels=${this.options.channels}`,
`--latency=${this.options.latency}ms`,
'-' // Lecture depuis stdin
];
if (this.options.targetDevice) {
args.push(`--target=${this.options.targetDevice}`);
}
this.playbackProcess = spawn('pw-cat', args);
this.playbackProcess.stderr.on('data', (data) => {
const msg = data.toString();
if (!msg.includes('stream state changed')) {
console.error('PipeWire playback stderr:', msg);
}
});
this.playbackProcess.on('error', (error) => {
console.error('Erreur processus PipeWire playback:', error);
this.emit('error', error);
});
this.playbackProcess.on('close', (code) => {
console.log(`Processus PipeWire playback fermé (code ${code})`);
this.isPlaying = false;
});
this.isPlaying = true;
this._startPlaybackLoop();
console.log(`✓ Lecture PipeWire démarrée : ${this.options.sampleRate}Hz, ${this.options.channels}ch`);
console.log(` Latence: ${this.options.latency}ms`);
} catch (error) {
console.error('Erreur démarrage lecture PipeWire:', error);
throw error;
}
}
/**
* Arrête la lecture audio
*/
stopPlayback() {
if (this.playbackProcess && this.isPlaying) {
this.playbackProcess.kill('SIGTERM');
this.playbackProcess = null;
this.isPlaying = false;
this.playbackBuffer = [];
console.log('✓ Lecture PipeWire arrêtée');
}
}
/**
* Ajoute des données audio au buffer de lecture
* @param {Buffer} audioData - Données PCM 16-bit
*/
queueAudio(audioData) {
if (!this.isPlaying) {
console.warn('Tentative ajout audio alors que lecture PipeWire inactive');
return;
}
if (this.playbackBuffer.length < this.maxBufferSize) {
this.playbackBuffer.push(audioData);
} else {
this.emit('bufferOverrun');
}
}
/**
* Boucle de lecture du buffer circulaire
* @private
*/
_startPlaybackLoop() {
const playNextChunk = () => {
if (!this.isPlaying || !this.playbackProcess) return;
if (this.playbackBuffer.length > 0) {
const chunk = this.playbackBuffer.shift();
try {
this.playbackProcess.stdin.write(chunk);
} catch (error) {
console.error('Erreur écriture stdin PipeWire:', error);
}
} else {
// Buffer vide : underrun (silence)
const silenceBuffer = Buffer.alloc(this.options.framesPerBuffer * 2 * this.options.channels);
try {
this.playbackProcess.stdin.write(silenceBuffer);
} catch (error) {
// Ignore si le process est fermé
}
this.emit('bufferUnderrun');
}
const intervalMs = (this.options.framesPerBuffer / this.options.sampleRate) * 1000;
setTimeout(playNextChunk, intervalMs);
};
playNextChunk();
}
/**
* Arrête tous les streams
*/
destroy() {
this.stopCapture();
this.stopPlayback();
this.removeAllListeners();
console.log('✓ PipeWireBackend détruit');
}
/**
* Obtient les statistiques du backend
* @returns {Object}
*/
getStats() {
return {
capturing: this.isCapturing,
playing: this.isPlaying,
playbackBufferSize: this.playbackBuffer.length,
sampleRate: this.options.sampleRate,
channels: this.options.channels,
framesPerBuffer: this.options.framesPerBuffer,
latency: this.options.latency,
pipewireServerRunning: PipeWireBackend.isServerRunning()
};
}
/**
* Obtient les informations du serveur PipeWire
* @returns {Object}
*/
static getServerInfo() {
if (!this.isServerRunning()) {
return { running: false };
}
try {
const output = execSync('pw-cli info 0', { encoding: 'utf8' });
// Parse basique des infos
const versionMatch = output.match(/version:\s*"([^"]+)"/);
return {
running: true,
version: versionMatch ? versionMatch[1] : 'unknown',
devices: this.getDevices().length
};
} catch (error) {
return { running: true };
}
}
}
export default PipeWireBackend;