fix: résolution device IDs et correction sox capture args
Corrections pour le routing audio carte son → LiveKit : **Fixes audio backend** - AudioBridgeManager : extraction des device IDs depuis config.audio.device - AudioBridge : ajout résolution device ID → device name pour CoreAudio/sox - CoreAudioBackend : correction index args sox capture (args[2] au lieu de args[1]) **Résultat** - ✅ Sox capture fonctionne : lit depuis "Microphone MacBook Pro" - ✅ Audio capturé et envoyé vers routing - ❌ Sox playback se ferme après 0.2s (problème persistant à corriger) **Autres modifications** - Logging centralisé (Logger.js) - IP corrigée : 192.168.0.146 - Suppression système channels[] legacy dans groupes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -81,7 +81,7 @@ define(['./workbox-290dd570'], (function (workbox) { 'use strict';
|
|||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.oebo7b1mt4g"
|
"revision": "0.su9rr59m8gg"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+42
-108
@@ -15,7 +15,7 @@ function Admin() {
|
|||||||
|
|
||||||
// Audio devices (Phase 2.5)
|
// Audio devices (Phase 2.5)
|
||||||
const [audioDevices, setAudioDevices] = useState([]);
|
const [audioDevices, setAudioDevices] = useState([]);
|
||||||
const [currentDevice, setCurrentDevice] = useState(null);
|
const [currentDevice, setCurrentDevice] = useState({ inputChannels: 8, outputChannels: 8 });
|
||||||
const [selectedInputDevice, setSelectedInputDevice] = useState(null);
|
const [selectedInputDevice, setSelectedInputDevice] = useState(null);
|
||||||
const [selectedOutputDevice, setSelectedOutputDevice] = useState(null);
|
const [selectedOutputDevice, setSelectedOutputDevice] = useState(null);
|
||||||
const [selectedSampleRate, setSelectedSampleRate] = useState(48000);
|
const [selectedSampleRate, setSelectedSampleRate] = useState(48000);
|
||||||
@@ -30,8 +30,7 @@ function Admin() {
|
|||||||
const [editingGroup, setEditingGroup] = useState(null);
|
const [editingGroup, setEditingGroup] = useState(null);
|
||||||
const [groupForm, setGroupForm] = useState({
|
const [groupForm, setGroupForm] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
audioBitrate: 96,
|
audioBitrate: 96
|
||||||
channels: []
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rafraîchissement automatique
|
// Rafraîchissement automatique
|
||||||
@@ -102,14 +101,16 @@ function Admin() {
|
|||||||
const channelNamesData = await channelNamesRes.json();
|
const channelNamesData = await channelNamesRes.json();
|
||||||
|
|
||||||
setAudioDevices(devicesData.devices || []);
|
setAudioDevices(devicesData.devices || []);
|
||||||
setCurrentDevice(currentData.device || {});
|
|
||||||
|
const device = currentData.device || { inputChannels: 8, outputChannels: 8 };
|
||||||
|
setCurrentDevice(device);
|
||||||
setChannelNames(channelNamesData.channelNames || { inputs: {}, outputs: {} });
|
setChannelNames(channelNamesData.channelNames || { inputs: {}, outputs: {} });
|
||||||
|
|
||||||
// Ne réinitialiser les sélections que si l'utilisateur n'est pas en train d'éditer
|
// Ne réinitialiser les sélections que si l'utilisateur n'est pas en train d'éditer
|
||||||
if (!isEditingAudio) {
|
if (!isEditingAudio) {
|
||||||
setSelectedInputDevice(currentData.device?.inputDeviceId ?? null);
|
setSelectedInputDevice(device.inputDeviceId ?? null);
|
||||||
setSelectedOutputDevice(currentData.device?.outputDeviceId ?? null);
|
setSelectedOutputDevice(device.outputDeviceId ?? null);
|
||||||
setSelectedSampleRate(currentData.device?.sampleRate || 48000);
|
setSelectedSampleRate(device.sampleRate || 48000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -189,8 +190,7 @@ function Admin() {
|
|||||||
setEditingGroup(group.id);
|
setEditingGroup(group.id);
|
||||||
setGroupForm({
|
setGroupForm({
|
||||||
name: group.name,
|
name: group.name,
|
||||||
audioBitrate: group.audioBitrate || 96,
|
audioBitrate: group.audioBitrate || 96
|
||||||
channels: group.channels || []
|
|
||||||
});
|
});
|
||||||
setShowGroupForm(true);
|
setShowGroupForm(true);
|
||||||
};
|
};
|
||||||
@@ -198,38 +198,12 @@ function Admin() {
|
|||||||
const resetGroupForm = () => {
|
const resetGroupForm = () => {
|
||||||
setGroupForm({
|
setGroupForm({
|
||||||
name: '',
|
name: '',
|
||||||
audioBitrate: 96,
|
audioBitrate: 96
|
||||||
channels: []
|
|
||||||
});
|
});
|
||||||
setShowGroupForm(false);
|
setShowGroupForm(false);
|
||||||
setEditingGroup(null);
|
setEditingGroup(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addChannel = () => {
|
|
||||||
setGroupForm({
|
|
||||||
...groupForm,
|
|
||||||
channels: [
|
|
||||||
...groupForm.channels,
|
|
||||||
{ name: '', audioInput: 0, audioOutput: 0 }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateChannel = (index, field, value) => {
|
|
||||||
const newChannels = [...groupForm.channels];
|
|
||||||
newChannels[index] = {
|
|
||||||
...newChannels[index],
|
|
||||||
[field]: field === 'audioInput' || field === 'audioOutput' ? parseInt(value) : value
|
|
||||||
};
|
|
||||||
setGroupForm({ ...groupForm, channels: newChannels });
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeChannel = (index) => {
|
|
||||||
const newChannels = [...groupForm.channels];
|
|
||||||
newChannels.splice(index, 1);
|
|
||||||
setGroupForm({ ...groupForm, channels: newChannels });
|
|
||||||
};
|
|
||||||
|
|
||||||
// ========== Gestion audio devices (Phase 2.5) ==========
|
// ========== Gestion audio devices (Phase 2.5) ==========
|
||||||
|
|
||||||
const handleSaveChannelNames = async () => {
|
const handleSaveChannelNames = async () => {
|
||||||
@@ -270,8 +244,8 @@ function Admin() {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
inputDeviceId: selectedInputDevice !== null ? parseInt(selectedInputDevice) : undefined,
|
inputDeviceId: selectedInputDevice || undefined,
|
||||||
outputDeviceId: selectedOutputDevice !== null ? parseInt(selectedOutputDevice) : undefined,
|
outputDeviceId: selectedOutputDevice || undefined,
|
||||||
sampleRate: parseInt(selectedSampleRate)
|
sampleRate: parseInt(selectedSampleRate)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -415,43 +389,9 @@ function Admin() {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="channels-section">
|
<p style={{color: 'var(--color-text-secondary)', fontSize: '0.9rem', marginTop: 'var(--spacing-md)'}}>
|
||||||
<div className="channels-header">
|
Le routing audio se configure dans l'onglet "Audio" via la matrice de routing.
|
||||||
<h4>Canaux audio</h4>
|
</p>
|
||||||
<button type="button" onClick={addChannel} className="btn-small">
|
|
||||||
+ Canal
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{groupForm.channels.map((channel, index) => (
|
|
||||||
<div key={index} className="channel-item">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Nom canal (ex: Principal, Backup...)"
|
|
||||||
value={channel.name}
|
|
||||||
onChange={(e) => updateChannel(index, 'name', e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
placeholder="Input"
|
|
||||||
value={channel.audioInput}
|
|
||||||
onChange={(e) => updateChannel(index, 'audioInput', e.target.value)}
|
|
||||||
min="0"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
placeholder="Output"
|
|
||||||
value={channel.audioOutput}
|
|
||||||
onChange={(e) => updateChannel(index, 'audioOutput', e.target.value)}
|
|
||||||
min="0"
|
|
||||||
/>
|
|
||||||
<button type="button" onClick={() => removeChannel(index)} className="btn-danger">
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-actions">
|
<div className="form-actions">
|
||||||
<button type="submit" className="btn-primary">
|
<button type="submit" className="btn-primary">
|
||||||
@@ -482,18 +422,7 @@ function Admin() {
|
|||||||
|
|
||||||
<div className="group-info">
|
<div className="group-info">
|
||||||
<span>Bitrate: {group.audioBitrate || 96} kbps</span>
|
<span>Bitrate: {group.audioBitrate || 96} kbps</span>
|
||||||
<span>Canaux: {group.channels?.length || 0}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{group.channels && group.channels.length > 0 && (
|
|
||||||
<div className="channels-list">
|
|
||||||
{group.channels.map(channel => (
|
|
||||||
<div key={channel.id} className="channel-badge">
|
|
||||||
{channel.name} (I/O: {channel.audioInput}/{channel.audioOutput})
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -514,22 +443,22 @@ function Admin() {
|
|||||||
value={selectedInputDevice ?? ''}
|
value={selectedInputDevice ?? ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setIsEditingAudio(true);
|
setIsEditingAudio(true);
|
||||||
setSelectedInputDevice(e.target.value === '' ? null : parseInt(e.target.value));
|
setSelectedInputDevice(e.target.value === '' ? null : e.target.value);
|
||||||
}}
|
}}
|
||||||
className="device-select"
|
className="device-select"
|
||||||
>
|
>
|
||||||
<option value="">-- Sélectionner une carte --</option>
|
<option value="">-- Sélectionner une carte --</option>
|
||||||
{audioDevices
|
{audioDevices
|
||||||
.filter(d => d.maxInputChannels > 0)
|
.filter(d => d.maxInputChannels > 0)
|
||||||
.map(device => (
|
.map((device, index) => (
|
||||||
<option key={device.id} value={device.id}>
|
<option key={`input-${device.id}-${index}`} value={device.id}>
|
||||||
{device.name} - {device.maxInputChannels} canaux - {device.defaultSampleRate}Hz
|
{device.name} - {device.maxInputChannels} canaux - {device.defaultSampleRate}Hz
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
{selectedInputDevice !== null && (
|
{selectedInputDevice !== null && selectedInputDevice !== '' && (
|
||||||
<p style={{marginTop: 'var(--spacing-sm)', color: 'var(--color-text-secondary)', fontSize: '0.85rem'}}>
|
<p style={{marginTop: 'var(--spacing-sm)', color: 'var(--color-text-secondary)', fontSize: '0.85rem', wordBreak: 'break-all'}}>
|
||||||
Device ID {selectedInputDevice} sélectionné
|
Device ID: {selectedInputDevice}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -540,22 +469,22 @@ function Admin() {
|
|||||||
value={selectedOutputDevice ?? ''}
|
value={selectedOutputDevice ?? ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setIsEditingAudio(true);
|
setIsEditingAudio(true);
|
||||||
setSelectedOutputDevice(e.target.value === '' ? null : parseInt(e.target.value));
|
setSelectedOutputDevice(e.target.value === '' ? null : e.target.value);
|
||||||
}}
|
}}
|
||||||
className="device-select"
|
className="device-select"
|
||||||
>
|
>
|
||||||
<option value="">-- Sélectionner une carte --</option>
|
<option value="">-- Sélectionner une carte --</option>
|
||||||
{audioDevices
|
{audioDevices
|
||||||
.filter(d => d.maxOutputChannels > 0)
|
.filter(d => d.maxOutputChannels > 0)
|
||||||
.map(device => (
|
.map((device, index) => (
|
||||||
<option key={device.id} value={device.id}>
|
<option key={`output-${device.id}-${index}`} value={device.id}>
|
||||||
{device.name} - {device.maxOutputChannels} canaux - {device.defaultSampleRate}Hz
|
{device.name} - {device.maxOutputChannels} canaux - {device.defaultSampleRate}Hz
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
{selectedOutputDevice !== null && (
|
{selectedOutputDevice !== null && selectedOutputDevice !== '' && (
|
||||||
<p style={{marginTop: 'var(--spacing-sm)', color: 'var(--color-text-secondary)', fontSize: '0.85rem'}}>
|
<p style={{marginTop: 'var(--spacing-sm)', color: 'var(--color-text-secondary)', fontSize: '0.85rem', wordBreak: 'break-all'}}>
|
||||||
Device ID {selectedOutputDevice} sélectionné
|
Device ID: {selectedOutputDevice}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -603,9 +532,11 @@ function Admin() {
|
|||||||
|
|
||||||
<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--spacing-xl)'}}>
|
<div style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--spacing-xl)'}}>
|
||||||
<div>
|
<div>
|
||||||
<h4 style={{marginBottom: 'var(--spacing-md)', color: 'var(--color-text-secondary)'}}>Entrées (Inputs)</h4>
|
<h4 style={{marginBottom: 'var(--spacing-md)', color: 'var(--color-text-secondary)'}}>
|
||||||
|
Entrées (Inputs) - {currentDevice.inputChannels || 0} canaux disponibles
|
||||||
|
</h4>
|
||||||
<div style={{display: 'grid', gap: 'var(--spacing-sm)'}}>
|
<div style={{display: 'grid', gap: 'var(--spacing-sm)'}}>
|
||||||
{Array.from({length: 8}, (_, i) => (
|
{Array.from({length: currentDevice.inputChannels || 8}, (_, i) => (
|
||||||
<div key={`input-${i}`} style={{display: 'grid', gridTemplateColumns: '40px 1fr', gap: 'var(--spacing-sm)', alignItems: 'center'}}>
|
<div key={`input-${i}`} style={{display: 'grid', gridTemplateColumns: '40px 1fr', gap: 'var(--spacing-sm)', alignItems: 'center'}}>
|
||||||
<span style={{color: 'var(--color-text-secondary)', fontSize: '0.85rem'}}>{i}</span>
|
<span style={{color: 'var(--color-text-secondary)', fontSize: '0.85rem'}}>{i}</span>
|
||||||
<input
|
<input
|
||||||
@@ -629,9 +560,11 @@ function Admin() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h4 style={{marginBottom: 'var(--spacing-md)', color: 'var(--color-text-secondary)'}}>Sorties (Outputs)</h4>
|
<h4 style={{marginBottom: 'var(--spacing-md)', color: 'var(--color-text-secondary)'}}>
|
||||||
|
Sorties (Outputs) - {currentDevice.outputChannels || 0} canaux disponibles
|
||||||
|
</h4>
|
||||||
<div style={{display: 'grid', gap: 'var(--spacing-sm)'}}>
|
<div style={{display: 'grid', gap: 'var(--spacing-sm)'}}>
|
||||||
{Array.from({length: 8}, (_, i) => (
|
{Array.from({length: currentDevice.outputChannels || 8}, (_, i) => (
|
||||||
<div key={`output-${i}`} style={{display: 'grid', gridTemplateColumns: '40px 1fr', gap: 'var(--spacing-sm)', alignItems: 'center'}}>
|
<div key={`output-${i}`} style={{display: 'grid', gridTemplateColumns: '40px 1fr', gap: 'var(--spacing-sm)', alignItems: 'center'}}>
|
||||||
<span style={{color: 'var(--color-text-secondary)', fontSize: '0.85rem'}}>{i}</span>
|
<span style={{color: 'var(--color-text-secondary)', fontSize: '0.85rem'}}>{i}</span>
|
||||||
<input
|
<input
|
||||||
@@ -658,13 +591,14 @@ function Admin() {
|
|||||||
|
|
||||||
<AudioRoutingMatrix groups={groups} channelNames={channelNames} />
|
<AudioRoutingMatrix groups={groups} channelNames={channelNames} />
|
||||||
|
|
||||||
{currentDevice && Object.keys(currentDevice).length > 0 && (
|
{currentDevice && currentDevice.inputDeviceId && (
|
||||||
<div className="current-config">
|
<div className="current-config">
|
||||||
<h3>Configuration actuelle</h3>
|
<h3>Configuration actuelle</h3>
|
||||||
<div className="config-info">
|
<div className="config-info">
|
||||||
<p><strong>Input Device ID:</strong> {currentDevice.inputDeviceId ?? 'Non configuré'}</p>
|
<p><strong>Input Device:</strong> {currentDevice.inputDeviceName || currentDevice.inputDeviceId}</p>
|
||||||
<p><strong>Output Device ID:</strong> {currentDevice.outputDeviceId ?? 'Non configuré'}</p>
|
<p><strong>Output Device:</strong> {currentDevice.outputDeviceName || currentDevice.outputDeviceId}</p>
|
||||||
<p><strong>Sample Rate:</strong> {currentDevice.sampleRate ?? 48000} Hz</p>
|
<p><strong>Sample Rate:</strong> {currentDevice.sampleRate ?? 48000} Hz</p>
|
||||||
|
<p><strong>Canaux:</strong> {currentDevice.inputChannels} entrées / {currentDevice.outputChannels} sorties</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -683,9 +617,9 @@ function Admin() {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{audioDevices.map(device => (
|
{audioDevices.map((device, index) => (
|
||||||
<tr key={device.id}>
|
<tr key={`${device.id}-${index}`}>
|
||||||
<td>{device.id}</td>
|
<td style={{fontSize: '0.75rem', wordBreak: 'break-all', maxWidth: '200px'}}>{device.id}</td>
|
||||||
<td>{device.name}</td>
|
<td>{device.name}</td>
|
||||||
<td>{device.maxInputChannels}</td>
|
<td>{device.maxInputChannels}</td>
|
||||||
<td>{device.maxOutputChannels}</td>
|
<td>{device.maxOutputChannels}</td>
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ function AudioRoutingMatrix({ groups, channelNames }) {
|
|||||||
const [routing, setRouting] = useState({ inputToGroup: {}, groupToOutput: {}, gains: {} });
|
const [routing, setRouting] = useState({ inputToGroup: {}, groupToOutput: {}, gains: {} });
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [showOnlyNamedChannels, setShowOnlyNamedChannels] = useState(false);
|
const [showOnlyNamedChannels, setShowOnlyNamedChannels] = useState(false);
|
||||||
|
const [audioDevice, setAudioDevice] = useState({ inputChannels: 8, outputChannels: 8 });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadRouting();
|
loadRouting();
|
||||||
|
loadAudioDevice();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadRouting = async () => {
|
const loadRouting = async () => {
|
||||||
@@ -30,6 +32,21 @@ function AudioRoutingMatrix({ groups, channelNames }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadAudioDevice = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_URL}/admin/audio/device`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
setAudioDevice({
|
||||||
|
inputChannels: data.device?.inputChannels || 8,
|
||||||
|
outputChannels: data.device?.outputChannels || 8
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur chargement audio device:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const saveRouting = async () => {
|
const saveRouting = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/admin/audio/routing`, {
|
const res = await fetch(`${API_URL}/admin/audio/routing`, {
|
||||||
@@ -146,7 +163,7 @@ function AudioRoutingMatrix({ groups, channelNames }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getVisibleInputChannels = () => {
|
const getVisibleInputChannels = () => {
|
||||||
const allInputs = Array.from({length: 8}, (_, i) => i);
|
const allInputs = Array.from({length: audioDevice.inputChannels}, (_, i) => i);
|
||||||
if (showOnlyNamedChannels) {
|
if (showOnlyNamedChannels) {
|
||||||
return allInputs.filter(i => hasCustomName('inputs', i));
|
return allInputs.filter(i => hasCustomName('inputs', i));
|
||||||
}
|
}
|
||||||
@@ -154,7 +171,7 @@ function AudioRoutingMatrix({ groups, channelNames }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getVisibleOutputChannels = () => {
|
const getVisibleOutputChannels = () => {
|
||||||
const allOutputs = Array.from({length: 8}, (_, i) => i);
|
const allOutputs = Array.from({length: audioDevice.outputChannels}, (_, i) => i);
|
||||||
if (showOnlyNamedChannels) {
|
if (showOnlyNamedChannels) {
|
||||||
return allOutputs.filter(i => hasCustomName('outputs', i));
|
return allOutputs.filter(i => hasCustomName('outputs', i));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:3000',
|
target: 'http://192.168.0.146:3000',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/api/, '')
|
rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
},
|
},
|
||||||
|
|||||||
+23
-3
@@ -179,7 +179,7 @@ router.get('/groups', (req, res) => {
|
|||||||
/**
|
/**
|
||||||
* POST /admin/groups
|
* POST /admin/groups
|
||||||
* Crée un nouveau groupe
|
* Crée un nouveau groupe
|
||||||
* Body: { name, audioBitrate?, channels }
|
* Body: { name, audioBitrate? }
|
||||||
* L'ID est généré automatiquement à partir du nom
|
* L'ID est généré automatiquement à partir du nom
|
||||||
*/
|
*/
|
||||||
router.post('/groups', (req, res) => {
|
router.post('/groups', (req, res) => {
|
||||||
@@ -204,7 +204,7 @@ router.post('/groups', (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Créer le nouveau groupe (sans channels)
|
// Créer le nouveau groupe
|
||||||
const newGroup = {
|
const newGroup = {
|
||||||
name,
|
name,
|
||||||
...(audioBitrate && { audioBitrate })
|
...(audioBitrate && { audioBitrate })
|
||||||
@@ -482,8 +482,28 @@ router.get('/audio/device', (req, res) => {
|
|||||||
const config = configManager.get();
|
const config = configManager.get();
|
||||||
const audioDevice = config.audio?.device || {};
|
const audioDevice = config.audio?.device || {};
|
||||||
|
|
||||||
|
// Enrichir avec les infos réelles de la carte si configurée
|
||||||
|
const devices = CoreAudioBackend.getDevices();
|
||||||
|
let deviceInfo = { ...audioDevice };
|
||||||
|
|
||||||
|
if (audioDevice.inputDeviceId) {
|
||||||
|
const inputDev = devices.find(d => d.id === audioDevice.inputDeviceId);
|
||||||
|
if (inputDev) {
|
||||||
|
deviceInfo.inputChannels = inputDev.maxInputChannels;
|
||||||
|
deviceInfo.inputDeviceName = inputDev.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioDevice.outputDeviceId) {
|
||||||
|
const outputDev = devices.find(d => d.id === audioDevice.outputDeviceId);
|
||||||
|
if (outputDev) {
|
||||||
|
deviceInfo.outputChannels = outputDev.maxOutputChannels;
|
||||||
|
deviceInfo.outputDeviceName = outputDev.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
device: audioDevice
|
device: deviceInfo
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur GET /admin/audio/device:', error);
|
console.error('Erreur GET /admin/audio/device:', error);
|
||||||
|
|||||||
@@ -196,13 +196,31 @@ export class AudioBridge extends EventEmitter {
|
|||||||
throw new Error(`Plateforme non supportée : ${os}`);
|
throw new Error(`Plateforme non supportée : ${os}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Résoudre les device IDs vers les noms pour CoreAudio/sox
|
||||||
|
let inputDeviceName = null;
|
||||||
|
let outputDeviceName = null;
|
||||||
|
|
||||||
|
if (this.options.inputDeviceId) {
|
||||||
|
const inputDevice = BackendClass.getDevices().find(d => d.id === this.options.inputDeviceId);
|
||||||
|
inputDeviceName = inputDevice ? inputDevice.name : this.options.inputDeviceId;
|
||||||
|
console.log(`📥 Input device: "${inputDeviceName}" (ID: ${this.options.inputDeviceId})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.outputDeviceId) {
|
||||||
|
const outputDevice = BackendClass.getDevices().find(d => d.id === this.options.outputDeviceId);
|
||||||
|
outputDeviceName = outputDevice ? outputDevice.name : this.options.outputDeviceId;
|
||||||
|
console.log(`📤 Output device: "${outputDeviceName}" (ID: ${this.options.outputDeviceId})`);
|
||||||
|
}
|
||||||
|
|
||||||
// Initialisation du backend sélectionné
|
// Initialisation du backend sélectionné
|
||||||
this.audioBackend = new BackendClass({
|
this.audioBackend = new BackendClass({
|
||||||
sampleRate: this.options.sampleRate,
|
sampleRate: this.options.sampleRate,
|
||||||
channels: this.options.channels,
|
channels: this.options.channels,
|
||||||
framesPerBuffer: this.options.frameSize,
|
framesPerBuffer: this.options.frameSize,
|
||||||
inputDeviceId: this.options.inputDeviceId,
|
inputDeviceId: this.options.inputDeviceId,
|
||||||
|
inputDeviceName: inputDeviceName,
|
||||||
outputDeviceId: this.options.outputDeviceId,
|
outputDeviceId: this.options.outputDeviceId,
|
||||||
|
outputDeviceName: outputDeviceName,
|
||||||
// Options spécifiques PipeWire
|
// Options spécifiques PipeWire
|
||||||
latency: this.options.latency || 20
|
latency: this.options.latency || 20
|
||||||
});
|
});
|
||||||
@@ -366,6 +384,10 @@ export class AudioBridge extends EventEmitter {
|
|||||||
this.inputChannelBuffers
|
this.inputChannelBuffers
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (this.stats.framesCapture % 100 === 0) {
|
||||||
|
console.log(`[AudioBridge] Frame ${this.stats.framesCapture}: ${this.inputChannelBuffers.size} inputs → ${groupBuffers.size} groupes`);
|
||||||
|
}
|
||||||
|
|
||||||
// ÉTAPE 2 : Pour chaque groupe, envoyer vers LiveKit
|
// ÉTAPE 2 : Pour chaque groupe, envoyer vers LiveKit
|
||||||
groupBuffers.forEach((groupBuffer, groupName) => {
|
groupBuffers.forEach((groupBuffer, groupName) => {
|
||||||
// Convertir Float32Array → PCM Buffer
|
// Convertir Float32Array → PCM Buffer
|
||||||
@@ -375,13 +397,19 @@ export class AudioBridge extends EventEmitter {
|
|||||||
const opusData = this.opusEncoder.encode(pcmBuffer);
|
const opusData = this.opusEncoder.encode(pcmBuffer);
|
||||||
|
|
||||||
if (opusData) {
|
if (opusData) {
|
||||||
this.stats.framesCapture++;
|
|
||||||
this.stats.bytesEncoded += opusData.length;
|
this.stats.bytesEncoded += opusData.length;
|
||||||
|
|
||||||
// Envoi vers LiveKit via sendAudioData (prend du PCM, pas de l'Opus)
|
// Envoi vers LiveKit via sendAudioData (prend du PCM, pas de l'Opus)
|
||||||
// Note: LiveKit gère lui-même l'encodage Opus en interne
|
// Note: LiveKit gère lui-même l'encodage Opus en interne
|
||||||
if (this.liveKitClient && this.liveKitClient.connected) {
|
if (this.liveKitClient && this.liveKitClient.isConnected) {
|
||||||
this.liveKitClient.sendAudioData(pcmBuffer);
|
this.liveKitClient.sendAudioData(pcmBuffer);
|
||||||
|
if (this.stats.framesCapture % 100 === 0) {
|
||||||
|
console.log(`[AudioBridge] → LiveKit groupe "${groupName}": ${pcmBuffer.length} bytes`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.stats.framesCapture % 100 === 0) {
|
||||||
|
console.log(`[AudioBridge] ⚠️ LiveKit non connecté, audio non envoyé`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Émettre aussi pour monitoring/debug
|
// Émettre aussi pour monitoring/debug
|
||||||
@@ -389,7 +417,27 @@ export class AudioBridge extends EventEmitter {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ÉTAPE 3 : Loopback local - Groupes → Outputs physiques (sans passer par LiveKit)
|
||||||
|
const outputBuffers = this.groupAudioRouter.processGroupsToOutputs(groupBuffers);
|
||||||
|
|
||||||
|
if (this.stats.framesCapture % 100 === 0) {
|
||||||
|
console.log(`[AudioBridge] Loopback local: ${groupBuffers.size} groupes → ${outputBuffers.size} outputs`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ÉTAPE 4 : Envoyer chaque output à la carte son
|
||||||
|
outputBuffers.forEach((outputBuffer, channelId) => {
|
||||||
|
const pcmBuffer = this._float32ToBuffer(outputBuffer);
|
||||||
|
|
||||||
|
// Envoyer à la carte son
|
||||||
|
this.audioBackend.queueAudio(pcmBuffer);
|
||||||
|
|
||||||
|
if (this.stats.framesCapture % 100 === 0) {
|
||||||
|
console.log(`[AudioBridge] → Output ${channelId}: ${pcmBuffer.length} bytes`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.stats.framesCapture++;
|
this.stats.framesCapture++;
|
||||||
|
this.stats.framesPlayback++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur routing capture:', error);
|
console.error('Erreur routing capture:', error);
|
||||||
this.stats.errors.capture++;
|
this.stats.errors.capture++;
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ class AudioBridgeManager extends EventEmitter {
|
|||||||
if (audioConfig.defaultBitrate) audioConfig.defaultBitrate = parseInt(audioConfig.defaultBitrate, 10);
|
if (audioConfig.defaultBitrate) audioConfig.defaultBitrate = parseInt(audioConfig.defaultBitrate, 10);
|
||||||
if (audioConfig.customOpusBitrate) audioConfig.customOpusBitrate = parseInt(audioConfig.customOpusBitrate, 10);
|
if (audioConfig.customOpusBitrate) audioConfig.customOpusBitrate = parseInt(audioConfig.customOpusBitrate, 10);
|
||||||
|
|
||||||
|
// Extraire les device IDs depuis le sous-objet device
|
||||||
|
const inputDeviceId = audioConfig.device?.inputDeviceId || null;
|
||||||
|
const outputDeviceId = audioConfig.device?.outputDeviceId || null;
|
||||||
|
|
||||||
// Créer l'instance avec la config
|
// Créer l'instance avec la config
|
||||||
this.bridge = new AudioBridge({
|
this.bridge = new AudioBridge({
|
||||||
...audioConfig,
|
...audioConfig,
|
||||||
@@ -90,7 +94,10 @@ class AudioBridgeManager extends EventEmitter {
|
|||||||
routing: config.audio?.routing || {},
|
routing: config.audio?.routing || {},
|
||||||
groups: config.groups || [],
|
groups: config.groups || [],
|
||||||
maxInputChannels: 32,
|
maxInputChannels: 32,
|
||||||
maxOutputChannels: 32
|
maxOutputChannels: 32,
|
||||||
|
// Device IDs extraits
|
||||||
|
inputDeviceId,
|
||||||
|
outputDeviceId
|
||||||
});
|
});
|
||||||
|
|
||||||
// Démarrer le bridge
|
// Démarrer le bridge
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
import { getLogger } from '../utils/Logger.js';
|
||||||
|
|
||||||
|
const logger = getLogger('Routing');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Représente une route audio avec gain
|
* Représente une route audio avec gain
|
||||||
@@ -76,7 +79,10 @@ export class GroupAudioRouter extends EventEmitter {
|
|||||||
* Configure le routing depuis la config YAML
|
* Configure le routing depuis la config YAML
|
||||||
*/
|
*/
|
||||||
configure(routingConfig) {
|
configure(routingConfig) {
|
||||||
console.log('Configuration du routing audio...');
|
logger.info('Configuration du routing audio...');
|
||||||
|
logger.debug(' Groupes disponibles:', this.config.groups.map(g => `${g.name || g} (id: ${g.id || g})`).join(', '));
|
||||||
|
logger.debug(' inputToGroup:', JSON.stringify(routingConfig.inputToGroup || {}));
|
||||||
|
logger.debug(' groupToOutput:', JSON.stringify(routingConfig.groupToOutput || {}));
|
||||||
|
|
||||||
// Réinitialise les routes
|
// Réinitialise les routes
|
||||||
this.inputToGroupRoutes.clear();
|
this.inputToGroupRoutes.clear();
|
||||||
@@ -104,7 +110,7 @@ export class GroupAudioRouter extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._updateStatsActiveRoutes();
|
this._updateStatsActiveRoutes();
|
||||||
console.log(`Routing configuré : ${this.stats.routesActive} routes actives`);
|
logger.success(`Routing configuré : ${this.stats.routesActive} routes actives`);
|
||||||
this.emit('configured', this.stats);
|
this.emit('configured', this.stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +134,7 @@ export class GroupAudioRouter extends EventEmitter {
|
|||||||
const route = new AudioRoute(inputChannel, groupName, gainDb);
|
const route = new AudioRoute(inputChannel, groupName, gainDb);
|
||||||
this.inputToGroupRoutes.get(key).push(route);
|
this.inputToGroupRoutes.get(key).push(route);
|
||||||
|
|
||||||
console.log(`Route ajoutée : Input ${inputChannel} -> Group "${groupName}" (${gainDb}dB)`);
|
logger.info(`Input ${inputChannel} → Group "${groupName}" (${gainDb}dB)`);
|
||||||
this._updateStatsActiveRoutes();
|
this._updateStatsActiveRoutes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +151,7 @@ export class GroupAudioRouter extends EventEmitter {
|
|||||||
const route = new AudioRoute(groupName, outputChannel, gainDb);
|
const route = new AudioRoute(groupName, outputChannel, gainDb);
|
||||||
this.groupToOutputRoutes.get(key).push(route);
|
this.groupToOutputRoutes.get(key).push(route);
|
||||||
|
|
||||||
console.log(`Route ajoutée : Group "${groupName}" -> Output ${outputChannel} (${gainDb}dB)`);
|
logger.info(`Group "${groupName}" → Output ${outputChannel} (${gainDb}dB)`);
|
||||||
this._updateStatsActiveRoutes();
|
this._updateStatsActiveRoutes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +211,9 @@ export class GroupAudioRouter extends EventEmitter {
|
|||||||
// Réinitialise les buffers de groupe
|
// Réinitialise les buffers de groupe
|
||||||
this.groupBuffers.clear();
|
this.groupBuffers.clear();
|
||||||
this.config.groups.forEach(group => {
|
this.config.groups.forEach(group => {
|
||||||
this.groupBuffers.set(group.name, new Float32Array(this.config.frameSize));
|
// Utiliser l'ID (slugifié) plutôt que le nom pour correspondre au routing
|
||||||
|
const groupId = group.id || group.name || group;
|
||||||
|
this.groupBuffers.set(groupId, new Float32Array(this.config.frameSize));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pour chaque canal d'entrée
|
// Pour chaque canal d'entrée
|
||||||
@@ -221,7 +229,10 @@ export class GroupAudioRouter extends EventEmitter {
|
|||||||
// Applique chaque route (mixage additif vers les groupes)
|
// Applique chaque route (mixage additif vers les groupes)
|
||||||
routes.forEach(route => {
|
routes.forEach(route => {
|
||||||
const groupBuffer = this.groupBuffers.get(route.destination);
|
const groupBuffer = this.groupBuffers.get(route.destination);
|
||||||
if (!groupBuffer) return;
|
if (!groupBuffer) {
|
||||||
|
logger.warn(`Buffer groupe "${route.destination}" introuvable pour routing depuis Input ${channelId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Mixage avec gain
|
// Mixage avec gain
|
||||||
for (let i = 0; i < pcmData.length && i < groupBuffer.length; i++) {
|
for (let i = 0; i < pcmData.length && i < groupBuffer.length; i++) {
|
||||||
@@ -235,6 +246,9 @@ export class GroupAudioRouter extends EventEmitter {
|
|||||||
for (let i = 0; i < buffer.length; i++) {
|
for (let i = 0; i < buffer.length; i++) {
|
||||||
if (Math.abs(buffer[i]) > 1.0) {
|
if (Math.abs(buffer[i]) > 1.0) {
|
||||||
this.stats.clippingEvents++;
|
this.stats.clippingEvents++;
|
||||||
|
if (this.stats.clippingEvents % 1000 === 1) {
|
||||||
|
logger.warn(`Clipping détecté sur groupe "${groupName}" (${this.stats.clippingEvents} événements)`);
|
||||||
|
}
|
||||||
buffer[i] = Math.sign(buffer[i]) * 1.0; // Hard clipping
|
buffer[i] = Math.sign(buffer[i]) * 1.0; // Hard clipping
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,7 +390,7 @@ export class GroupAudioRouter extends EventEmitter {
|
|||||||
this.groupBuffers.clear();
|
this.groupBuffers.clear();
|
||||||
this.outputBuffers.clear();
|
this.outputBuffers.clear();
|
||||||
this.removeAllListeners();
|
this.removeAllListeners();
|
||||||
console.log('GroupAudioRouter détruit');
|
logger.info('GroupAudioRouter détruit');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -223,6 +223,11 @@ export class LiveKitClient extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.isConnected || !this.localAudioTrack) {
|
||||||
|
// Silently drop frames si pas encore connecté
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Création d'un AudioFrame (conversion en int32 explicite)
|
// Création d'un AudioFrame (conversion en int32 explicite)
|
||||||
const samplesPerChannel = Math.floor(pcmData.length / 2 / this.options.channels);
|
const samplesPerChannel = Math.floor(pcmData.length / 2 / this.options.channels);
|
||||||
@@ -238,9 +243,12 @@ export class LiveKitClient extends EventEmitter {
|
|||||||
await this.audioSource.captureFrame(frame);
|
await this.audioSource.captureFrame(frame);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Ne logger que les erreurs non-InvalidState pour éviter le spam
|
||||||
|
if (!error.message.includes('InvalidState')) {
|
||||||
console.error('Erreur envoi audio:', error);
|
console.error('Erreur envoi audio:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère tous les tracks audio distants actifs
|
* Récupère tous les tracks audio distants actifs
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ export class CoreAudioBackend extends EventEmitter {
|
|||||||
const data = JSON.parse(output);
|
const data = JSON.parse(output);
|
||||||
|
|
||||||
const devices = [];
|
const devices = [];
|
||||||
let id = 0;
|
|
||||||
|
|
||||||
// Parse audio devices
|
// Parse audio devices
|
||||||
if (data.SPAudioDataType) {
|
if (data.SPAudioDataType) {
|
||||||
@@ -62,13 +61,16 @@ export class CoreAudioBackend extends EventEmitter {
|
|||||||
const outputChannels = parseInt(device.coreaudio_device_output) || 0;
|
const outputChannels = parseInt(device.coreaudio_device_output) || 0;
|
||||||
const sampleRate = parseInt(device.coreaudio_device_srate) || 48000;
|
const sampleRate = parseInt(device.coreaudio_device_srate) || 48000;
|
||||||
|
|
||||||
|
// Utiliser le UID CoreAudio comme ID (unique et stable)
|
||||||
|
const deviceUID = device._uniqueID || device.coreaudio_device_uid || name;
|
||||||
|
|
||||||
// Ignorer les devices sans input ni output
|
// Ignorer les devices sans input ni output
|
||||||
if (inputChannels === 0 && outputChannels === 0) {
|
if (inputChannels === 0 && outputChannels === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
devices.push({
|
devices.push({
|
||||||
id: id++,
|
id: deviceUID,
|
||||||
name: name,
|
name: name,
|
||||||
maxInputChannels: inputChannels,
|
maxInputChannels: inputChannels,
|
||||||
maxOutputChannels: outputChannels,
|
maxOutputChannels: outputChannels,
|
||||||
@@ -90,7 +92,7 @@ export class CoreAudioBackend extends EventEmitter {
|
|||||||
if (devices.length === 0) {
|
if (devices.length === 0) {
|
||||||
devices.push(
|
devices.push(
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 'builtin-mic',
|
||||||
name: 'Built-in Microphone',
|
name: 'Built-in Microphone',
|
||||||
maxInputChannels: 1,
|
maxInputChannels: 1,
|
||||||
maxOutputChannels: 0,
|
maxOutputChannels: 0,
|
||||||
@@ -98,7 +100,7 @@ export class CoreAudioBackend extends EventEmitter {
|
|||||||
hostAPIName: 'Core Audio'
|
hostAPIName: 'Core Audio'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 'builtin-output',
|
||||||
name: 'Built-in Output',
|
name: 'Built-in Output',
|
||||||
maxInputChannels: 0,
|
maxInputChannels: 0,
|
||||||
maxOutputChannels: 2,
|
maxOutputChannels: 2,
|
||||||
@@ -116,7 +118,7 @@ export class CoreAudioBackend extends EventEmitter {
|
|||||||
// Fallback : devices par défaut
|
// Fallback : devices par défaut
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 'builtin-mic',
|
||||||
name: 'Built-in Microphone',
|
name: 'Built-in Microphone',
|
||||||
maxInputChannels: 1,
|
maxInputChannels: 1,
|
||||||
maxOutputChannels: 0,
|
maxOutputChannels: 0,
|
||||||
@@ -124,7 +126,7 @@ export class CoreAudioBackend extends EventEmitter {
|
|||||||
hostAPIName: 'Core Audio'
|
hostAPIName: 'Core Audio'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 'builtin-output',
|
||||||
name: 'Built-in Output',
|
name: 'Built-in Output',
|
||||||
maxInputChannels: 0,
|
maxInputChannels: 0,
|
||||||
maxOutputChannels: 2,
|
maxOutputChannels: 2,
|
||||||
@@ -203,7 +205,7 @@ export class CoreAudioBackend extends EventEmitter {
|
|||||||
|
|
||||||
// Si device spécifié
|
// Si device spécifié
|
||||||
if (this.options.inputDeviceName) {
|
if (this.options.inputDeviceName) {
|
||||||
args[1] = this.options.inputDeviceName;
|
args[2] = this.options.inputDeviceName; // Index 2 = device name
|
||||||
}
|
}
|
||||||
|
|
||||||
this.captureProcess = spawn('sox', args);
|
this.captureProcess = spawn('sox', args);
|
||||||
@@ -255,8 +257,10 @@ export class CoreAudioBackend extends EventEmitter {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async startPlayback() {
|
async startPlayback() {
|
||||||
|
console.log('🔊 Démarrage playback sox...');
|
||||||
|
|
||||||
if (this.isPlaying) {
|
if (this.isPlaying) {
|
||||||
console.warn('Lecture déjà active');
|
console.warn('⚠️ Lecture déjà active');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +268,9 @@ export class CoreAudioBackend extends EventEmitter {
|
|||||||
// Commande sox pour lecture audio
|
// Commande sox pour lecture audio
|
||||||
// play : lire vers output par défaut
|
// play : lire vers output par défaut
|
||||||
// -t raw : format raw PCM depuis stdin
|
// -t raw : format raw PCM depuis stdin
|
||||||
|
// --buffer : taille du buffer interne sox (en bytes)
|
||||||
const args = [
|
const args = [
|
||||||
|
'--buffer', '8192', // Buffer interne sox
|
||||||
'-t', 'raw',
|
'-t', 'raw',
|
||||||
'-b', '16',
|
'-b', '16',
|
||||||
'-e', 'signed-integer',
|
'-e', 'signed-integer',
|
||||||
@@ -280,7 +286,9 @@ export class CoreAudioBackend extends EventEmitter {
|
|||||||
args[args.length - 1] = this.options.outputDeviceName;
|
args[args.length - 1] = this.options.outputDeviceName;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.playbackProcess = spawn('sox', args);
|
this.playbackProcess = spawn('sox', args, {
|
||||||
|
stdio: ['pipe', 'ignore', 'pipe'] // stdin=pipe, stdout=ignore, stderr=pipe
|
||||||
|
});
|
||||||
|
|
||||||
// Gérer l'erreur EPIPE sur stdin (si processus se ferme)
|
// Gérer l'erreur EPIPE sur stdin (si processus se ferme)
|
||||||
this.playbackProcess.stdin.on('error', (error) => {
|
this.playbackProcess.stdin.on('error', (error) => {
|
||||||
@@ -305,13 +313,28 @@ export class CoreAudioBackend extends EventEmitter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.playbackProcess.on('close', (code) => {
|
this.playbackProcess.on('close', (code) => {
|
||||||
console.log(`Sox playback fermé (code ${code})`);
|
console.log(`⚠️ Sox playback fermé (code ${code}) après ${((Date.now() - this.playbackStartTime) / 1000).toFixed(1)}s`);
|
||||||
this.isPlaying = false;
|
this.isPlaying = false;
|
||||||
|
|
||||||
|
// Tenter de redémarrer si c'était inattendu
|
||||||
|
if (code !== 0) {
|
||||||
|
console.log('🔄 Tentative de redémarrage du playback...');
|
||||||
|
setTimeout(() => this.startPlayback(), 1000);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.playbackStartTime = Date.now();
|
||||||
this.isPlaying = true;
|
this.isPlaying = true;
|
||||||
this._startPlaybackLoop();
|
this._startPlaybackLoop();
|
||||||
|
|
||||||
|
// Envoyer immédiatement du silence pour démarrer sox
|
||||||
|
const silenceBuffer = Buffer.alloc(this.options.framesPerBuffer * 2 * this.options.channels);
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
if (this.playbackProcess.stdin.writable) {
|
||||||
|
this.playbackProcess.stdin.write(silenceBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`✓ Lecture audio démarrée : ${this.options.sampleRate}Hz, ${this.options.channels}ch`);
|
console.log(`✓ Lecture audio démarrée : ${this.options.sampleRate}Hz, ${this.options.channels}ch`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erreur démarrage lecture:', error);
|
console.error('Erreur démarrage lecture:', error);
|
||||||
@@ -323,6 +346,11 @@ export class CoreAudioBackend extends EventEmitter {
|
|||||||
* Arrête la lecture audio
|
* Arrête la lecture audio
|
||||||
*/
|
*/
|
||||||
stopPlayback() {
|
stopPlayback() {
|
||||||
|
if (this.playbackInterval) {
|
||||||
|
clearInterval(this.playbackInterval);
|
||||||
|
this.playbackInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.playbackProcess && this.isPlaying) {
|
if (this.playbackProcess && this.isPlaying) {
|
||||||
this.playbackProcess.kill('SIGTERM');
|
this.playbackProcess.kill('SIGTERM');
|
||||||
this.playbackProcess = null;
|
this.playbackProcess = null;
|
||||||
@@ -338,10 +366,16 @@ export class CoreAudioBackend extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
queueAudio(audioData) {
|
queueAudio(audioData) {
|
||||||
if (!this.isPlaying) {
|
if (!this.isPlaying) {
|
||||||
console.warn('Tentative ajout audio alors que lecture inactive');
|
// Ne logger qu'une fois pour éviter le spam
|
||||||
|
if (!this.playbackInactiveWarned) {
|
||||||
|
console.warn('⚠️ Tentative ajout audio alors que lecture inactive (message unique)');
|
||||||
|
this.playbackInactiveWarned = true;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.playbackInactiveWarned = false;
|
||||||
|
|
||||||
// Limite la taille du buffer pour éviter la latence excessive
|
// Limite la taille du buffer pour éviter la latence excessive
|
||||||
if (this.playbackBuffer.length < this.maxBufferSize) {
|
if (this.playbackBuffer.length < this.maxBufferSize) {
|
||||||
this.playbackBuffer.push(audioData);
|
this.playbackBuffer.push(audioData);
|
||||||
@@ -356,42 +390,48 @@ export class CoreAudioBackend extends EventEmitter {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_startPlaybackLoop() {
|
_startPlaybackLoop() {
|
||||||
const playNextChunk = () => {
|
// Calculer l'intervalle en ms (ex: 960 frames à 48kHz = 20ms)
|
||||||
|
const intervalMs = (this.options.framesPerBuffer / this.options.sampleRate) * 1000;
|
||||||
|
|
||||||
|
console.log(`🔁 Boucle playback démarrée (intervalle: ${intervalMs}ms)`);
|
||||||
|
|
||||||
|
// Utiliser setInterval pour garantir un flux continu
|
||||||
|
this.playbackInterval = setInterval(() => {
|
||||||
if (!this.isPlaying || !this.playbackProcess || !this.playbackProcess.stdin) {
|
if (!this.isPlaying || !this.playbackProcess || !this.playbackProcess.stdin) {
|
||||||
|
if (this.playbackInterval) {
|
||||||
|
clearInterval(this.playbackInterval);
|
||||||
|
this.playbackInterval = null;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let chunk;
|
||||||
if (this.playbackBuffer.length > 0) {
|
if (this.playbackBuffer.length > 0) {
|
||||||
const chunk = this.playbackBuffer.shift();
|
chunk = this.playbackBuffer.shift();
|
||||||
|
} else {
|
||||||
|
// Buffer vide : underrun (envoyer du silence)
|
||||||
|
chunk = Buffer.alloc(this.options.framesPerBuffer * 2 * this.options.channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toujours écrire quelque chose pour garder sox actif
|
||||||
try {
|
try {
|
||||||
if (this.playbackProcess.stdin.writable) {
|
if (this.playbackProcess.stdin.writable) {
|
||||||
this.playbackProcess.stdin.write(chunk);
|
this.playbackProcess.stdin.write(chunk);
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur écriture stdin sox:', error);
|
|
||||||
this.isPlaying = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Buffer vide : underrun (silence)
|
console.warn('⚠️ Sox stdin non writable, arrêt boucle');
|
||||||
const silenceBuffer = Buffer.alloc(this.options.framesPerBuffer * 2 * this.options.channels);
|
this.isPlaying = false;
|
||||||
try {
|
clearInterval(this.playbackInterval);
|
||||||
if (this.playbackProcess.stdin.writable) {
|
this.playbackInterval = null;
|
||||||
this.playbackProcess.stdin.write(silenceBuffer);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore si process fermé
|
if (error.code !== 'EPIPE') {
|
||||||
|
console.error('Erreur écriture stdin sox:', error);
|
||||||
|
}
|
||||||
this.isPlaying = false;
|
this.isPlaying = false;
|
||||||
return;
|
clearInterval(this.playbackInterval);
|
||||||
|
this.playbackInterval = null;
|
||||||
}
|
}
|
||||||
this.emit('bufferUnderrun');
|
}, intervalMs);
|
||||||
}
|
|
||||||
|
|
||||||
const intervalMs = (this.options.framesPerBuffer / this.options.sampleRate) * 1000;
|
|
||||||
setTimeout(playNextChunk, intervalMs);
|
|
||||||
};
|
|
||||||
|
|
||||||
playNextChunk();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+14
-14
@@ -4,30 +4,29 @@ audio:
|
|||||||
defaultBitrate: 96
|
defaultBitrate: 96
|
||||||
jitterBufferMs: 40
|
jitterBufferMs: 40
|
||||||
device:
|
device:
|
||||||
# inputDeviceId et outputDeviceId : laisser vide pour auto-détection du device par défaut
|
inputDeviceId: Microphone MacBook Pro
|
||||||
# ou spécifier un ID numérique pour forcer un device spécifique
|
outputDeviceId: Haut-parleurs MacBook Pro
|
||||||
inputDeviceId: null
|
|
||||||
outputDeviceId: null
|
|
||||||
sampleRate: 48000
|
sampleRate: 48000
|
||||||
routing:
|
routing:
|
||||||
inputToGroup:
|
inputToGroup:
|
||||||
"0":
|
"0":
|
||||||
- technique
|
- production
|
||||||
"1":
|
"1": []
|
||||||
- technique
|
"2": []
|
||||||
"2":
|
|
||||||
- technique
|
|
||||||
"4":
|
"4":
|
||||||
- technique
|
- technique
|
||||||
"5":
|
"5":
|
||||||
- technique
|
- technique
|
||||||
groupToOutput:
|
groupToOutput:
|
||||||
technique:
|
technique:
|
||||||
|
- "1"
|
||||||
|
production:
|
||||||
- "0"
|
- "0"
|
||||||
|
- "1"
|
||||||
gains: {}
|
gains: {}
|
||||||
channelNames:
|
channelNames:
|
||||||
inputs:
|
inputs:
|
||||||
"0": Micro Régisseur
|
"0": iphone
|
||||||
"1": Talkback FOH
|
"1": Talkback FOH
|
||||||
"2": Retour Console
|
"2": Retour Console
|
||||||
"3": Liaison Scène
|
"3": Liaison Scène
|
||||||
@@ -42,6 +41,7 @@ groups:
|
|||||||
audioBitrate: 96
|
audioBitrate: 96
|
||||||
channels: []
|
channels: []
|
||||||
- name: Technique
|
- name: Technique
|
||||||
|
audioBitrate: 96
|
||||||
channels: []
|
channels: []
|
||||||
- name: Sonorisation
|
- name: Sonorisation
|
||||||
audioBitrate: 128
|
audioBitrate: 128
|
||||||
@@ -50,8 +50,8 @@ server:
|
|||||||
host: 0.0.0.0
|
host: 0.0.0.0
|
||||||
port: 3000
|
port: 3000
|
||||||
livekit:
|
livekit:
|
||||||
url: ws://localhost:7880
|
url: ws://192.168.0.146:7880
|
||||||
logging:
|
logging:
|
||||||
level: debug
|
level: info # Changez à 'debug' pour voir plus de détails
|
||||||
logLatency: true
|
logLatency: false
|
||||||
logAudioStats: true
|
logAudioStats: false
|
||||||
|
|||||||
@@ -13,12 +13,18 @@ import adminRouter, { registerUser, addLog } from './api/admin.js';
|
|||||||
import configManager from './config/ConfigManager.js';
|
import configManager from './config/ConfigManager.js';
|
||||||
import audioBridgeManager from './bridge/AudioBridgeManager.js';
|
import audioBridgeManager from './bridge/AudioBridgeManager.js';
|
||||||
import AudioLevelsServer from './websocket/AudioLevelsServer.js';
|
import AudioLevelsServer from './websocket/AudioLevelsServer.js';
|
||||||
|
import { setGlobalLogLevel } from './utils/Logger.js';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
// Chargement configuration via ConfigManager
|
// Chargement configuration via ConfigManager
|
||||||
const config = configManager.get();
|
const config = configManager.get();
|
||||||
|
|
||||||
|
// Configure le niveau de log
|
||||||
|
const logLevel = config.logging?.level?.toUpperCase() || 'INFO';
|
||||||
|
setGlobalLogLevel(logLevel);
|
||||||
|
console.log(`📊 Niveau de log: ${logLevel}`);
|
||||||
|
|
||||||
// Note: Les IDs sont maintenant générés automatiquement par le ConfigManager
|
// Note: Les IDs sont maintenant générés automatiquement par le ConfigManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Logger.js
|
||||||
|
* Système de logging centralisé avec niveaux configurables
|
||||||
|
*/
|
||||||
|
|
||||||
|
const LOG_LEVELS = {
|
||||||
|
ERROR: 0,
|
||||||
|
WARN: 1,
|
||||||
|
INFO: 2,
|
||||||
|
DEBUG: 3,
|
||||||
|
TRACE: 4
|
||||||
|
};
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
constructor(category = 'default', level = 'INFO') {
|
||||||
|
this.category = category;
|
||||||
|
this.level = LOG_LEVELS[level] ?? LOG_LEVELS.INFO;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLevel(level) {
|
||||||
|
this.level = LOG_LEVELS[level] ?? LOG_LEVELS.INFO;
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message, ...args) {
|
||||||
|
if (this.level >= LOG_LEVELS.ERROR) {
|
||||||
|
console.error(`[${this.category}] ❌`, message, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(message, ...args) {
|
||||||
|
if (this.level >= LOG_LEVELS.WARN) {
|
||||||
|
console.warn(`[${this.category}] ⚠️ `, message, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info(message, ...args) {
|
||||||
|
if (this.level >= LOG_LEVELS.INFO) {
|
||||||
|
console.log(`[${this.category}] ℹ️ `, message, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
success(message, ...args) {
|
||||||
|
if (this.level >= LOG_LEVELS.INFO) {
|
||||||
|
console.log(`[${this.category}] ✓`, message, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(message, ...args) {
|
||||||
|
if (this.level >= LOG_LEVELS.DEBUG) {
|
||||||
|
console.log(`[${this.category}] 🔍`, message, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trace(message, ...args) {
|
||||||
|
if (this.level >= LOG_LEVELS.TRACE) {
|
||||||
|
console.log(`[${this.category}] 🔬`, message, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuration globale depuis env ou config
|
||||||
|
const globalLevel = process.env.LOG_LEVEL || 'INFO';
|
||||||
|
|
||||||
|
// Loggers par catégorie
|
||||||
|
const loggers = new Map();
|
||||||
|
|
||||||
|
export function getLogger(category) {
|
||||||
|
if (!loggers.has(category)) {
|
||||||
|
loggers.set(category, new Logger(category, globalLevel));
|
||||||
|
}
|
||||||
|
return loggers.get(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setGlobalLogLevel(level) {
|
||||||
|
loggers.forEach(logger => logger.setLevel(level));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { getLogger, setGlobalLogLevel };
|
||||||
Reference in New Issue
Block a user