Add active speaker indicators to the meet room UI.
Deploy Ladill Meet / deploy (push) Successful in 43s
Deploy Ladill Meet / deploy (push) Successful in 43s
Highlight speaking participants on video tiles and in the panel, with header copy that names one or multiple speakers at once. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -59,6 +59,7 @@ function meetRoom() {
|
||||
connectionStatus: 'Connecting…',
|
||||
participants: [],
|
||||
sessionParticipants: [],
|
||||
activeSpeakerIds: [],
|
||||
roomHost: null,
|
||||
floatingReactions: [],
|
||||
pollTimer: null,
|
||||
@@ -254,6 +255,9 @@ function meetRoom() {
|
||||
this.connectionStatus = 'Connected';
|
||||
}
|
||||
});
|
||||
room.on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
|
||||
this.syncActiveSpeakers(speakers);
|
||||
});
|
||||
|
||||
await room.prepareConnection(this.config.url, this.config.token);
|
||||
|
||||
@@ -545,6 +549,71 @@ function meetRoom() {
|
||||
return labels[role] || 'Participant';
|
||||
},
|
||||
|
||||
isSpeaking(uuid) {
|
||||
return this.activeSpeakerIds.includes(uuid);
|
||||
},
|
||||
|
||||
activeSpeakerNames() {
|
||||
return this.activeSpeakerIds
|
||||
.map((id) => {
|
||||
const person = this.sessionParticipants.find((p) => p.uuid === id);
|
||||
if (person?.display_name) {
|
||||
return person.display_name;
|
||||
}
|
||||
|
||||
if (room?.localParticipant?.identity === id) {
|
||||
return room.localParticipant.name || 'You';
|
||||
}
|
||||
|
||||
return room?.remoteParticipants.get(id)?.name || 'Someone';
|
||||
})
|
||||
.filter(Boolean);
|
||||
},
|
||||
|
||||
activeSpeakersMessage() {
|
||||
const names = this.activeSpeakerNames();
|
||||
if (names.length === 0) return '';
|
||||
if (names.length === 1) return `${names[0]} is speaking`;
|
||||
if (names.length === 2) return `${names[0]} and ${names[1]} are speaking`;
|
||||
|
||||
const last = names[names.length - 1];
|
||||
const rest = names.slice(0, -1).join(', ');
|
||||
|
||||
return `${rest}, and ${last} are speaking`;
|
||||
},
|
||||
|
||||
activeSpeakerCount() {
|
||||
return this.activeSpeakerIds.length;
|
||||
},
|
||||
|
||||
inCallParticipantsSorted() {
|
||||
return [...this.inCallParticipants()].sort((a, b) => {
|
||||
const aSpeaking = this.isSpeaking(a.uuid);
|
||||
const bSpeaking = this.isSpeaking(b.uuid);
|
||||
|
||||
if (aSpeaking !== bSpeaking) {
|
||||
return aSpeaking ? -1 : 1;
|
||||
}
|
||||
|
||||
return a.display_name.localeCompare(b.display_name);
|
||||
});
|
||||
},
|
||||
|
||||
syncActiveSpeakers(speakers) {
|
||||
this.activeSpeakerIds = speakers.map((p) => p.identity);
|
||||
|
||||
const grid = document.getElementById('video-grid');
|
||||
if (!grid) return;
|
||||
|
||||
grid.querySelectorAll('[data-participant-tile]').forEach((tile) => {
|
||||
const identity = tile.dataset.participantTile;
|
||||
const speaking = this.activeSpeakerIds.includes(identity);
|
||||
|
||||
tile.classList.toggle('meet-tile-speaking', speaking);
|
||||
tile.querySelector('[data-tile-speaking]')?.classList.toggle('hidden', !speaking);
|
||||
});
|
||||
},
|
||||
|
||||
async admitParticipant(uuid) {
|
||||
if (!this.isHost || !this.config.sessionUuid) return;
|
||||
|
||||
@@ -591,6 +660,13 @@ function meetRoom() {
|
||||
<div class="absolute inset-x-0 bottom-0 flex items-center justify-between gap-2 bg-gradient-to-t from-black/80 via-black/40 to-transparent px-3 pb-2 pt-8">
|
||||
<span data-tile-name class="truncate text-xs font-medium text-white"></span>
|
||||
<div class="flex shrink-0 items-center gap-1.5">
|
||||
<span data-tile-speaking class="hidden meet-speaking-badge rounded-full bg-emerald-500/90 p-1 text-white" title="Speaking">
|
||||
<svg class="h-3.5 w-3.5" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
||||
<rect class="meet-speaking-bar meet-speaking-bar--1" x="1" y="5" width="2.5" height="6" rx="1"/>
|
||||
<rect class="meet-speaking-bar meet-speaking-bar--2" x="6.75" y="3" width="2.5" height="10" rx="1"/>
|
||||
<rect class="meet-speaking-bar meet-speaking-bar--3" x="12.5" y="6" width="2.5" height="4" rx="1"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span data-tile-mic class="hidden rounded-full bg-black/50 p-1 text-red-400" title="Muted">
|
||||
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 18.75a6 6 0 0 0 6-6v-1.5m-6 7.5a6 6 0 0 1-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 0 1-3-3V4.5a3 3 0 0 1 6 0v8.25a3 3 0 0 1-3 3Z"/><path stroke-linecap="round" stroke-linejoin="round" d="m3 3 18 18"/></svg>
|
||||
</span>
|
||||
@@ -609,6 +685,10 @@ function meetRoom() {
|
||||
circle.textContent = this.participantInitials(displayName);
|
||||
tile.querySelector('[data-tile-name]').textContent = displayName;
|
||||
|
||||
const speaking = this.activeSpeakerIds.includes(identity);
|
||||
tile.classList.toggle('meet-tile-speaking', speaking);
|
||||
tile.querySelector('[data-tile-speaking]')?.classList.toggle('hidden', !speaking);
|
||||
|
||||
return tile;
|
||||
},
|
||||
|
||||
@@ -1020,6 +1100,8 @@ function meetRoom() {
|
||||
tile.remove();
|
||||
}
|
||||
});
|
||||
|
||||
this.syncActiveSpeakers(room.activeSpeakers ?? []);
|
||||
},
|
||||
|
||||
pruneTilesFromSession() {
|
||||
|
||||
Reference in New Issue
Block a user