Display call member speaking status on bottom bar (#2742)

* add mutation observer hok

* add hook to read speaking member by observing iframe content

* display speaking member name in call status bar and improve layout

* fix shrining
This commit is contained in:
Ajay Bura
2026-03-08 22:00:35 +11:00
committed by GitHub
parent fad999c580
commit a6f5c3e842
8 changed files with 229 additions and 21 deletions
+60
View File
@@ -0,0 +1,60 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { CallEmbed } from '../plugins/call';
import { useMutationObserver } from './useMutationObserver';
import { isUserId } from '../utils/matrix';
import { useCallMembers, useCallSession } from './useCall';
import { useCallJoined } from './useCallEmbed';
export const useCallSpeakers = (callEmbed: CallEmbed): Set<string> => {
const [speakers, setSpeakers] = useState(new Set<string>());
const callSession = useCallSession(callEmbed.room);
const callMembers = useCallMembers(callEmbed.room, callSession);
const joined = useCallJoined(callEmbed);
const videoContainers = useMemo(() => {
if (callMembers && joined) return callEmbed.document?.querySelectorAll('[data-video-fit]');
return undefined;
}, [callEmbed, callMembers, joined]);
const mutationObserver = useMutationObserver(
useCallback(
(mutations) => {
const s = new Set<string>();
mutations.forEach((mutation) => {
if (mutation.type !== 'attributes') return;
const el = mutation.target as HTMLElement;
const style = callEmbed.iframe.contentWindow?.getComputedStyle(el, '::before');
if (!style) return;
const tileBackgroundImage = style.getPropertyValue('background-image');
const speaking = tileBackgroundImage !== 'none';
if (!speaking) return;
const speakerId = el.querySelector('[aria-label]')?.getAttribute('aria-label');
if (speakerId && isUserId(speakerId)) {
s.add(speakerId);
}
});
setSpeakers(s);
},
[callEmbed]
)
);
useEffect(() => {
videoContainers?.forEach((element) => {
mutationObserver.observe(element, {
attributes: true,
attributeFilter: ['class', 'style'],
});
});
return () => {
mutationObserver.disconnect();
};
}, [videoContainers, mutationObserver]);
return speakers;
};
+37
View File
@@ -0,0 +1,37 @@
import { useEffect, useMemo } from 'react';
export type OnMutationCallback = (mutations: MutationRecord[]) => void;
export const getMutationRecord = (
target: Node,
mutations: MutationRecord[]
): MutationRecord | undefined => mutations.find((mutation) => mutation.target === target);
export const useMutationObserver = (
onMutationCallback: OnMutationCallback,
observeElement?: Node | null | (() => Node | null),
options?: MutationObserverInit
): MutationObserver => {
const mutationObserver = useMemo(
() => new MutationObserver(onMutationCallback),
[onMutationCallback]
);
useEffect(() => () => mutationObserver?.disconnect(), [mutationObserver]);
useEffect(() => {
const element = typeof observeElement === 'function' ? observeElement() : observeElement;
if (element) {
mutationObserver.observe(element, options);
}
return () => {
if (element) {
mutationObserver.disconnect();
}
};
}, [mutationObserver, observeElement, options]);
return mutationObserver;
};