refactor: remplacement système de canaux statiques par canaux virtuels depuis routing
This commit is contained in:
+10
-6
@@ -23,7 +23,8 @@ function App() {
|
|||||||
disconnect,
|
disconnect,
|
||||||
switchGroup,
|
switchGroup,
|
||||||
startTalking,
|
startTalking,
|
||||||
stopTalking
|
stopTalking,
|
||||||
|
toggleParticipantMute
|
||||||
} = useLiveKit();
|
} = useLiveKit();
|
||||||
|
|
||||||
// Charger configuration au démarrage
|
// Charger configuration au démarrage
|
||||||
@@ -91,8 +92,8 @@ function App() {
|
|||||||
|
|
||||||
console.log('🔗 Connexion LiveKit:', livekitUrl);
|
console.log('🔗 Connexion LiveKit:', livekitUrl);
|
||||||
|
|
||||||
// Se connecter à LiveKit
|
// Se connecter à LiveKit avec les canaux virtuels
|
||||||
await connect(livekitUrl, data.token);
|
await connect(livekitUrl, data.token, data.virtualChannels || []);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Erreur connexion:', err);
|
console.error('Erreur connexion:', err);
|
||||||
@@ -136,8 +137,8 @@ function App() {
|
|||||||
livekitUrl = `${window.location.protocol}//${window.location.host}/livekit`;
|
livekitUrl = `${window.location.protocol}//${window.location.host}/livekit`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Changer de room LiveKit
|
// Changer de room LiveKit avec les canaux virtuels du nouveau groupe
|
||||||
await switchGroup(livekitUrl, data.token);
|
await switchGroup(livekitUrl, data.token, data.virtualChannels || []);
|
||||||
|
|
||||||
// Mettre à jour l'état
|
// Mettre à jour l'état
|
||||||
setGroupId(newGroupId);
|
setGroupId(newGroupId);
|
||||||
@@ -238,7 +239,10 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Liste des participants */}
|
{/* Liste des participants */}
|
||||||
<UserList participants={participants} />
|
<UserList
|
||||||
|
participants={participants}
|
||||||
|
onToggleMute={toggleParticipantMute}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Bouton PTT principal avec VU-mètre intégré */}
|
{/* Bouton PTT principal avec VU-mètre intégré */}
|
||||||
<PTTButton
|
<PTTButton
|
||||||
|
|||||||
@@ -84,6 +84,20 @@
|
|||||||
background: rgba(16, 185, 129, 0.1);
|
background: rgba(16, 185, 129, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Canal virtuel */
|
||||||
|
.user-item.virtual-channel {
|
||||||
|
border-left: 3px solid var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item.virtual-channel.muted {
|
||||||
|
opacity: 0.5;
|
||||||
|
border-left-color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar.channel {
|
||||||
|
background: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
/* Avatar */
|
/* Avatar */
|
||||||
.user-avatar {
|
.user-avatar {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
@@ -190,3 +204,44 @@
|
|||||||
max-height: 120px;
|
max-height: 120px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bouton mute/unmute */
|
||||||
|
.mute-button {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: none;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mute-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mute-button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mute-button svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-item.muted .mute-button {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--color-accent);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,27 +1,69 @@
|
|||||||
import './UserList.css';
|
import './UserList.css';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Liste des participants connectés
|
* Liste des participants connectés (utilisateurs + canaux virtuels)
|
||||||
*/
|
*/
|
||||||
export default function UserList({ participants }) {
|
export default function UserList({ participants, onToggleMute }) {
|
||||||
if (participants.length === 0) {
|
if (participants.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="user-list empty">
|
<div className="user-list empty">
|
||||||
<p className="empty-message">Aucun autre participant</p>
|
<p className="empty-message">Aucun participant ou canal</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Séparer canaux virtuels et utilisateurs
|
||||||
|
const virtualChannels = participants.filter(p => p.isVirtual);
|
||||||
|
const users = participants.filter(p => !p.isVirtual);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="user-list">
|
<div className="user-list">
|
||||||
<div className="user-list-header">
|
<div className="user-list-header">
|
||||||
<span className="user-count">
|
<span className="user-count">
|
||||||
{participants.length} participant{participants.length > 1 ? 's' : ''}
|
{virtualChannels.length > 0 && `${virtualChannels.length} canal${virtualChannels.length > 1 ? 'aux' : ''}`}
|
||||||
|
{virtualChannels.length > 0 && users.length > 0 && ' • '}
|
||||||
|
{users.length > 0 && `${users.length} utilisateur${users.length > 1 ? 's' : ''}`}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="user-list-items">
|
<div className="user-list-items">
|
||||||
{participants.map((participant) => (
|
{/* Canaux virtuels en premier */}
|
||||||
|
{virtualChannels.map((participant) => (
|
||||||
|
<div
|
||||||
|
key={participant.identity}
|
||||||
|
className={`user-item virtual-channel ${participant.isSpeaking ? 'speaking' : ''} ${participant.isMuted ? 'muted' : ''}`}
|
||||||
|
>
|
||||||
|
<div className="user-avatar channel">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor" width="24" height="24">
|
||||||
|
<path d="M12 3v9.28c-.47-.17-.97-.28-1.5-.28C8.01 12 6 14.01 6 16.5S8.01 21 10.5 21c2.31 0 4.2-1.75 4.45-4H15V6h4V3h-7z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="user-info">
|
||||||
|
<span className="user-name">{participant.name}</span>
|
||||||
|
<span className="user-status channel-label">Canal audio</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="mute-button"
|
||||||
|
onClick={() => onToggleMute(participant.identity, participant.isVirtual)}
|
||||||
|
title={participant.isMuted ? 'Activer' : 'Désactiver'}
|
||||||
|
>
|
||||||
|
{participant.isMuted ? (
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Utilisateurs WebRTC */}
|
||||||
|
{users.map((participant) => (
|
||||||
<div
|
<div
|
||||||
key={participant.identity}
|
key={participant.identity}
|
||||||
className={`user-item ${participant.isSpeaking ? 'speaking' : ''}`}
|
className={`user-item ${participant.isSpeaking ? 'speaking' : ''}`}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export default function useLiveKit() {
|
|||||||
const analyserRef = useRef(null);
|
const analyserRef = useRef(null);
|
||||||
const animationFrameRef = useRef(null);
|
const animationFrameRef = useRef(null);
|
||||||
const isAudioUnlockedRef = useRef(false);
|
const isAudioUnlockedRef = useRef(false);
|
||||||
|
const virtualChannelsRef = useRef([]);
|
||||||
|
const mutedChannelsRef = useRef(new Set()); // IDs des canaux muted
|
||||||
|
|
||||||
// Analyseur audio pour pistes distantes (audio entrant)
|
// Analyseur audio pour pistes distantes (audio entrant)
|
||||||
const remoteAudioContextRef = useRef(null);
|
const remoteAudioContextRef = useRef(null);
|
||||||
@@ -25,8 +27,11 @@ export default function useLiveKit() {
|
|||||||
/**
|
/**
|
||||||
* Connexion à la room LiveKit
|
* Connexion à la room LiveKit
|
||||||
*/
|
*/
|
||||||
const connect = useCallback(async (url, token) => {
|
const connect = useCallback(async (url, token, virtualChannels = []) => {
|
||||||
try {
|
try {
|
||||||
|
// Stocker les canaux virtuels
|
||||||
|
virtualChannelsRef.current = virtualChannels;
|
||||||
|
|
||||||
// Créer room
|
// Créer room
|
||||||
const room = new Room({
|
const room = new Room({
|
||||||
adaptiveStream: true,
|
adaptiveStream: true,
|
||||||
@@ -154,7 +159,7 @@ export default function useLiveKit() {
|
|||||||
/**
|
/**
|
||||||
* Changer de groupe (reconnexion à une nouvelle room)
|
* Changer de groupe (reconnexion à une nouvelle room)
|
||||||
*/
|
*/
|
||||||
const switchGroup = useCallback(async (url, token) => {
|
const switchGroup = useCallback(async (url, token, virtualChannels = []) => {
|
||||||
console.log('🔄 Changement de groupe...');
|
console.log('🔄 Changement de groupe...');
|
||||||
|
|
||||||
// Déconnexion propre
|
// Déconnexion propre
|
||||||
@@ -167,8 +172,11 @@ export default function useLiveKit() {
|
|||||||
setIsConnected(false);
|
setIsConnected(false);
|
||||||
setParticipants([]);
|
setParticipants([]);
|
||||||
|
|
||||||
|
// Reset canaux muted
|
||||||
|
mutedChannelsRef.current.clear();
|
||||||
|
|
||||||
// Reconnexion avec nouveau token
|
// Reconnexion avec nouveau token
|
||||||
await connect(url, token);
|
await connect(url, token, virtualChannels);
|
||||||
}, [connect]);
|
}, [connect]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -252,15 +260,30 @@ export default function useLiveKit() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mise à jour liste participants
|
* Mise à jour liste participants (inclut canaux virtuels)
|
||||||
*/
|
*/
|
||||||
const updateParticipants = () => {
|
const updateParticipants = useCallback(() => {
|
||||||
if (!roomRef.current) return;
|
if (!roomRef.current) return;
|
||||||
|
|
||||||
const room = roomRef.current;
|
const room = roomRef.current;
|
||||||
const participantsList = [];
|
const participantsList = [];
|
||||||
|
|
||||||
// Participants distants
|
// Canaux virtuels (affichés en premier)
|
||||||
|
virtualChannelsRef.current.forEach((channel) => {
|
||||||
|
participantsList.push({
|
||||||
|
identity: channel.id,
|
||||||
|
name: channel.name,
|
||||||
|
isLocal: false,
|
||||||
|
isVirtual: true,
|
||||||
|
isSpeaking: false, // TODO: détection audio depuis bridge
|
||||||
|
hasAudio: true,
|
||||||
|
isMuted: mutedChannelsRef.current.has(channel.id),
|
||||||
|
audioInput: channel.audioInput,
|
||||||
|
audioOutput: channel.audioOutput
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Participants distants (utilisateurs WebRTC)
|
||||||
room.remoteParticipants.forEach((participant) => {
|
room.remoteParticipants.forEach((participant) => {
|
||||||
const audioTracks = participant.audioTracks ? Array.from(participant.audioTracks.values()) : [];
|
const audioTracks = participant.audioTracks ? Array.from(participant.audioTracks.values()) : [];
|
||||||
const audioPublication = audioTracks[0];
|
const audioPublication = audioTracks[0];
|
||||||
@@ -270,13 +293,63 @@ export default function useLiveKit() {
|
|||||||
identity: participant.identity,
|
identity: participant.identity,
|
||||||
name: participant.name || participant.identity,
|
name: participant.name || participant.identity,
|
||||||
isLocal: false,
|
isLocal: false,
|
||||||
|
isVirtual: false,
|
||||||
isSpeaking,
|
isSpeaking,
|
||||||
hasAudio: audioPublication?.isSubscribed || false
|
hasAudio: audioPublication?.isSubscribed || false,
|
||||||
|
isMuted: false
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
setParticipants(participantsList);
|
setParticipants(participantsList);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle mute/unmute d'un participant (canal virtuel ou utilisateur)
|
||||||
|
*/
|
||||||
|
const toggleParticipantMute = useCallback((participantId, isVirtual) => {
|
||||||
|
if (isVirtual) {
|
||||||
|
// Canal virtuel : toggle dans l'état local
|
||||||
|
const isMuted = mutedChannelsRef.current.has(participantId);
|
||||||
|
|
||||||
|
if (isMuted) {
|
||||||
|
mutedChannelsRef.current.delete(participantId);
|
||||||
|
console.log('🔊 Canal virtuel unmuted:', participantId);
|
||||||
|
} else {
|
||||||
|
mutedChannelsRef.current.add(participantId);
|
||||||
|
console.log('🔇 Canal virtuel muted:', participantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Phase 3: Envoyer commande au bridge audio via DataChannel
|
||||||
|
// pour vraiment muter/unmuter le canal physique
|
||||||
|
|
||||||
|
// Mettre à jour l'affichage
|
||||||
|
updateParticipants();
|
||||||
|
} else {
|
||||||
|
// Utilisateur WebRTC : muter localement la lecture audio
|
||||||
|
if (!roomRef.current) return;
|
||||||
|
|
||||||
|
const participant = roomRef.current.remoteParticipants.get(participantId);
|
||||||
|
if (!participant) return;
|
||||||
|
|
||||||
|
const audioTracks = Array.from(participant.audioTracks.values());
|
||||||
|
const audioPublication = audioTracks[0];
|
||||||
|
|
||||||
|
if (audioPublication && audioPublication.audioTrack) {
|
||||||
|
const track = audioPublication.audioTrack;
|
||||||
|
const newMutedState = !track.isMuted;
|
||||||
|
|
||||||
|
if (newMutedState) {
|
||||||
|
track.mute();
|
||||||
|
console.log('🔇 Participant muted:', participantId);
|
||||||
|
} else {
|
||||||
|
track.unmute();
|
||||||
|
console.log('🔊 Participant unmuted:', participantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateParticipants();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [updateParticipants]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup analyseur audio pour VU-mètre (micro local)
|
* Setup analyseur audio pour VU-mètre (micro local)
|
||||||
@@ -412,6 +485,7 @@ export default function useLiveKit() {
|
|||||||
disconnect,
|
disconnect,
|
||||||
switchGroup,
|
switchGroup,
|
||||||
startTalking,
|
startTalking,
|
||||||
stopTalking
|
stopTalking,
|
||||||
|
toggleParticipantMute
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
--color-success: #10b981;
|
--color-success: #10b981;
|
||||||
--color-warning: #f59e0b;
|
--color-warning: #f59e0b;
|
||||||
--color-danger: #ef4444;
|
--color-danger: #ef4444;
|
||||||
|
--color-accent: #8b5cf6;
|
||||||
|
--color-error: #ef4444;
|
||||||
|
|
||||||
/* PTT States */
|
/* PTT States */
|
||||||
--color-ptt-idle: #374151;
|
--color-ptt-idle: #374151;
|
||||||
|
|||||||
+10
-35
@@ -52,16 +52,12 @@ function loadConfig() {
|
|||||||
const configFile = readFileSync(configPath, 'utf8');
|
const configFile = readFileSync(configPath, 'utf8');
|
||||||
const config = YAML.parse(configFile);
|
const config = YAML.parse(configFile);
|
||||||
|
|
||||||
// Générer les IDs pour les groupes et canaux
|
// Générer les IDs pour les groupes
|
||||||
config.groups = config.groups.map(group => {
|
config.groups = config.groups.map(group => {
|
||||||
const groupId = slugify(group.name);
|
const groupId = slugify(group.name);
|
||||||
return {
|
return {
|
||||||
...group,
|
...group,
|
||||||
id: groupId,
|
id: groupId
|
||||||
channels: group.channels ? group.channels.map(channel => ({
|
|
||||||
...channel,
|
|
||||||
id: channel.id || `${groupId}-${slugify(channel.name)}`
|
|
||||||
})) : []
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -78,13 +74,7 @@ function saveConfig(config) {
|
|||||||
...config,
|
...config,
|
||||||
groups: config.groups.map(group => {
|
groups: config.groups.map(group => {
|
||||||
const { id, ...groupWithoutId } = group;
|
const { id, ...groupWithoutId } = group;
|
||||||
return {
|
return groupWithoutId;
|
||||||
...groupWithoutId,
|
|
||||||
channels: group.channels ? group.channels.map(channel => {
|
|
||||||
const { id: channelId, ...channelWithoutId } = channel;
|
|
||||||
return channelWithoutId;
|
|
||||||
}) : []
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -194,11 +184,11 @@ router.get('/groups', (req, res) => {
|
|||||||
*/
|
*/
|
||||||
router.post('/groups', (req, res) => {
|
router.post('/groups', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { name, audioBitrate, channels } = req.body;
|
const { name, audioBitrate } = req.body;
|
||||||
|
|
||||||
if (!name || !channels || !Array.isArray(channels)) {
|
if (!name) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: 'Missing required fields: name, channels'
|
error: 'Missing required field: name'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,17 +204,10 @@ router.post('/groups', (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Générer les IDs pour les canaux
|
// Créer le nouveau groupe (sans channels)
|
||||||
const channelsWithIds = channels.map(channel => ({
|
|
||||||
...channel,
|
|
||||||
id: channel.id || `${id}-${slugify(channel.name)}`
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Créer le nouveau groupe
|
|
||||||
const newGroup = {
|
const newGroup = {
|
||||||
name,
|
name,
|
||||||
audioBitrate: audioBitrate || config.audio.defaultBitrate,
|
...(audioBitrate && { audioBitrate })
|
||||||
channels: channelsWithIds
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config.groups.push(newGroup);
|
config.groups.push(newGroup);
|
||||||
@@ -246,13 +229,13 @@ router.post('/groups', (req, res) => {
|
|||||||
/**
|
/**
|
||||||
* PUT /admin/groups/:id
|
* PUT /admin/groups/:id
|
||||||
* Modifie un groupe existant
|
* Modifie un groupe existant
|
||||||
* Body: { name?, audioBitrate?, channels? }
|
* Body: { name?, audioBitrate? }
|
||||||
* Note: l'ID est un slug généré, on cherche le groupe par nom dans le YAML
|
* Note: l'ID est un slug généré, on cherche le groupe par nom dans le YAML
|
||||||
*/
|
*/
|
||||||
router.put('/groups/:id', (req, res) => {
|
router.put('/groups/:id', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { name, audioBitrate, channels } = req.body;
|
const { name, audioBitrate } = req.body;
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
@@ -268,14 +251,6 @@ router.put('/groups/:id', (req, res) => {
|
|||||||
// Mettre à jour les champs fournis
|
// Mettre à jour les champs fournis
|
||||||
if (name !== undefined) config.groups[groupIndex].name = name;
|
if (name !== undefined) config.groups[groupIndex].name = name;
|
||||||
if (audioBitrate !== undefined) config.groups[groupIndex].audioBitrate = audioBitrate;
|
if (audioBitrate !== undefined) config.groups[groupIndex].audioBitrate = audioBitrate;
|
||||||
if (channels !== undefined) {
|
|
||||||
// Pas besoin de générer les IDs ici, ils seront générés au chargement
|
|
||||||
config.groups[groupIndex].channels = channels.map(channel => ({
|
|
||||||
name: channel.name,
|
|
||||||
audioInput: channel.audioInput,
|
|
||||||
audioOutput: channel.audioOutput
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
saveConfig(config);
|
saveConfig(config);
|
||||||
|
|
||||||
|
|||||||
+24
-18
@@ -7,30 +7,36 @@ audio:
|
|||||||
inputDeviceId: 0
|
inputDeviceId: 0
|
||||||
outputDeviceId: 2
|
outputDeviceId: 2
|
||||||
sampleRate: 48000
|
sampleRate: 48000
|
||||||
|
routing:
|
||||||
|
inputToGroup:
|
||||||
|
"1":
|
||||||
|
- technique
|
||||||
|
"2":
|
||||||
|
- technique
|
||||||
|
"4":
|
||||||
|
- technique
|
||||||
|
"5":
|
||||||
|
- technique
|
||||||
|
groupToOutput: {}
|
||||||
|
gains: {}
|
||||||
|
channelNames:
|
||||||
|
inputs:
|
||||||
|
"0": "Micro Régisseur"
|
||||||
|
"1": "Talkback FOH"
|
||||||
|
"2": "Retour Console"
|
||||||
|
"3": "Liaison Scène"
|
||||||
|
"4": "Monitor Mix"
|
||||||
|
"5": "Spare 1"
|
||||||
|
outputs:
|
||||||
|
"0": "Sortie Principale"
|
||||||
|
"1": "Retour Scène"
|
||||||
|
"2": "Talkback Console"
|
||||||
groups:
|
groups:
|
||||||
- name: Production
|
- name: Production
|
||||||
audioBitrate: 96
|
audioBitrate: 96
|
||||||
channels:
|
|
||||||
- name: Principal
|
|
||||||
audioInput: 0
|
|
||||||
audioOutput: 0
|
|
||||||
- name: Backup
|
|
||||||
audioInput: 1
|
|
||||||
audioOutput: 1
|
|
||||||
- name: Technique
|
- name: Technique
|
||||||
channels:
|
|
||||||
- name: Général
|
|
||||||
audioInput: 2
|
|
||||||
audioOutput: 2
|
|
||||||
- name: Sonorisation
|
- name: Sonorisation
|
||||||
audioBitrate: 128
|
audioBitrate: 128
|
||||||
channels:
|
|
||||||
- name: Principal
|
|
||||||
audioInput: 3
|
|
||||||
audioOutput: 3
|
|
||||||
- name: Retours
|
|
||||||
audioInput: 4
|
|
||||||
audioOutput: 4
|
|
||||||
server:
|
server:
|
||||||
host: 0.0.0.0
|
host: 0.0.0.0
|
||||||
port: 3000
|
port: 3000
|
||||||
|
|||||||
+21
-6
@@ -196,11 +196,7 @@ app.get('/config', (req, res) => {
|
|||||||
const clientConfig = {
|
const clientConfig = {
|
||||||
groups: config.groups.map(g => ({
|
groups: config.groups.map(g => ({
|
||||||
id: g.id,
|
id: g.id,
|
||||||
name: g.name,
|
name: g.name
|
||||||
channels: g.channels.map(c => ({
|
|
||||||
id: c.id,
|
|
||||||
name: c.name
|
|
||||||
}))
|
|
||||||
})),
|
})),
|
||||||
audio: {
|
audio: {
|
||||||
sampleRate: config.audio.sampleRate,
|
sampleRate: config.audio.sampleRate,
|
||||||
@@ -281,11 +277,30 @@ app.post('/token', async (req, res) => {
|
|||||||
// Enregistrer l'utilisateur dans le système admin
|
// Enregistrer l'utilisateur dans le système admin
|
||||||
registerUser(participantIdentity, username, groupId, roomName);
|
registerUser(participantIdentity, username, groupId, roomName);
|
||||||
|
|
||||||
|
// Générer les canaux virtuels depuis le routing (inputs uniquement)
|
||||||
|
const virtualChannels = [];
|
||||||
|
const inputToGroup = config.audio?.routing?.inputToGroup || {};
|
||||||
|
const channelNames = config.audio?.channelNames?.inputs || {};
|
||||||
|
|
||||||
|
// Trouver tous les canaux physiques routés vers ce groupe
|
||||||
|
for (const [inputChannel, groups] of Object.entries(inputToGroup)) {
|
||||||
|
if (groups.includes(groupId)) {
|
||||||
|
const channelName = channelNames[inputChannel] || `Canal ${inputChannel}`;
|
||||||
|
virtualChannels.push({
|
||||||
|
id: `input-${inputChannel}`,
|
||||||
|
name: channelName,
|
||||||
|
isVirtual: true,
|
||||||
|
audioInput: parseInt(inputChannel, 10)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
token,
|
token,
|
||||||
url: LIVEKIT_URL,
|
url: LIVEKIT_URL,
|
||||||
roomName,
|
roomName,
|
||||||
participantIdentity
|
participantIdentity,
|
||||||
|
virtualChannels
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user