Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 8e1ccba

Browse files
committed
Add voice channel store
Signed-off-by: Robin Townsend <[email protected]>
1 parent 5f31f27 commit 8e1ccba

File tree

1 file changed

+246
-0
lines changed

1 file changed

+246
-0
lines changed

src/stores/VoiceChannelStore.ts

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { EventEmitter } from "events";
18+
import { ClientWidgetApi, IWidgetApiRequest } from "matrix-widget-api";
19+
20+
import { ElementWidgetActions } from "./widgets/ElementWidgetActions";
21+
import { WidgetMessagingStore } from "./widgets/WidgetMessagingStore";
22+
import { getVoiceChannel } from "../utils/VoiceChannelUtils";
23+
import { timeout } from "../utils/promise";
24+
import WidgetUtils from "../utils/WidgetUtils";
25+
26+
export enum VoiceChannelEvent {
27+
Connect = "connect",
28+
Disconnect = "disconnect",
29+
Participants = "participants",
30+
MuteAudio = "mute_audio",
31+
UnmuteAudio = "unmute_audio",
32+
MuteVideo = "mute_video",
33+
UnmuteVideo = "unmute_video",
34+
}
35+
36+
export interface IJitsiParticipant {
37+
avatarURL: string;
38+
displayName: string;
39+
formattedDisplayName: string;
40+
participantId: string;
41+
}
42+
43+
/*
44+
* Holds information about the currently active voice channel.
45+
*/
46+
export default class VoiceChannelStore extends EventEmitter {
47+
private static _instance: VoiceChannelStore;
48+
private static TIMEOUT = 8000;
49+
50+
public static get instance(): VoiceChannelStore {
51+
if (!VoiceChannelStore._instance) {
52+
VoiceChannelStore._instance = new VoiceChannelStore();
53+
}
54+
return VoiceChannelStore._instance;
55+
}
56+
57+
private activeChannel: ClientWidgetApi;
58+
private _roomId: string;
59+
private _participants: IJitsiParticipant[];
60+
private _audioMuted: boolean;
61+
private _videoMuted: boolean;
62+
63+
public get roomId(): string {
64+
return this._roomId;
65+
}
66+
67+
public get participants(): IJitsiParticipant[] {
68+
return this._participants;
69+
}
70+
71+
public get audioMuted(): boolean {
72+
return this._audioMuted;
73+
}
74+
75+
public get videoMuted(): boolean {
76+
return this._videoMuted;
77+
}
78+
79+
public connect = async (roomId: string) => {
80+
if (this.activeChannel) await this.disconnect();
81+
82+
const jitsi = getVoiceChannel(roomId);
83+
if (!jitsi) throw new Error(`No voice channel in room ${roomId}`);
84+
85+
const messaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(jitsi));
86+
if (!messaging) throw new Error(`Failed to bind voice channel in room ${roomId}`);
87+
88+
this.activeChannel = messaging;
89+
this._roomId = roomId;
90+
91+
// Participant data and mute state will come down the event pipeline very quickly,
92+
// so prepare in advance
93+
messaging.on(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants);
94+
messaging.on(`action:${ElementWidgetActions.MuteAudio}`, this.onMuteAudio);
95+
messaging.on(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio);
96+
messaging.on(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo);
97+
messaging.on(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo);
98+
99+
// Actually perform the join
100+
const waitForJoin = new Promise<void>(resolve =>
101+
messaging.once(`action:${ElementWidgetActions.JoinCall}`, (ev: CustomEvent<IWidgetApiRequest>) => {
102+
this.ack(ev);
103+
resolve();
104+
}),
105+
);
106+
messaging.transport.send(ElementWidgetActions.JoinCall, {});
107+
try {
108+
await this.timeout(waitForJoin);
109+
} catch (e) {
110+
// If it timed out, clean up our advance preparations
111+
this.activeChannel = null;
112+
this._roomId = null;
113+
114+
messaging.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants);
115+
messaging.off(`action:${ElementWidgetActions.MuteAudio}`, this.onMuteAudio);
116+
messaging.off(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio);
117+
messaging.off(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo);
118+
messaging.off(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo);
119+
120+
throw e;
121+
}
122+
123+
messaging.once(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
124+
125+
this.emit(VoiceChannelEvent.Connect);
126+
};
127+
128+
public disconnect = async () => {
129+
this.assertConnected();
130+
131+
const waitForHangup = new Promise<void>(resolve =>
132+
this.activeChannel.once(`action:${ElementWidgetActions.HangupCall}`, () => resolve()),
133+
);
134+
this.activeChannel.transport.send(ElementWidgetActions.HangupCall, {});
135+
await this.timeout(waitForHangup);
136+
137+
// onHangup cleans up for us
138+
};
139+
140+
public muteAudio = async () => {
141+
this.assertConnected();
142+
143+
const waitForMute = new Promise<void>(resolve =>
144+
this.activeChannel.once(`action:${ElementWidgetActions.MuteAudio}`, () => resolve()),
145+
);
146+
this.activeChannel.transport.send(ElementWidgetActions.MuteAudio, {});
147+
await this.timeout(waitForMute);
148+
};
149+
150+
public unmuteAudio = async () => {
151+
this.assertConnected();
152+
153+
const waitForUnmute = new Promise<void>(resolve =>
154+
this.activeChannel.once(`action:${ElementWidgetActions.UnmuteAudio}`, () => resolve()),
155+
);
156+
this.activeChannel.transport.send(ElementWidgetActions.UnmuteAudio, {});
157+
await this.timeout(waitForUnmute);
158+
};
159+
160+
public muteVideo = async () => {
161+
this.assertConnected();
162+
163+
const waitForMute = new Promise<void>(resolve =>
164+
this.activeChannel.once(`action:${ElementWidgetActions.MuteVideo}`, () => resolve()),
165+
);
166+
this.activeChannel.transport.send(ElementWidgetActions.MuteVideo, {});
167+
await this.timeout(waitForMute);
168+
};
169+
170+
public unmuteVideo = async () => {
171+
this.assertConnected();
172+
173+
const waitForUnmute = new Promise<void>(resolve =>
174+
this.activeChannel.once(`action:${ElementWidgetActions.UnmuteVideo}`, () => resolve()),
175+
);
176+
this.activeChannel.transport.send(ElementWidgetActions.UnmuteVideo, {});
177+
await this.timeout(waitForUnmute);
178+
};
179+
180+
private assertConnected = () => {
181+
if (!this.activeChannel) throw new Error("Not connected to any voice channel");
182+
};
183+
184+
private timeout = async (wait: Promise<void>) => {
185+
if (await timeout(wait, false, VoiceChannelStore.TIMEOUT) === false) {
186+
throw new Error("Communication with voice channel timed out");
187+
}
188+
};
189+
190+
private ack = (ev: CustomEvent<IWidgetApiRequest>) => {
191+
this.activeChannel.transport.reply(ev.detail, {});
192+
};
193+
194+
private onHangup = (ev: CustomEvent<IWidgetApiRequest>) => {
195+
this.ack(ev);
196+
197+
this.activeChannel.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants);
198+
this.activeChannel.off(`action:${ElementWidgetActions.MuteAudio}`, this.onMuteAudio);
199+
this.activeChannel.off(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio);
200+
this.activeChannel.off(`action:${ElementWidgetActions.MuteVideo}`, this.onMuteVideo);
201+
this.activeChannel.off(`action:${ElementWidgetActions.UnmuteVideo}`, this.onUnmuteVideo);
202+
203+
this.activeChannel = null;
204+
this._roomId = null;
205+
this._participants = null;
206+
this._audioMuted = null;
207+
this._videoMuted = null;
208+
209+
this.emit(VoiceChannelEvent.Disconnect);
210+
};
211+
212+
private onParticipants = (ev: CustomEvent<IWidgetApiRequest>) => {
213+
this.ack(ev);
214+
215+
this._participants = ev.detail.data.participants as IJitsiParticipant[];
216+
this.emit(VoiceChannelEvent.Participants, ev.detail.data.participants);
217+
};
218+
219+
private onMuteAudio = (ev: CustomEvent<IWidgetApiRequest>) => {
220+
this.ack(ev);
221+
222+
this._audioMuted = true;
223+
this.emit(VoiceChannelEvent.MuteAudio);
224+
};
225+
226+
private onUnmuteAudio = (ev: CustomEvent<IWidgetApiRequest>) => {
227+
this.ack(ev);
228+
229+
this._audioMuted = false;
230+
this.emit(VoiceChannelEvent.UnmuteAudio);
231+
};
232+
233+
private onMuteVideo = (ev: CustomEvent<IWidgetApiRequest>) => {
234+
this.ack(ev);
235+
236+
this._videoMuted = true;
237+
this.emit(VoiceChannelEvent.MuteVideo);
238+
};
239+
240+
private onUnmuteVideo = (ev: CustomEvent<IWidgetApiRequest>) => {
241+
this.ack(ev);
242+
243+
this._videoMuted = false;
244+
this.emit(VoiceChannelEvent.UnmuteVideo);
245+
};
246+
}

0 commit comments

Comments
 (0)