diff --git a/src/room/MuteStates.ts b/src/room/MuteStates.ts index 2688c51e..8ec58265 100644 --- a/src/room/MuteStates.ts +++ b/src/room/MuteStates.ts @@ -14,10 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Dispatch, SetStateAction, useMemo } from "react"; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useMemo, +} from "react"; +import { IWidgetApiRequest } from "matrix-widget-api"; import { MediaDevice, useMediaDevices } from "../livekit/MediaDevicesContext"; import { useReactiveState } from "../useReactiveState"; +import { ElementWidgetActions, widget } from "../widget"; /** * If there already are this many participants in the call, we automatically mute @@ -74,5 +82,62 @@ export function useMuteStates(): MuteStates { const audio = useMuteState(devices.audioInput, () => true); const video = useMuteState(devices.videoInput, () => true); + useEffect(() => { + widget?.api.transport.send(ElementWidgetActions.DeviceMute, { + audio_enabled: audio.enabled, + video_enabled: video.enabled, + }); + }, [audio, video]); + + const onMuteStateChangeRequest = useCallback( + (ev: CustomEvent) => { + // First copy the current state into our new state. + const newState = { + audio_enabled: audio.enabled, + video_enabled: video.enabled, + }; + // Update new state if there are any requested changes from the widget action + // in `ev.detail.data`. + if ( + ev.detail.data.audio_enabled != null && + typeof ev.detail.data.audio_enabled === "boolean" + ) { + audio.setEnabled?.(ev.detail.data.audio_enabled); + newState.audio_enabled = ev.detail.data.audio_enabled; + } + if ( + ev.detail.data.video_enabled != null && + typeof ev.detail.data.video_enabled === "boolean" + ) { + video.setEnabled?.(ev.detail.data.video_enabled); + newState.video_enabled = ev.detail.data.video_enabled; + } + // Always reply with the new (now "current") state. + // This allows to also use this action to just get the unaltered current state + // by using a fromWidget request with: `ev.detail.data = {}` + widget!.api.transport.reply(ev.detail, newState); + }, + [audio, video], + ); + useEffect(() => { + // We setup a event listener for the widget action ElementWidgetActions.DeviceMute. + if (widget) { + // only setup the listener in widget mode + + widget.lazyActions.on( + ElementWidgetActions.DeviceMute, + onMuteStateChangeRequest, + ); + + return (): void => { + // return a call to `off` so that we always clean up our listener. + widget?.lazyActions.off( + ElementWidgetActions.DeviceMute, + onMuteStateChangeRequest, + ); + }; + } + }, [onMuteStateChangeRequest]); + return useMemo(() => ({ audio, video }), [audio, video]); } diff --git a/src/widget.ts b/src/widget.ts index c8d8e3c0..8c92b600 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -46,6 +46,21 @@ export enum ElementWidgetActions { // host -> Element Call telling EC to stop screen sharing, or that // the user cancelled when selecting a source after a ScreenshareRequest ScreenshareStop = "io.element.screenshare_stop", + // This can be sent as from or to widget + // fromWidget: updates the client about the current device mute state + // toWidget: the client requests a specific device mute configuration + // The reply will always be the resulting configuration + // It is possible to sent an empty configuration to retrieve the current values or + // just one of the fields to update that particular value + // An undefined field means that EC will keep the mute state as is. + // -> this will allow the client to only get the current state + // + // The data of the widget action request and the response are: + // { + // audio_enabled?: boolean, + // video_enabled?: boolean + // } + DeviceMute = "io.element.device_mute", } export interface JoinCallData { @@ -88,6 +103,7 @@ export const widget = ((): WidgetHelpers | null => { ElementWidgetActions.SpotlightLayout, ElementWidgetActions.ScreenshareStart, ElementWidgetActions.ScreenshareStop, + ElementWidgetActions.DeviceMute, ].forEach((action) => { api.on(`action:${action}`, (ev: CustomEvent) => { ev.preventDefault();