forked from github/cinny
* 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 * add joined call control bar * remove chat toggle from room header * change member speaking icon to mic * fix joined call control appear in other * show spinner on end call button * hide call statusbar for mobile view when room is selected * make call statusbar more mobile friendly * fix call status bar item align
240 lines
5.7 KiB
TypeScript
240 lines
5.7 KiB
TypeScript
import { ClientWidgetApi } from 'matrix-widget-api';
|
|
import EventEmitter from 'events';
|
|
import { CallControlState } from './CallControlState';
|
|
import { ElementMediaStateDetail, ElementMediaStatePayload, ElementWidgetActions } from './types';
|
|
|
|
export enum CallControlEvent {
|
|
StateUpdate = 'state_update',
|
|
}
|
|
|
|
export class CallControl extends EventEmitter implements CallControlState {
|
|
private state: CallControlState;
|
|
|
|
private call: ClientWidgetApi;
|
|
|
|
private iframe: HTMLIFrameElement;
|
|
|
|
private controlMutationObserver: MutationObserver;
|
|
|
|
private get document(): Document | undefined {
|
|
return this.iframe.contentDocument ?? this.iframe.contentWindow?.document;
|
|
}
|
|
|
|
private get screenshareButton(): HTMLElement | undefined {
|
|
const screenshareBtn = this.document?.querySelector(
|
|
'[data-testid="incall_screenshare"]'
|
|
) as HTMLElement | null;
|
|
|
|
return screenshareBtn ?? undefined;
|
|
}
|
|
|
|
private get settingsButton(): HTMLElement | undefined {
|
|
const leaveBtn = this.document?.querySelector('[data-testid="incall_leave"]');
|
|
|
|
const settingsButton = leaveBtn?.previousElementSibling as HTMLElement | null;
|
|
|
|
return settingsButton ?? undefined;
|
|
}
|
|
|
|
private get reactionsButton(): HTMLElement | undefined {
|
|
const reactionsButton = this.settingsButton?.previousElementSibling as HTMLElement | null;
|
|
|
|
return reactionsButton ?? undefined;
|
|
}
|
|
|
|
private get spotlightButton(): HTMLInputElement | undefined {
|
|
const spotlightButton = this.document?.querySelector(
|
|
'input[value="spotlight"]'
|
|
) as HTMLInputElement | null;
|
|
|
|
return spotlightButton ?? undefined;
|
|
}
|
|
|
|
private get gridButton(): HTMLInputElement | undefined {
|
|
const gridButton = this.document?.querySelector(
|
|
'input[value="grid"]'
|
|
) as HTMLInputElement | null;
|
|
|
|
return gridButton ?? undefined;
|
|
}
|
|
|
|
constructor(state: CallControlState, call: ClientWidgetApi, iframe: HTMLIFrameElement) {
|
|
super();
|
|
|
|
this.state = state;
|
|
this.call = call;
|
|
this.iframe = iframe;
|
|
|
|
this.controlMutationObserver = new MutationObserver(this.onControlMutation.bind(this));
|
|
}
|
|
|
|
public getState(): CallControlState {
|
|
return this.state;
|
|
}
|
|
|
|
public get microphone(): boolean {
|
|
return this.state.microphone;
|
|
}
|
|
|
|
public get video(): boolean {
|
|
return this.state.video;
|
|
}
|
|
|
|
public get sound(): boolean {
|
|
return this.state.sound;
|
|
}
|
|
|
|
public get screenshare(): boolean {
|
|
return this.state.screenshare;
|
|
}
|
|
|
|
public get spotlight(): boolean {
|
|
return this.state.spotlight;
|
|
}
|
|
|
|
public async applyState() {
|
|
await this.setMediaState({
|
|
audio_enabled: this.microphone,
|
|
video_enabled: this.video,
|
|
});
|
|
this.setSound(this.sound);
|
|
this.emitStateUpdate();
|
|
}
|
|
|
|
public startObserving() {
|
|
this.controlMutationObserver.disconnect();
|
|
|
|
const screenshareBtn = this.screenshareButton;
|
|
if (screenshareBtn) {
|
|
this.controlMutationObserver.observe(screenshareBtn, {
|
|
attributes: true,
|
|
attributeFilter: ['data-kind'],
|
|
});
|
|
}
|
|
const spotlightBtn = this.spotlightButton;
|
|
if (spotlightBtn) {
|
|
this.controlMutationObserver.observe(spotlightBtn, {
|
|
attributes: true,
|
|
});
|
|
}
|
|
|
|
this.onControlMutation();
|
|
}
|
|
|
|
public applySound() {
|
|
this.setSound(this.sound);
|
|
}
|
|
|
|
private setMediaState(state: ElementMediaStatePayload) {
|
|
return this.call.transport.send(ElementWidgetActions.DeviceMute, state);
|
|
}
|
|
|
|
private setSound(sound: boolean): void {
|
|
const callDocument = this.iframe.contentDocument ?? this.iframe.contentWindow?.document;
|
|
if (callDocument) {
|
|
callDocument.querySelectorAll('audio').forEach((el) => {
|
|
// eslint-disable-next-line no-param-reassign
|
|
el.muted = !sound;
|
|
});
|
|
}
|
|
}
|
|
|
|
public onMediaState(evt: CustomEvent<ElementMediaStateDetail>) {
|
|
const { data } = evt.detail;
|
|
if (!data) return;
|
|
|
|
const state = new CallControlState(
|
|
data.audio_enabled ?? this.microphone,
|
|
data.video_enabled ?? this.video,
|
|
this.sound,
|
|
this.screenshare,
|
|
this.spotlight
|
|
);
|
|
|
|
this.state = state;
|
|
this.emitStateUpdate();
|
|
|
|
if (this.microphone && !this.sound) {
|
|
this.toggleSound();
|
|
}
|
|
}
|
|
|
|
public onControlMutation() {
|
|
const screenshare: boolean = this.screenshareButton?.getAttribute('data-kind') === 'primary';
|
|
const spotlight: boolean = this.spotlightButton?.checked ?? false;
|
|
|
|
this.state = new CallControlState(
|
|
this.microphone,
|
|
this.video,
|
|
this.sound,
|
|
screenshare,
|
|
spotlight
|
|
);
|
|
this.emitStateUpdate();
|
|
}
|
|
|
|
public toggleMicrophone() {
|
|
const payload: ElementMediaStatePayload = {
|
|
audio_enabled: !this.microphone,
|
|
video_enabled: this.video,
|
|
};
|
|
return this.setMediaState(payload);
|
|
}
|
|
|
|
public toggleVideo() {
|
|
const payload: ElementMediaStatePayload = {
|
|
audio_enabled: this.microphone,
|
|
video_enabled: !this.video,
|
|
};
|
|
return this.setMediaState(payload);
|
|
}
|
|
|
|
public toggleSound() {
|
|
const sound = !this.sound;
|
|
|
|
this.setSound(sound);
|
|
|
|
const state = new CallControlState(
|
|
this.microphone,
|
|
this.video,
|
|
sound,
|
|
this.screenshare,
|
|
this.spotlight
|
|
);
|
|
this.state = state;
|
|
this.emitStateUpdate();
|
|
|
|
if (!this.sound && this.microphone) {
|
|
this.toggleMicrophone();
|
|
}
|
|
}
|
|
|
|
public toggleScreenshare() {
|
|
this.screenshareButton?.click();
|
|
}
|
|
|
|
public toggleSpotlight() {
|
|
if (this.spotlight) {
|
|
this.gridButton?.click();
|
|
return;
|
|
}
|
|
this.spotlightButton?.click();
|
|
}
|
|
|
|
public toggleReactions() {
|
|
this.reactionsButton?.click();
|
|
}
|
|
|
|
public toggleSettings() {
|
|
this.settingsButton?.click();
|
|
}
|
|
|
|
public dispose() {
|
|
this.controlMutationObserver.disconnect();
|
|
}
|
|
|
|
private emitStateUpdate() {
|
|
this.emit(CallControlEvent.StateUpdate);
|
|
}
|
|
}
|