@unwanted/matrix-sdk-mini 34.12.0-1 → 34.12.0-2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (185) hide show
  1. package/git-revision.txt +1 -1
  2. package/lib/@types/event.d.ts +0 -19
  3. package/lib/@types/event.d.ts.map +1 -1
  4. package/lib/@types/event.js.map +1 -1
  5. package/lib/client.d.ts +2 -50
  6. package/lib/client.d.ts.map +1 -1
  7. package/lib/client.js +391 -501
  8. package/lib/client.js.map +1 -1
  9. package/lib/embedded.d.ts.map +1 -1
  10. package/lib/embedded.js +0 -1
  11. package/lib/embedded.js.map +1 -1
  12. package/lib/matrix.d.ts +0 -6
  13. package/lib/matrix.d.ts.map +1 -1
  14. package/lib/matrix.js +1 -5
  15. package/lib/matrix.js.map +1 -1
  16. package/package.json +1 -1
  17. package/src/@types/event.ts +2 -36
  18. package/src/client.ts +1 -150
  19. package/src/embedded.ts +0 -2
  20. package/src/matrix.ts +0 -13
  21. package/lib/matrixrtc/CallMembership.d.ts +0 -66
  22. package/lib/matrixrtc/CallMembership.d.ts.map +0 -1
  23. package/lib/matrixrtc/CallMembership.js +0 -197
  24. package/lib/matrixrtc/CallMembership.js.map +0 -1
  25. package/lib/matrixrtc/LivekitFocus.d.ts +0 -16
  26. package/lib/matrixrtc/LivekitFocus.d.ts.map +0 -1
  27. package/lib/matrixrtc/LivekitFocus.js +0 -20
  28. package/lib/matrixrtc/LivekitFocus.js.map +0 -1
  29. package/lib/matrixrtc/MatrixRTCSession.d.ts +0 -295
  30. package/lib/matrixrtc/MatrixRTCSession.d.ts.map +0 -1
  31. package/lib/matrixrtc/MatrixRTCSession.js +0 -1043
  32. package/lib/matrixrtc/MatrixRTCSession.js.map +0 -1
  33. package/lib/matrixrtc/MatrixRTCSessionManager.d.ts +0 -40
  34. package/lib/matrixrtc/MatrixRTCSessionManager.d.ts.map +0 -1
  35. package/lib/matrixrtc/MatrixRTCSessionManager.js +0 -146
  36. package/lib/matrixrtc/MatrixRTCSessionManager.js.map +0 -1
  37. package/lib/matrixrtc/focus.d.ts +0 -10
  38. package/lib/matrixrtc/focus.d.ts.map +0 -1
  39. package/lib/matrixrtc/focus.js +0 -1
  40. package/lib/matrixrtc/focus.js.map +0 -1
  41. package/lib/matrixrtc/index.d.ts +0 -7
  42. package/lib/matrixrtc/index.d.ts.map +0 -1
  43. package/lib/matrixrtc/index.js +0 -21
  44. package/lib/matrixrtc/index.js.map +0 -1
  45. package/lib/matrixrtc/types.d.ts +0 -19
  46. package/lib/matrixrtc/types.d.ts.map +0 -1
  47. package/lib/matrixrtc/types.js +0 -1
  48. package/lib/matrixrtc/types.js.map +0 -1
  49. package/lib/webrtc/audioContext.d.ts +0 -15
  50. package/lib/webrtc/audioContext.d.ts.map +0 -1
  51. package/lib/webrtc/audioContext.js +0 -46
  52. package/lib/webrtc/audioContext.js.map +0 -1
  53. package/lib/webrtc/call.d.ts +0 -560
  54. package/lib/webrtc/call.d.ts.map +0 -1
  55. package/lib/webrtc/call.js +0 -2541
  56. package/lib/webrtc/call.js.map +0 -1
  57. package/lib/webrtc/callEventHandler.d.ts +0 -37
  58. package/lib/webrtc/callEventHandler.d.ts.map +0 -1
  59. package/lib/webrtc/callEventHandler.js +0 -344
  60. package/lib/webrtc/callEventHandler.js.map +0 -1
  61. package/lib/webrtc/callEventTypes.d.ts +0 -73
  62. package/lib/webrtc/callEventTypes.d.ts.map +0 -1
  63. package/lib/webrtc/callEventTypes.js +0 -13
  64. package/lib/webrtc/callEventTypes.js.map +0 -1
  65. package/lib/webrtc/callFeed.d.ts +0 -128
  66. package/lib/webrtc/callFeed.d.ts.map +0 -1
  67. package/lib/webrtc/callFeed.js +0 -289
  68. package/lib/webrtc/callFeed.js.map +0 -1
  69. package/lib/webrtc/groupCall.d.ts +0 -323
  70. package/lib/webrtc/groupCall.d.ts.map +0 -1
  71. package/lib/webrtc/groupCall.js +0 -1337
  72. package/lib/webrtc/groupCall.js.map +0 -1
  73. package/lib/webrtc/groupCallEventHandler.d.ts +0 -31
  74. package/lib/webrtc/groupCallEventHandler.d.ts.map +0 -1
  75. package/lib/webrtc/groupCallEventHandler.js +0 -178
  76. package/lib/webrtc/groupCallEventHandler.js.map +0 -1
  77. package/lib/webrtc/mediaHandler.d.ts +0 -89
  78. package/lib/webrtc/mediaHandler.d.ts.map +0 -1
  79. package/lib/webrtc/mediaHandler.js +0 -437
  80. package/lib/webrtc/mediaHandler.js.map +0 -1
  81. package/lib/webrtc/stats/callFeedStatsReporter.d.ts +0 -8
  82. package/lib/webrtc/stats/callFeedStatsReporter.d.ts.map +0 -1
  83. package/lib/webrtc/stats/callFeedStatsReporter.js +0 -82
  84. package/lib/webrtc/stats/callFeedStatsReporter.js.map +0 -1
  85. package/lib/webrtc/stats/callStatsReportGatherer.d.ts +0 -25
  86. package/lib/webrtc/stats/callStatsReportGatherer.d.ts.map +0 -1
  87. package/lib/webrtc/stats/callStatsReportGatherer.js +0 -199
  88. package/lib/webrtc/stats/callStatsReportGatherer.js.map +0 -1
  89. package/lib/webrtc/stats/callStatsReportSummary.d.ts +0 -17
  90. package/lib/webrtc/stats/callStatsReportSummary.d.ts.map +0 -1
  91. package/lib/webrtc/stats/callStatsReportSummary.js +0 -1
  92. package/lib/webrtc/stats/callStatsReportSummary.js.map +0 -1
  93. package/lib/webrtc/stats/connectionStats.d.ts +0 -28
  94. package/lib/webrtc/stats/connectionStats.d.ts.map +0 -1
  95. package/lib/webrtc/stats/connectionStats.js +0 -26
  96. package/lib/webrtc/stats/connectionStats.js.map +0 -1
  97. package/lib/webrtc/stats/connectionStatsBuilder.d.ts +0 -5
  98. package/lib/webrtc/stats/connectionStatsBuilder.d.ts.map +0 -1
  99. package/lib/webrtc/stats/connectionStatsBuilder.js +0 -27
  100. package/lib/webrtc/stats/connectionStatsBuilder.js.map +0 -1
  101. package/lib/webrtc/stats/connectionStatsReportBuilder.d.ts +0 -7
  102. package/lib/webrtc/stats/connectionStatsReportBuilder.d.ts.map +0 -1
  103. package/lib/webrtc/stats/connectionStatsReportBuilder.js +0 -121
  104. package/lib/webrtc/stats/connectionStatsReportBuilder.js.map +0 -1
  105. package/lib/webrtc/stats/groupCallStats.d.ts +0 -22
  106. package/lib/webrtc/stats/groupCallStats.d.ts.map +0 -1
  107. package/lib/webrtc/stats/groupCallStats.js +0 -78
  108. package/lib/webrtc/stats/groupCallStats.js.map +0 -1
  109. package/lib/webrtc/stats/media/mediaSsrcHandler.d.ts +0 -10
  110. package/lib/webrtc/stats/media/mediaSsrcHandler.d.ts.map +0 -1
  111. package/lib/webrtc/stats/media/mediaSsrcHandler.js +0 -57
  112. package/lib/webrtc/stats/media/mediaSsrcHandler.js.map +0 -1
  113. package/lib/webrtc/stats/media/mediaTrackHandler.d.ts +0 -12
  114. package/lib/webrtc/stats/media/mediaTrackHandler.d.ts.map +0 -1
  115. package/lib/webrtc/stats/media/mediaTrackHandler.js +0 -62
  116. package/lib/webrtc/stats/media/mediaTrackHandler.js.map +0 -1
  117. package/lib/webrtc/stats/media/mediaTrackStats.d.ts +0 -86
  118. package/lib/webrtc/stats/media/mediaTrackStats.d.ts.map +0 -1
  119. package/lib/webrtc/stats/media/mediaTrackStats.js +0 -142
  120. package/lib/webrtc/stats/media/mediaTrackStats.js.map +0 -1
  121. package/lib/webrtc/stats/media/mediaTrackStatsHandler.d.ts +0 -22
  122. package/lib/webrtc/stats/media/mediaTrackStatsHandler.d.ts.map +0 -1
  123. package/lib/webrtc/stats/media/mediaTrackStatsHandler.js +0 -76
  124. package/lib/webrtc/stats/media/mediaTrackStatsHandler.js.map +0 -1
  125. package/lib/webrtc/stats/statsReport.d.ts +0 -99
  126. package/lib/webrtc/stats/statsReport.d.ts.map +0 -1
  127. package/lib/webrtc/stats/statsReport.js +0 -32
  128. package/lib/webrtc/stats/statsReport.js.map +0 -1
  129. package/lib/webrtc/stats/statsReportEmitter.d.ts +0 -15
  130. package/lib/webrtc/stats/statsReportEmitter.d.ts.map +0 -1
  131. package/lib/webrtc/stats/statsReportEmitter.js +0 -33
  132. package/lib/webrtc/stats/statsReportEmitter.js.map +0 -1
  133. package/lib/webrtc/stats/summaryStatsReportGatherer.d.ts +0 -16
  134. package/lib/webrtc/stats/summaryStatsReportGatherer.d.ts.map +0 -1
  135. package/lib/webrtc/stats/summaryStatsReportGatherer.js +0 -116
  136. package/lib/webrtc/stats/summaryStatsReportGatherer.js.map +0 -1
  137. package/lib/webrtc/stats/trackStatsBuilder.d.ts +0 -19
  138. package/lib/webrtc/stats/trackStatsBuilder.d.ts.map +0 -1
  139. package/lib/webrtc/stats/trackStatsBuilder.js +0 -168
  140. package/lib/webrtc/stats/trackStatsBuilder.js.map +0 -1
  141. package/lib/webrtc/stats/transportStats.d.ts +0 -11
  142. package/lib/webrtc/stats/transportStats.d.ts.map +0 -1
  143. package/lib/webrtc/stats/transportStats.js +0 -1
  144. package/lib/webrtc/stats/transportStats.js.map +0 -1
  145. package/lib/webrtc/stats/transportStatsBuilder.d.ts +0 -5
  146. package/lib/webrtc/stats/transportStatsBuilder.d.ts.map +0 -1
  147. package/lib/webrtc/stats/transportStatsBuilder.js +0 -34
  148. package/lib/webrtc/stats/transportStatsBuilder.js.map +0 -1
  149. package/lib/webrtc/stats/valueFormatter.d.ts +0 -4
  150. package/lib/webrtc/stats/valueFormatter.d.ts.map +0 -1
  151. package/lib/webrtc/stats/valueFormatter.js +0 -25
  152. package/lib/webrtc/stats/valueFormatter.js.map +0 -1
  153. package/src/matrixrtc/CallMembership.ts +0 -247
  154. package/src/matrixrtc/LivekitFocus.ts +0 -39
  155. package/src/matrixrtc/MatrixRTCSession.ts +0 -1319
  156. package/src/matrixrtc/MatrixRTCSessionManager.ts +0 -166
  157. package/src/matrixrtc/focus.ts +0 -25
  158. package/src/matrixrtc/index.ts +0 -22
  159. package/src/matrixrtc/types.ts +0 -36
  160. package/src/webrtc/audioContext.ts +0 -44
  161. package/src/webrtc/call.ts +0 -3074
  162. package/src/webrtc/callEventHandler.ts +0 -425
  163. package/src/webrtc/callEventTypes.ts +0 -93
  164. package/src/webrtc/callFeed.ts +0 -364
  165. package/src/webrtc/groupCall.ts +0 -1735
  166. package/src/webrtc/groupCallEventHandler.ts +0 -234
  167. package/src/webrtc/mediaHandler.ts +0 -484
  168. package/src/webrtc/stats/callFeedStatsReporter.ts +0 -94
  169. package/src/webrtc/stats/callStatsReportGatherer.ts +0 -219
  170. package/src/webrtc/stats/callStatsReportSummary.ts +0 -30
  171. package/src/webrtc/stats/connectionStats.ts +0 -47
  172. package/src/webrtc/stats/connectionStatsBuilder.ts +0 -28
  173. package/src/webrtc/stats/connectionStatsReportBuilder.ts +0 -140
  174. package/src/webrtc/stats/groupCallStats.ts +0 -93
  175. package/src/webrtc/stats/media/mediaSsrcHandler.ts +0 -57
  176. package/src/webrtc/stats/media/mediaTrackHandler.ts +0 -76
  177. package/src/webrtc/stats/media/mediaTrackStats.ts +0 -176
  178. package/src/webrtc/stats/media/mediaTrackStatsHandler.ts +0 -90
  179. package/src/webrtc/stats/statsReport.ts +0 -133
  180. package/src/webrtc/stats/statsReportEmitter.ts +0 -49
  181. package/src/webrtc/stats/summaryStatsReportGatherer.ts +0 -148
  182. package/src/webrtc/stats/trackStatsBuilder.ts +0 -207
  183. package/src/webrtc/stats/transportStats.ts +0 -26
  184. package/src/webrtc/stats/transportStatsBuilder.ts +0 -48
  185. package/src/webrtc/stats/valueFormatter.ts +0 -27
@@ -1,1735 +0,0 @@
1
- import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
2
- import { CallFeed, SPEAKING_THRESHOLD } from "./callFeed.ts";
3
- import { MatrixClient, IMyDevice } from "../client.ts";
4
- import {
5
- CallErrorCode,
6
- CallEvent,
7
- CallEventHandlerMap,
8
- CallState,
9
- genCallID,
10
- MatrixCall,
11
- setTracksEnabled,
12
- createNewMatrixCall,
13
- CallError,
14
- } from "./call.ts";
15
- import { RoomMember } from "../models/room-member.ts";
16
- import { Room } from "../models/room.ts";
17
- import { RoomStateEvent } from "../models/room-state.ts";
18
- import { logger } from "../logger.ts";
19
- import { ReEmitter } from "../ReEmitter.ts";
20
- import { SDPStreamMetadataPurpose } from "./callEventTypes.ts";
21
- import { MatrixEvent } from "../models/event.ts";
22
- import { EventType } from "../@types/event.ts";
23
- import { CallEventHandlerEvent } from "./callEventHandler.ts";
24
- import { GroupCallEventHandlerEvent } from "./groupCallEventHandler.ts";
25
- import { IScreensharingOpts } from "./mediaHandler.ts";
26
- import { mapsEqual } from "../utils.ts";
27
- import { GroupCallStats } from "./stats/groupCallStats.ts";
28
- import {
29
- ByteSentStatsReport,
30
- CallFeedReport,
31
- ConnectionStatsReport,
32
- StatsReport,
33
- SummaryStatsReport,
34
- } from "./stats/statsReport.ts";
35
- import { SummaryStatsReportGatherer } from "./stats/summaryStatsReportGatherer.ts";
36
- import { CallFeedStatsReporter } from "./stats/callFeedStatsReporter.ts";
37
- import { KnownMembership } from "../@types/membership.ts";
38
- import { CallMembershipData } from "../matrixrtc/CallMembership.ts";
39
-
40
- export enum GroupCallIntent {
41
- Ring = "m.ring",
42
- Prompt = "m.prompt",
43
- Room = "m.room",
44
- }
45
-
46
- export enum GroupCallType {
47
- Video = "m.video",
48
- Voice = "m.voice",
49
- }
50
-
51
- export enum GroupCallTerminationReason {
52
- CallEnded = "call_ended",
53
- }
54
-
55
- export type CallsByUserAndDevice = Map<string, Map<string, MatrixCall>>;
56
-
57
- /**
58
- * Because event names are just strings, they do need
59
- * to be unique over all event types of event emitter.
60
- * Some objects could emit more then one set of events.
61
- */
62
- export enum GroupCallEvent {
63
- GroupCallStateChanged = "group_call_state_changed",
64
- ActiveSpeakerChanged = "active_speaker_changed",
65
- CallsChanged = "calls_changed",
66
- UserMediaFeedsChanged = "user_media_feeds_changed",
67
- ScreenshareFeedsChanged = "screenshare_feeds_changed",
68
- LocalScreenshareStateChanged = "local_screenshare_state_changed",
69
- LocalMuteStateChanged = "local_mute_state_changed",
70
- ParticipantsChanged = "participants_changed",
71
- Error = "group_call_error",
72
- }
73
-
74
- export type GroupCallEventHandlerMap = {
75
- [GroupCallEvent.GroupCallStateChanged]: (newState: GroupCallState, oldState: GroupCallState) => void;
76
- [GroupCallEvent.ActiveSpeakerChanged]: (activeSpeaker: CallFeed | undefined) => void;
77
- [GroupCallEvent.CallsChanged]: (calls: CallsByUserAndDevice) => void;
78
- [GroupCallEvent.UserMediaFeedsChanged]: (feeds: CallFeed[]) => void;
79
- [GroupCallEvent.ScreenshareFeedsChanged]: (feeds: CallFeed[]) => void;
80
- [GroupCallEvent.LocalScreenshareStateChanged]: (
81
- isScreensharing: boolean,
82
- feed?: CallFeed,
83
- sourceId?: string,
84
- ) => void;
85
- [GroupCallEvent.LocalMuteStateChanged]: (audioMuted: boolean, videoMuted: boolean) => void;
86
- [GroupCallEvent.ParticipantsChanged]: (participants: Map<RoomMember, Map<string, ParticipantState>>) => void;
87
- /**
88
- * Fires whenever an error occurs when call.js encounters an issue with setting up the call.
89
- * <p>
90
- * The error given will have a code equal to either `MatrixCall.ERR_LOCAL_OFFER_FAILED` or
91
- * `MatrixCall.ERR_NO_USER_MEDIA`. `ERR_LOCAL_OFFER_FAILED` is emitted when the local client
92
- * fails to create an offer. `ERR_NO_USER_MEDIA` is emitted when the user has denied access
93
- * to their audio/video hardware.
94
- * @param error - The error raised by MatrixCall.
95
- * @example
96
- * ```
97
- * matrixCall.on("error", function(err){
98
- * console.error(err.code, err);
99
- * });
100
- * ```
101
- */
102
- [GroupCallEvent.Error]: (error: GroupCallError) => void;
103
- };
104
-
105
- export enum GroupCallStatsReportEvent {
106
- ConnectionStats = "GroupCall.connection_stats",
107
- ByteSentStats = "GroupCall.byte_sent_stats",
108
- SummaryStats = "GroupCall.summary_stats",
109
- CallFeedStats = "GroupCall.call_feed_stats",
110
- }
111
-
112
- /**
113
- * The final report-events that get consumed by client.
114
- */
115
- export type GroupCallStatsReportEventHandlerMap = {
116
- [GroupCallStatsReportEvent.ConnectionStats]: (report: GroupCallStatsReport<ConnectionStatsReport>) => void;
117
- [GroupCallStatsReportEvent.ByteSentStats]: (report: GroupCallStatsReport<ByteSentStatsReport>) => void;
118
- [GroupCallStatsReportEvent.SummaryStats]: (report: GroupCallStatsReport<SummaryStatsReport>) => void;
119
- [GroupCallStatsReportEvent.CallFeedStats]: (report: GroupCallStatsReport<CallFeedReport>) => void;
120
- };
121
-
122
- export enum GroupCallErrorCode {
123
- NoUserMedia = "no_user_media",
124
- UnknownDevice = "unknown_device",
125
- PlaceCallFailed = "place_call_failed",
126
- }
127
-
128
- export interface GroupCallStatsReport<
129
- T extends ConnectionStatsReport | ByteSentStatsReport | SummaryStatsReport | CallFeedReport,
130
- > {
131
- report: T;
132
- }
133
-
134
- export class GroupCallError extends Error {
135
- public code: string;
136
-
137
- public constructor(code: GroupCallErrorCode, msg: string, err?: Error) {
138
- // Still don't think there's any way to have proper nested errors
139
- if (err) {
140
- super(msg + ": " + err);
141
- } else {
142
- super(msg);
143
- }
144
-
145
- this.code = code;
146
- }
147
- }
148
-
149
- export class GroupCallUnknownDeviceError extends GroupCallError {
150
- public constructor(public userId: string) {
151
- super(GroupCallErrorCode.UnknownDevice, "No device found for " + userId);
152
- }
153
- }
154
-
155
- export class OtherUserSpeakingError extends Error {
156
- public constructor() {
157
- super("Cannot unmute: another user is speaking");
158
- }
159
- }
160
-
161
- export interface IGroupCallDataChannelOptions {
162
- ordered: boolean;
163
- maxPacketLifeTime: number;
164
- maxRetransmits: number;
165
- protocol: string;
166
- }
167
-
168
- export interface IGroupCallRoomState {
169
- "m.intent": GroupCallIntent;
170
- "m.type": GroupCallType;
171
- "m.terminated"?: GroupCallTerminationReason;
172
- "io.element.ptt"?: boolean;
173
- // TODO: Specify data-channels
174
- "dataChannelsEnabled"?: boolean;
175
- "dataChannelOptions"?: IGroupCallDataChannelOptions;
176
-
177
- "io.element.livekit_service_url"?: string;
178
- }
179
-
180
- export interface IGroupCallRoomMemberFeed {
181
- purpose: SDPStreamMetadataPurpose;
182
- }
183
-
184
- export interface IGroupCallRoomMemberDevice {
185
- device_id: string;
186
- session_id: string;
187
- expires_ts: number;
188
- feeds: IGroupCallRoomMemberFeed[];
189
- }
190
-
191
- export interface IGroupCallRoomMemberCallState {
192
- "m.call_id": string;
193
- "m.foci"?: string[];
194
- "m.devices": IGroupCallRoomMemberDevice[];
195
- }
196
-
197
- export interface IGroupCallRoomMemberState {
198
- "m.calls": IGroupCallRoomMemberCallState[];
199
- }
200
-
201
- // XXX: this hasn't made it into the MSC yet
202
- export interface ExperimentalGroupCallRoomMemberState {
203
- memberships: CallMembershipData[];
204
- }
205
-
206
- export enum GroupCallState {
207
- LocalCallFeedUninitialized = "local_call_feed_uninitialized",
208
- InitializingLocalCallFeed = "initializing_local_call_feed",
209
- LocalCallFeedInitialized = "local_call_feed_initialized",
210
- Entered = "entered",
211
- Ended = "ended",
212
- }
213
-
214
- export interface ParticipantState {
215
- sessionId: string;
216
- screensharing: boolean;
217
- }
218
-
219
- interface ICallHandlers {
220
- onCallFeedsChanged: (feeds: CallFeed[]) => void;
221
- onCallStateChanged: (state: CallState, oldState: CallState | undefined) => void;
222
- onCallHangup: (call: MatrixCall) => void;
223
- onCallReplaced: (newCall: MatrixCall) => void;
224
- }
225
-
226
- const DEVICE_TIMEOUT = 1000 * 60 * 60; // 1 hour
227
-
228
- function getCallUserId(call: MatrixCall): string | null {
229
- return call.getOpponentMember()?.userId || call.invitee || null;
230
- }
231
-
232
- export class GroupCall extends TypedEventEmitter<
233
- GroupCallEvent | CallEvent | GroupCallStatsReportEvent,
234
- GroupCallEventHandlerMap & CallEventHandlerMap & GroupCallStatsReportEventHandlerMap
235
- > {
236
- // Config
237
- public activeSpeakerInterval = 1000;
238
- public retryCallInterval = 5000;
239
- public participantTimeout = 1000 * 15;
240
- public pttMaxTransmitTime = 1000 * 20;
241
-
242
- public activeSpeaker?: CallFeed;
243
- public localCallFeed?: CallFeed;
244
- public localScreenshareFeed?: CallFeed;
245
- public localDesktopCapturerSourceId?: string;
246
- public readonly userMediaFeeds: CallFeed[] = [];
247
- public readonly screenshareFeeds: CallFeed[] = [];
248
- public groupCallId: string;
249
- public readonly allowCallWithoutVideoAndAudio: boolean;
250
-
251
- private readonly calls = new Map<string, Map<string, MatrixCall>>(); // user_id -> device_id -> MatrixCall
252
- private callHandlers = new Map<string, Map<string, ICallHandlers>>(); // user_id -> device_id -> ICallHandlers
253
- private activeSpeakerLoopInterval?: ReturnType<typeof setTimeout>;
254
- private retryCallLoopInterval?: ReturnType<typeof setTimeout>;
255
- private retryCallCounts: Map<string, Map<string, number>> = new Map(); // user_id -> device_id -> count
256
- private reEmitter: ReEmitter;
257
- private transmitTimer: ReturnType<typeof setTimeout> | null = null;
258
- private participantsExpirationTimer: ReturnType<typeof setTimeout> | null = null;
259
- private resendMemberStateTimer: ReturnType<typeof setInterval> | null = null;
260
- private initWithAudioMuted = false;
261
- private initWithVideoMuted = false;
262
- private initCallFeedPromise?: Promise<void>;
263
- private _livekitServiceURL?: string;
264
-
265
- private stats: GroupCallStats | undefined;
266
- /**
267
- * Configure default webrtc stats collection interval in ms
268
- * Disable collecting webrtc stats by setting interval to 0
269
- */
270
- private statsCollectIntervalTime = 0;
271
-
272
- public constructor(
273
- private client: MatrixClient,
274
- public room: Room,
275
- public type: GroupCallType,
276
- public isPtt: boolean,
277
- public intent: GroupCallIntent,
278
- groupCallId?: string,
279
- private dataChannelsEnabled?: boolean,
280
- private dataChannelOptions?: IGroupCallDataChannelOptions,
281
- isCallWithoutVideoAndAudio?: boolean,
282
- // this tells the js-sdk not to actually establish any calls to exchange media and just to
283
- // create the group call signaling events, with the intention that the actual media will be
284
- // handled using livekit. The js-sdk doesn't contain any code to do the actual livekit call though.
285
- private useLivekit = false,
286
- livekitServiceURL?: string,
287
- ) {
288
- super();
289
- this.reEmitter = new ReEmitter(this);
290
- this.groupCallId = groupCallId ?? genCallID();
291
- this._livekitServiceURL = livekitServiceURL;
292
- this.creationTs =
293
- room.currentState.getStateEvents(EventType.GroupCallPrefix, this.groupCallId)?.getTs() ?? null;
294
- this.updateParticipants();
295
-
296
- room.on(RoomStateEvent.Update, this.onRoomState);
297
- this.on(GroupCallEvent.ParticipantsChanged, this.onParticipantsChanged);
298
- this.on(GroupCallEvent.GroupCallStateChanged, this.onStateChanged);
299
- this.on(GroupCallEvent.LocalScreenshareStateChanged, this.onLocalFeedsChanged);
300
- this.allowCallWithoutVideoAndAudio = !!isCallWithoutVideoAndAudio;
301
- }
302
-
303
- private onConnectionStats = (report: ConnectionStatsReport): void => {
304
- // Final emit of the summary event, to be consumed by the client
305
- this.emit(GroupCallStatsReportEvent.ConnectionStats, { report });
306
- };
307
-
308
- private onByteSentStats = (report: ByteSentStatsReport): void => {
309
- // Final emit of the summary event, to be consumed by the client
310
- this.emit(GroupCallStatsReportEvent.ByteSentStats, { report });
311
- };
312
-
313
- private onSummaryStats = (report: SummaryStatsReport): void => {
314
- SummaryStatsReportGatherer.extendSummaryReport(report, this.participants);
315
- // Final emit of the summary event, to be consumed by the client
316
- this.emit(GroupCallStatsReportEvent.SummaryStats, { report });
317
- };
318
-
319
- private onCallFeedReport = (report: CallFeedReport): void => {
320
- if (this.localCallFeed) {
321
- report = CallFeedStatsReporter.expandCallFeedReport(report, [this.localCallFeed], "from-local-feed");
322
- }
323
-
324
- const callFeeds: CallFeed[] = [];
325
- this.forEachCall((call) => {
326
- if (call.callId === report.callId) {
327
- call.getFeeds().forEach((f) => callFeeds.push(f));
328
- }
329
- });
330
-
331
- report = CallFeedStatsReporter.expandCallFeedReport(report, callFeeds, "from-call-feed");
332
- this.emit(GroupCallStatsReportEvent.CallFeedStats, { report });
333
- };
334
-
335
- public async create(): Promise<GroupCall> {
336
- this.creationTs = Date.now();
337
- this.client.groupCallEventHandler!.groupCalls.set(this.room.roomId, this);
338
- this.client.emit(GroupCallEventHandlerEvent.Outgoing, this);
339
-
340
- await this.sendCallStateEvent();
341
-
342
- return this;
343
- }
344
-
345
- private async sendCallStateEvent(): Promise<void> {
346
- const groupCallState: IGroupCallRoomState = {
347
- "m.intent": this.intent,
348
- "m.type": this.type,
349
- "io.element.ptt": this.isPtt,
350
- // TODO: Specify data-channels better
351
- "dataChannelsEnabled": this.dataChannelsEnabled,
352
- "dataChannelOptions": this.dataChannelsEnabled ? this.dataChannelOptions : undefined,
353
- };
354
- if (this.livekitServiceURL) {
355
- groupCallState["io.element.livekit_service_url"] = this.livekitServiceURL;
356
- }
357
-
358
- await this.client.sendStateEvent(this.room.roomId, EventType.GroupCallPrefix, groupCallState, this.groupCallId);
359
- }
360
-
361
- public get livekitServiceURL(): string | undefined {
362
- return this._livekitServiceURL;
363
- }
364
-
365
- public updateLivekitServiceURL(newURL: string): Promise<void> {
366
- this._livekitServiceURL = newURL;
367
- return this.sendCallStateEvent();
368
- }
369
-
370
- private _state = GroupCallState.LocalCallFeedUninitialized;
371
-
372
- /**
373
- * The group call's state.
374
- */
375
- public get state(): GroupCallState {
376
- return this._state;
377
- }
378
-
379
- private set state(value: GroupCallState) {
380
- const prevValue = this._state;
381
- if (value !== prevValue) {
382
- this._state = value;
383
- this.emit(GroupCallEvent.GroupCallStateChanged, value, prevValue);
384
- }
385
- }
386
-
387
- private _participants = new Map<RoomMember, Map<string, ParticipantState>>();
388
-
389
- /**
390
- * The current participants in the call, as a map from members to device IDs
391
- * to participant info.
392
- */
393
- public get participants(): Map<RoomMember, Map<string, ParticipantState>> {
394
- return this._participants;
395
- }
396
-
397
- private set participants(value: Map<RoomMember, Map<string, ParticipantState>>) {
398
- const prevValue = this._participants;
399
- const participantStateEqual = (x: ParticipantState, y: ParticipantState): boolean =>
400
- x.sessionId === y.sessionId && x.screensharing === y.screensharing;
401
- const deviceMapsEqual = (x: Map<string, ParticipantState>, y: Map<string, ParticipantState>): boolean =>
402
- mapsEqual(x, y, participantStateEqual);
403
-
404
- // Only update if the map actually changed
405
- if (!mapsEqual(value, prevValue, deviceMapsEqual)) {
406
- this._participants = value;
407
- this.emit(GroupCallEvent.ParticipantsChanged, value);
408
- }
409
- }
410
-
411
- private _creationTs: number | null = null;
412
-
413
- /**
414
- * The timestamp at which the call was created, or null if it has not yet
415
- * been created.
416
- */
417
- public get creationTs(): number | null {
418
- return this._creationTs;
419
- }
420
-
421
- private set creationTs(value: number | null) {
422
- this._creationTs = value;
423
- }
424
-
425
- private _enteredViaAnotherSession = false;
426
-
427
- /**
428
- * Whether the local device has entered this call via another session, such
429
- * as a widget.
430
- */
431
- public get enteredViaAnotherSession(): boolean {
432
- return this._enteredViaAnotherSession;
433
- }
434
-
435
- public set enteredViaAnotherSession(value: boolean) {
436
- this._enteredViaAnotherSession = value;
437
- this.updateParticipants();
438
- }
439
-
440
- /**
441
- * Executes the given callback on all calls in this group call.
442
- * @param f - The callback.
443
- */
444
- public forEachCall(f: (call: MatrixCall) => void): void {
445
- for (const deviceMap of this.calls.values()) {
446
- for (const call of deviceMap.values()) f(call);
447
- }
448
- }
449
-
450
- public getLocalFeeds(): CallFeed[] {
451
- const feeds: CallFeed[] = [];
452
-
453
- if (this.localCallFeed) feeds.push(this.localCallFeed);
454
- if (this.localScreenshareFeed) feeds.push(this.localScreenshareFeed);
455
-
456
- return feeds;
457
- }
458
-
459
- public hasLocalParticipant(): boolean {
460
- return (
461
- this.participants.get(this.room.getMember(this.client.getUserId()!)!)?.has(this.client.getDeviceId()!) ??
462
- false
463
- );
464
- }
465
-
466
- /**
467
- * Determines whether the given call is one that we were expecting to exist
468
- * given our knowledge of who is participating in the group call.
469
- */
470
- private callExpected(call: MatrixCall): boolean {
471
- const userId = getCallUserId(call);
472
- const member = userId === null ? null : this.room.getMember(userId);
473
- const deviceId = call.getOpponentDeviceId();
474
- return member !== null && deviceId !== undefined && this.participants.get(member)?.get(deviceId) !== undefined;
475
- }
476
-
477
- public async initLocalCallFeed(): Promise<void> {
478
- if (this.useLivekit) {
479
- logger.info("Livekit group call: not starting local call feed.");
480
- return;
481
- }
482
-
483
- if (this.state !== GroupCallState.LocalCallFeedUninitialized) {
484
- throw new Error(`Cannot initialize local call feed in the "${this.state}" state.`);
485
- }
486
- this.state = GroupCallState.InitializingLocalCallFeed;
487
-
488
- // wraps the real method to serialise calls, because we don't want to try starting
489
- // multiple call feeds at once
490
- if (this.initCallFeedPromise) return this.initCallFeedPromise;
491
-
492
- try {
493
- this.initCallFeedPromise = this.initLocalCallFeedInternal();
494
- await this.initCallFeedPromise;
495
- } finally {
496
- this.initCallFeedPromise = undefined;
497
- }
498
- }
499
-
500
- private async initLocalCallFeedInternal(): Promise<void> {
501
- logger.log(`GroupCall ${this.groupCallId} initLocalCallFeedInternal() running`);
502
-
503
- let stream: MediaStream;
504
-
505
- try {
506
- stream = await this.client.getMediaHandler().getUserMediaStream(true, this.type === GroupCallType.Video);
507
- } catch (error) {
508
- // If is allowed to join a call without a media stream, then we
509
- // don't throw an error here. But we need an empty Local Feed to establish
510
- // a connection later.
511
- if (this.allowCallWithoutVideoAndAudio) {
512
- stream = new MediaStream();
513
- } else {
514
- this.state = GroupCallState.LocalCallFeedUninitialized;
515
- throw error;
516
- }
517
- }
518
-
519
- // The call could've been disposed while we were waiting, and could
520
- // also have been started back up again (hello, React 18) so if we're
521
- // still in this 'initializing' state, carry on, otherwise bail.
522
- if (this._state !== GroupCallState.InitializingLocalCallFeed) {
523
- this.client.getMediaHandler().stopUserMediaStream(stream);
524
- throw new Error("Group call disposed while gathering media stream");
525
- }
526
-
527
- const callFeed = new CallFeed({
528
- client: this.client,
529
- roomId: this.room.roomId,
530
- userId: this.client.getUserId()!,
531
- deviceId: this.client.getDeviceId()!,
532
- stream,
533
- purpose: SDPStreamMetadataPurpose.Usermedia,
534
- audioMuted: this.initWithAudioMuted || stream.getAudioTracks().length === 0 || this.isPtt,
535
- videoMuted: this.initWithVideoMuted || stream.getVideoTracks().length === 0,
536
- });
537
-
538
- setTracksEnabled(stream.getAudioTracks(), !callFeed.isAudioMuted());
539
- setTracksEnabled(stream.getVideoTracks(), !callFeed.isVideoMuted());
540
-
541
- this.localCallFeed = callFeed;
542
- this.addUserMediaFeed(callFeed);
543
-
544
- this.state = GroupCallState.LocalCallFeedInitialized;
545
- }
546
-
547
- public async updateLocalUsermediaStream(stream: MediaStream): Promise<void> {
548
- if (this.localCallFeed) {
549
- const oldStream = this.localCallFeed.stream;
550
- this.localCallFeed.setNewStream(stream);
551
- const micShouldBeMuted = this.localCallFeed.isAudioMuted();
552
- const vidShouldBeMuted = this.localCallFeed.isVideoMuted();
553
- logger.log(
554
- `GroupCall ${this.groupCallId} updateLocalUsermediaStream() (oldStreamId=${oldStream.id}, newStreamId=${stream.id}, micShouldBeMuted=${micShouldBeMuted}, vidShouldBeMuted=${vidShouldBeMuted})`,
555
- );
556
- setTracksEnabled(stream.getAudioTracks(), !micShouldBeMuted);
557
- setTracksEnabled(stream.getVideoTracks(), !vidShouldBeMuted);
558
- this.client.getMediaHandler().stopUserMediaStream(oldStream);
559
- }
560
- }
561
-
562
- public async enter(): Promise<void> {
563
- if (this.state === GroupCallState.LocalCallFeedUninitialized) {
564
- await this.initLocalCallFeed();
565
- } else if (this.state !== GroupCallState.LocalCallFeedInitialized) {
566
- throw new Error(`Cannot enter call in the "${this.state}" state`);
567
- }
568
-
569
- logger.log(`GroupCall ${this.groupCallId} enter() running`);
570
- this.state = GroupCallState.Entered;
571
-
572
- this.client.on(CallEventHandlerEvent.Incoming, this.onIncomingCall);
573
-
574
- for (const call of this.client.callEventHandler!.calls.values()) {
575
- this.onIncomingCall(call);
576
- }
577
-
578
- if (!this.useLivekit) {
579
- this.retryCallLoopInterval = setInterval(this.onRetryCallLoop, this.retryCallInterval);
580
-
581
- this.activeSpeaker = undefined;
582
- this.onActiveSpeakerLoop();
583
- this.activeSpeakerLoopInterval = setInterval(this.onActiveSpeakerLoop, this.activeSpeakerInterval);
584
- }
585
- }
586
-
587
- private dispose(): void {
588
- if (this.localCallFeed) {
589
- this.removeUserMediaFeed(this.localCallFeed);
590
- this.localCallFeed = undefined;
591
- }
592
-
593
- if (this.localScreenshareFeed) {
594
- this.client.getMediaHandler().stopScreensharingStream(this.localScreenshareFeed.stream);
595
- this.removeScreenshareFeed(this.localScreenshareFeed);
596
- this.localScreenshareFeed = undefined;
597
- this.localDesktopCapturerSourceId = undefined;
598
- }
599
-
600
- this.client.getMediaHandler().stopAllStreams();
601
-
602
- if (this.transmitTimer !== null) {
603
- clearTimeout(this.transmitTimer);
604
- this.transmitTimer = null;
605
- }
606
-
607
- if (this.retryCallLoopInterval !== undefined) {
608
- clearInterval(this.retryCallLoopInterval);
609
- this.retryCallLoopInterval = undefined;
610
- }
611
-
612
- if (this.participantsExpirationTimer !== null) {
613
- clearTimeout(this.participantsExpirationTimer);
614
- this.participantsExpirationTimer = null;
615
- }
616
-
617
- if (this.state !== GroupCallState.Entered) {
618
- return;
619
- }
620
-
621
- this.forEachCall((call) => call.hangup(CallErrorCode.UserHangup, false));
622
-
623
- this.activeSpeaker = undefined;
624
- clearInterval(this.activeSpeakerLoopInterval);
625
-
626
- this.retryCallCounts.clear();
627
- clearInterval(this.retryCallLoopInterval);
628
-
629
- this.client.removeListener(CallEventHandlerEvent.Incoming, this.onIncomingCall);
630
- this.stats?.stop();
631
- }
632
-
633
- public leave(): void {
634
- this.dispose();
635
- this.state = GroupCallState.LocalCallFeedUninitialized;
636
- }
637
-
638
- public async terminate(emitStateEvent = true): Promise<void> {
639
- this.dispose();
640
-
641
- this.room.off(RoomStateEvent.Update, this.onRoomState);
642
- this.client.groupCallEventHandler!.groupCalls.delete(this.room.roomId);
643
- this.client.emit(GroupCallEventHandlerEvent.Ended, this);
644
- this.state = GroupCallState.Ended;
645
-
646
- if (emitStateEvent) {
647
- const existingStateEvent = this.room.currentState.getStateEvents(
648
- EventType.GroupCallPrefix,
649
- this.groupCallId,
650
- )!;
651
-
652
- await this.client.sendStateEvent(
653
- this.room.roomId,
654
- EventType.GroupCallPrefix,
655
- {
656
- ...existingStateEvent.getContent(),
657
- "m.terminated": GroupCallTerminationReason.CallEnded,
658
- },
659
- this.groupCallId,
660
- );
661
- }
662
- }
663
-
664
- /*
665
- * Local Usermedia
666
- */
667
-
668
- public isLocalVideoMuted(): boolean {
669
- if (this.localCallFeed) {
670
- return this.localCallFeed.isVideoMuted();
671
- }
672
-
673
- return true;
674
- }
675
-
676
- public isMicrophoneMuted(): boolean {
677
- if (this.localCallFeed) {
678
- return this.localCallFeed.isAudioMuted();
679
- }
680
-
681
- return true;
682
- }
683
-
684
- /**
685
- * Sets the mute state of the local participants's microphone.
686
- * @param muted - Whether to mute the microphone
687
- * @returns Whether muting/unmuting was successful
688
- */
689
- public async setMicrophoneMuted(muted: boolean): Promise<boolean> {
690
- // hasAudioDevice can block indefinitely if the window has lost focus,
691
- // and it doesn't make much sense to keep a device from being muted, so
692
- // we always allow muted = true changes to go through
693
- if (!muted && !(await this.client.getMediaHandler().hasAudioDevice())) {
694
- return false;
695
- }
696
-
697
- const sendUpdatesBefore = !muted && this.isPtt;
698
-
699
- // set a timer for the maximum transmit time on PTT calls
700
- if (this.isPtt) {
701
- // Set or clear the max transmit timer
702
- if (!muted && this.isMicrophoneMuted()) {
703
- this.transmitTimer = setTimeout(() => {
704
- this.setMicrophoneMuted(true);
705
- }, this.pttMaxTransmitTime);
706
- } else if (muted && !this.isMicrophoneMuted()) {
707
- if (this.transmitTimer !== null) clearTimeout(this.transmitTimer);
708
- this.transmitTimer = null;
709
- }
710
- }
711
-
712
- this.forEachCall((call) => call.localUsermediaFeed?.setAudioVideoMuted(muted, null));
713
-
714
- const sendUpdates = async (): Promise<void> => {
715
- const updates: Promise<void>[] = [];
716
- this.forEachCall((call) => updates.push(call.sendMetadataUpdate()));
717
-
718
- await Promise.all(updates).catch((e) =>
719
- logger.info(
720
- `GroupCall ${this.groupCallId} setMicrophoneMuted() failed to send some metadata updates`,
721
- e,
722
- ),
723
- );
724
- };
725
-
726
- if (sendUpdatesBefore) await sendUpdates();
727
-
728
- if (this.localCallFeed) {
729
- logger.log(
730
- `GroupCall ${this.groupCallId} setMicrophoneMuted() (streamId=${this.localCallFeed.stream.id}, muted=${muted})`,
731
- );
732
-
733
- const hasPermission = await this.checkAudioPermissionIfNecessary(muted);
734
-
735
- if (!hasPermission) {
736
- return false;
737
- }
738
-
739
- this.localCallFeed.setAudioVideoMuted(muted, null);
740
- // I don't believe its actually necessary to enable these tracks: they
741
- // are the one on the GroupCall's own CallFeed and are cloned before being
742
- // given to any of the actual calls, so these tracks don't actually go
743
- // anywhere. Let's do it anyway to avoid confusion.
744
- setTracksEnabled(this.localCallFeed.stream.getAudioTracks(), !muted);
745
- } else {
746
- logger.log(`GroupCall ${this.groupCallId} setMicrophoneMuted() no stream muted (muted=${muted})`);
747
- this.initWithAudioMuted = muted;
748
- }
749
-
750
- this.forEachCall((call) =>
751
- setTracksEnabled(call.localUsermediaFeed!.stream.getAudioTracks(), !muted && this.callExpected(call)),
752
- );
753
- this.emit(GroupCallEvent.LocalMuteStateChanged, muted, this.isLocalVideoMuted());
754
-
755
- if (!sendUpdatesBefore) await sendUpdates();
756
-
757
- return true;
758
- }
759
-
760
- /**
761
- * If we allow entering a call without a camera and without video, it can happen that the access rights to the
762
- * devices have not yet been queried. If a stream does not yet have an audio track, we assume that the rights have
763
- * not yet been checked.
764
- *
765
- * `this.client.getMediaHandler().getUserMediaStream` clones the current stream, so it only wanted to be called when
766
- * not Audio Track exists.
767
- * As such, this is a compromise, because, the access rights should always be queried before the call.
768
- */
769
- private async checkAudioPermissionIfNecessary(muted: boolean): Promise<boolean> {
770
- // We needed this here to avoid an error in case user join a call without a device.
771
- try {
772
- if (!muted && this.localCallFeed && !this.localCallFeed.hasAudioTrack) {
773
- const stream = await this.client
774
- .getMediaHandler()
775
- .getUserMediaStream(true, !this.localCallFeed.isVideoMuted());
776
- if (stream?.getTracks().length === 0) {
777
- // if case permission denied to get a stream stop this here
778
- /* istanbul ignore next */
779
- logger.log(
780
- `GroupCall ${this.groupCallId} setMicrophoneMuted() no device to receive local stream, muted=${muted}`,
781
- );
782
- return false;
783
- }
784
- }
785
- } catch {
786
- /* istanbul ignore next */
787
- logger.log(
788
- `GroupCall ${this.groupCallId} setMicrophoneMuted() no device or permission to receive local stream, muted=${muted}`,
789
- );
790
- return false;
791
- }
792
-
793
- return true;
794
- }
795
-
796
- /**
797
- * Sets the mute state of the local participants's video.
798
- * @param muted - Whether to mute the video
799
- * @returns Whether muting/unmuting was successful
800
- */
801
- public async setLocalVideoMuted(muted: boolean): Promise<boolean> {
802
- // hasAudioDevice can block indefinitely if the window has lost focus,
803
- // and it doesn't make much sense to keep a device from being muted, so
804
- // we always allow muted = true changes to go through
805
- if (!muted && !(await this.client.getMediaHandler().hasVideoDevice())) {
806
- return false;
807
- }
808
-
809
- if (this.localCallFeed) {
810
- /* istanbul ignore next */
811
- logger.log(
812
- `GroupCall ${this.groupCallId} setLocalVideoMuted() (stream=${this.localCallFeed.stream.id}, muted=${muted})`,
813
- );
814
-
815
- try {
816
- const stream = await this.client.getMediaHandler().getUserMediaStream(true, !muted);
817
- await this.updateLocalUsermediaStream(stream);
818
- this.localCallFeed.setAudioVideoMuted(null, muted);
819
- setTracksEnabled(this.localCallFeed.stream.getVideoTracks(), !muted);
820
- } catch {
821
- // No permission to video device
822
- /* istanbul ignore next */
823
- logger.log(
824
- `GroupCall ${this.groupCallId} setLocalVideoMuted() no device or permission to receive local stream, muted=${muted}`,
825
- );
826
- return false;
827
- }
828
- } else {
829
- logger.log(`GroupCall ${this.groupCallId} setLocalVideoMuted() no stream muted (muted=${muted})`);
830
- this.initWithVideoMuted = muted;
831
- }
832
-
833
- const updates: Promise<unknown>[] = [];
834
- this.forEachCall((call) => updates.push(call.setLocalVideoMuted(muted)));
835
- await Promise.all(updates);
836
-
837
- // We setTracksEnabled again, independently from the call doing it
838
- // internally, since we might not be expecting the call
839
- this.forEachCall((call) =>
840
- setTracksEnabled(call.localUsermediaFeed!.stream.getVideoTracks(), !muted && this.callExpected(call)),
841
- );
842
-
843
- this.emit(GroupCallEvent.LocalMuteStateChanged, this.isMicrophoneMuted(), muted);
844
-
845
- return true;
846
- }
847
-
848
- public async setScreensharingEnabled(enabled: boolean, opts: IScreensharingOpts = {}): Promise<boolean> {
849
- if (enabled === this.isScreensharing()) {
850
- return enabled;
851
- }
852
-
853
- if (enabled) {
854
- try {
855
- logger.log(
856
- `GroupCall ${this.groupCallId} setScreensharingEnabled() is asking for screensharing permissions`,
857
- );
858
- const stream = await this.client.getMediaHandler().getScreensharingStream(opts);
859
-
860
- for (const track of stream.getTracks()) {
861
- const onTrackEnded = (): void => {
862
- this.setScreensharingEnabled(false);
863
- track.removeEventListener("ended", onTrackEnded);
864
- };
865
-
866
- track.addEventListener("ended", onTrackEnded);
867
- }
868
-
869
- logger.log(
870
- `GroupCall ${this.groupCallId} setScreensharingEnabled() granted screensharing permissions. Setting screensharing enabled on all calls`,
871
- );
872
-
873
- this.localDesktopCapturerSourceId = opts.desktopCapturerSourceId;
874
- this.localScreenshareFeed = new CallFeed({
875
- client: this.client,
876
- roomId: this.room.roomId,
877
- userId: this.client.getUserId()!,
878
- deviceId: this.client.getDeviceId()!,
879
- stream,
880
- purpose: SDPStreamMetadataPurpose.Screenshare,
881
- audioMuted: false,
882
- videoMuted: false,
883
- });
884
- this.addScreenshareFeed(this.localScreenshareFeed);
885
-
886
- this.emit(
887
- GroupCallEvent.LocalScreenshareStateChanged,
888
- true,
889
- this.localScreenshareFeed,
890
- this.localDesktopCapturerSourceId,
891
- );
892
-
893
- // TODO: handle errors
894
- this.forEachCall((call) => call.pushLocalFeed(this.localScreenshareFeed!.clone()));
895
-
896
- return true;
897
- } catch (error) {
898
- if (opts.throwOnFail) throw error;
899
- logger.error(
900
- `GroupCall ${this.groupCallId} setScreensharingEnabled() enabling screensharing error`,
901
- error,
902
- );
903
- this.emit(
904
- GroupCallEvent.Error,
905
- new GroupCallError(
906
- GroupCallErrorCode.NoUserMedia,
907
- "Failed to get screen-sharing stream: ",
908
- error as Error,
909
- ),
910
- );
911
- return false;
912
- }
913
- } else {
914
- this.forEachCall((call) => {
915
- if (call.localScreensharingFeed) call.removeLocalFeed(call.localScreensharingFeed);
916
- });
917
- this.client.getMediaHandler().stopScreensharingStream(this.localScreenshareFeed!.stream);
918
- this.removeScreenshareFeed(this.localScreenshareFeed!);
919
- this.localScreenshareFeed = undefined;
920
- this.localDesktopCapturerSourceId = undefined;
921
- this.emit(GroupCallEvent.LocalScreenshareStateChanged, false, undefined, undefined);
922
- return false;
923
- }
924
- }
925
-
926
- public isScreensharing(): boolean {
927
- return !!this.localScreenshareFeed;
928
- }
929
-
930
- /*
931
- * Call Setup
932
- *
933
- * There are two different paths for calls to be created:
934
- * 1. Incoming calls triggered by the Call.incoming event.
935
- * 2. Outgoing calls to the initial members of a room or new members
936
- * as they are observed by the RoomState.members event.
937
- */
938
-
939
- private onIncomingCall = (newCall: MatrixCall): void => {
940
- // The incoming calls may be for another room, which we will ignore.
941
- if (newCall.roomId !== this.room.roomId) {
942
- return;
943
- }
944
-
945
- if (newCall.state !== CallState.Ringing) {
946
- logger.warn(
947
- `GroupCall ${this.groupCallId} onIncomingCall() incoming call no longer in ringing state - ignoring`,
948
- );
949
- return;
950
- }
951
-
952
- if (!newCall.groupCallId || newCall.groupCallId !== this.groupCallId) {
953
- logger.log(
954
- `GroupCall ${this.groupCallId} onIncomingCall() ignored because it doesn't match the current group call`,
955
- );
956
- newCall.reject();
957
- return;
958
- }
959
-
960
- const opponentUserId = newCall.getOpponentMember()?.userId;
961
- if (opponentUserId === undefined) {
962
- logger.warn(`GroupCall ${this.groupCallId} onIncomingCall() incoming call with no member - ignoring`);
963
- return;
964
- }
965
-
966
- if (this.useLivekit) {
967
- logger.info("Received incoming call whilst in signaling-only mode! Ignoring.");
968
- return;
969
- }
970
-
971
- const deviceMap = this.calls.get(opponentUserId) ?? new Map<string, MatrixCall>();
972
- const prevCall = deviceMap.get(newCall.getOpponentDeviceId()!);
973
-
974
- if (prevCall?.callId === newCall.callId) return;
975
-
976
- logger.log(
977
- `GroupCall ${this.groupCallId} onIncomingCall() incoming call (userId=${opponentUserId}, callId=${newCall.callId})`,
978
- );
979
-
980
- if (prevCall) prevCall.hangup(CallErrorCode.Replaced, false);
981
- // We must do this before we start initialising / answering the call as we
982
- // need to know it is the active call for this user+deviceId and to not ignore
983
- // events from it.
984
- deviceMap.set(newCall.getOpponentDeviceId()!, newCall);
985
- this.calls.set(opponentUserId, deviceMap);
986
-
987
- this.initCall(newCall);
988
-
989
- const feeds = this.getLocalFeeds().map((feed) => feed.clone());
990
- if (!this.callExpected(newCall)) {
991
- // Disable our tracks for users not explicitly participating in the
992
- // call but trying to receive the feeds
993
- for (const feed of feeds) {
994
- setTracksEnabled(feed.stream.getAudioTracks(), false);
995
- setTracksEnabled(feed.stream.getVideoTracks(), false);
996
- }
997
- }
998
- newCall.answerWithCallFeeds(feeds);
999
-
1000
- this.emit(GroupCallEvent.CallsChanged, this.calls);
1001
- };
1002
-
1003
- /**
1004
- * Determines whether a given participant expects us to call them (versus
1005
- * them calling us).
1006
- * @param userId - The participant's user ID.
1007
- * @param deviceId - The participant's device ID.
1008
- * @returns Whether we need to place an outgoing call to the participant.
1009
- */
1010
- private wantsOutgoingCall(userId: string, deviceId: string): boolean {
1011
- const localUserId = this.client.getUserId()!;
1012
- const localDeviceId = this.client.getDeviceId()!;
1013
- return (
1014
- // If a user's ID is less than our own, they'll call us
1015
- userId >= localUserId &&
1016
- // If this is another one of our devices, compare device IDs to tell whether it'll call us
1017
- (userId !== localUserId || deviceId > localDeviceId)
1018
- );
1019
- }
1020
-
1021
- /**
1022
- * Places calls to all participants that we're responsible for calling.
1023
- */
1024
- private placeOutgoingCalls(): void {
1025
- let callsChanged = false;
1026
-
1027
- for (const [{ userId }, participantMap] of this.participants) {
1028
- const callMap = this.calls.get(userId) ?? new Map<string, MatrixCall>();
1029
-
1030
- for (const [deviceId, participant] of participantMap) {
1031
- const prevCall = callMap.get(deviceId);
1032
-
1033
- if (
1034
- prevCall?.getOpponentSessionId() !== participant.sessionId &&
1035
- this.wantsOutgoingCall(userId, deviceId)
1036
- ) {
1037
- callsChanged = true;
1038
-
1039
- if (prevCall !== undefined) {
1040
- logger.debug(
1041
- `GroupCall ${this.groupCallId} placeOutgoingCalls() replacing call (userId=${userId}, deviceId=${deviceId}, callId=${prevCall.callId})`,
1042
- );
1043
- prevCall.hangup(CallErrorCode.NewSession, false);
1044
- }
1045
-
1046
- const newCall = createNewMatrixCall(this.client, this.room.roomId, {
1047
- invitee: userId,
1048
- opponentDeviceId: deviceId,
1049
- opponentSessionId: participant.sessionId,
1050
- groupCallId: this.groupCallId,
1051
- });
1052
-
1053
- if (newCall === null) {
1054
- logger.error(
1055
- `GroupCall ${this.groupCallId} placeOutgoingCalls() failed to create call (userId=${userId}, device=${deviceId})`,
1056
- );
1057
- callMap.delete(deviceId);
1058
- } else {
1059
- this.initCall(newCall);
1060
- callMap.set(deviceId, newCall);
1061
-
1062
- logger.debug(
1063
- `GroupCall ${this.groupCallId} placeOutgoingCalls() placing call (userId=${userId}, deviceId=${deviceId}, sessionId=${participant.sessionId})`,
1064
- );
1065
-
1066
- newCall
1067
- .placeCallWithCallFeeds(
1068
- this.getLocalFeeds().map((feed) => feed.clone()),
1069
- participant.screensharing,
1070
- )
1071
- .then(() => {
1072
- if (this.dataChannelsEnabled) {
1073
- newCall.createDataChannel("datachannel", this.dataChannelOptions);
1074
- }
1075
- })
1076
- .catch((e) => {
1077
- logger.warn(
1078
- `GroupCall ${this.groupCallId} placeOutgoingCalls() failed to place call (userId=${userId})`,
1079
- e,
1080
- );
1081
-
1082
- if (e instanceof CallError && e.code === GroupCallErrorCode.UnknownDevice) {
1083
- this.emit(GroupCallEvent.Error, e);
1084
- } else {
1085
- this.emit(
1086
- GroupCallEvent.Error,
1087
- new GroupCallError(
1088
- GroupCallErrorCode.PlaceCallFailed,
1089
- `Failed to place call to ${userId}`,
1090
- ),
1091
- );
1092
- }
1093
-
1094
- newCall.hangup(CallErrorCode.SignallingFailed, false);
1095
- if (callMap.get(deviceId) === newCall) callMap.delete(deviceId);
1096
- });
1097
- }
1098
- }
1099
- }
1100
-
1101
- if (callMap.size > 0) {
1102
- this.calls.set(userId, callMap);
1103
- } else {
1104
- this.calls.delete(userId);
1105
- }
1106
- }
1107
-
1108
- if (callsChanged) this.emit(GroupCallEvent.CallsChanged, this.calls);
1109
- }
1110
-
1111
- /*
1112
- * Room Member State
1113
- */
1114
-
1115
- private getMemberStateEvents(): MatrixEvent[];
1116
- private getMemberStateEvents(userId: string): MatrixEvent | null;
1117
- private getMemberStateEvents(userId?: string): MatrixEvent[] | MatrixEvent | null {
1118
- return userId === undefined
1119
- ? this.room.currentState.getStateEvents(EventType.GroupCallMemberPrefix)
1120
- : this.room.currentState.getStateEvents(EventType.GroupCallMemberPrefix, userId);
1121
- }
1122
-
1123
- private onRetryCallLoop = (): void => {
1124
- let needsRetry = false;
1125
-
1126
- for (const [{ userId }, participantMap] of this.participants) {
1127
- const callMap = this.calls.get(userId);
1128
- let retriesMap = this.retryCallCounts.get(userId);
1129
-
1130
- for (const [deviceId, participant] of participantMap) {
1131
- const call = callMap?.get(deviceId);
1132
- const retries = retriesMap?.get(deviceId) ?? 0;
1133
-
1134
- if (
1135
- call?.getOpponentSessionId() !== participant.sessionId &&
1136
- this.wantsOutgoingCall(userId, deviceId) &&
1137
- retries < 3
1138
- ) {
1139
- if (retriesMap === undefined) {
1140
- retriesMap = new Map();
1141
- this.retryCallCounts.set(userId, retriesMap);
1142
- }
1143
- retriesMap.set(deviceId, retries + 1);
1144
- needsRetry = true;
1145
- }
1146
- }
1147
- }
1148
-
1149
- if (needsRetry) this.placeOutgoingCalls();
1150
- };
1151
-
1152
- private initCall(call: MatrixCall): void {
1153
- const opponentMemberId = getCallUserId(call);
1154
-
1155
- if (!opponentMemberId) {
1156
- throw new Error("Cannot init call without user id");
1157
- }
1158
-
1159
- const onCallFeedsChanged = (): void => this.onCallFeedsChanged(call);
1160
- const onCallStateChanged = (state: CallState, oldState?: CallState): void =>
1161
- this.onCallStateChanged(call, state, oldState);
1162
- const onCallHangup = this.onCallHangup;
1163
- const onCallReplaced = (newCall: MatrixCall): void => this.onCallReplaced(call, newCall);
1164
-
1165
- let deviceMap = this.callHandlers.get(opponentMemberId);
1166
- if (deviceMap === undefined) {
1167
- deviceMap = new Map();
1168
- this.callHandlers.set(opponentMemberId, deviceMap);
1169
- }
1170
-
1171
- deviceMap.set(call.getOpponentDeviceId()!, {
1172
- onCallFeedsChanged,
1173
- onCallStateChanged,
1174
- onCallHangup,
1175
- onCallReplaced,
1176
- });
1177
-
1178
- call.on(CallEvent.FeedsChanged, onCallFeedsChanged);
1179
- call.on(CallEvent.State, onCallStateChanged);
1180
- call.on(CallEvent.Hangup, onCallHangup);
1181
- call.on(CallEvent.Replaced, onCallReplaced);
1182
-
1183
- call.isPtt = this.isPtt;
1184
-
1185
- this.reEmitter.reEmit(call, Object.values(CallEvent));
1186
-
1187
- call.initStats(this.getGroupCallStats());
1188
-
1189
- onCallFeedsChanged();
1190
- }
1191
-
1192
- private disposeCall(call: MatrixCall, hangupReason: CallErrorCode): void {
1193
- const opponentMemberId = getCallUserId(call);
1194
- const opponentDeviceId = call.getOpponentDeviceId()!;
1195
-
1196
- if (!opponentMemberId) {
1197
- throw new Error("Cannot dispose call without user id");
1198
- }
1199
-
1200
- const deviceMap = this.callHandlers.get(opponentMemberId)!;
1201
- const { onCallFeedsChanged, onCallStateChanged, onCallHangup, onCallReplaced } =
1202
- deviceMap.get(opponentDeviceId)!;
1203
-
1204
- call.removeListener(CallEvent.FeedsChanged, onCallFeedsChanged);
1205
- call.removeListener(CallEvent.State, onCallStateChanged);
1206
- call.removeListener(CallEvent.Hangup, onCallHangup);
1207
- call.removeListener(CallEvent.Replaced, onCallReplaced);
1208
-
1209
- deviceMap.delete(opponentMemberId);
1210
- if (deviceMap.size === 0) this.callHandlers.delete(opponentMemberId);
1211
-
1212
- if (call.hangupReason === CallErrorCode.Replaced) {
1213
- return;
1214
- }
1215
-
1216
- const usermediaFeed = this.getUserMediaFeed(opponentMemberId, opponentDeviceId);
1217
-
1218
- if (usermediaFeed) {
1219
- this.removeUserMediaFeed(usermediaFeed);
1220
- }
1221
-
1222
- const screenshareFeed = this.getScreenshareFeed(opponentMemberId, opponentDeviceId);
1223
-
1224
- if (screenshareFeed) {
1225
- this.removeScreenshareFeed(screenshareFeed);
1226
- }
1227
- }
1228
-
1229
- private onCallFeedsChanged = (call: MatrixCall): void => {
1230
- const opponentMemberId = getCallUserId(call);
1231
- const opponentDeviceId = call.getOpponentDeviceId()!;
1232
-
1233
- if (!opponentMemberId) {
1234
- throw new Error("Cannot change call feeds without user id");
1235
- }
1236
-
1237
- const currentUserMediaFeed = this.getUserMediaFeed(opponentMemberId, opponentDeviceId);
1238
- const remoteUsermediaFeed = call.remoteUsermediaFeed;
1239
- const remoteFeedChanged = remoteUsermediaFeed !== currentUserMediaFeed;
1240
-
1241
- const deviceMap = this.calls.get(opponentMemberId);
1242
- const currentCallForUserDevice = deviceMap?.get(opponentDeviceId);
1243
- if (currentCallForUserDevice?.callId !== call.callId) {
1244
- // the call in question is not the current call for this user/deviceId
1245
- // so ignore feed events from it otherwise we'll remove our real feeds
1246
- return;
1247
- }
1248
-
1249
- if (remoteFeedChanged) {
1250
- if (!currentUserMediaFeed && remoteUsermediaFeed) {
1251
- this.addUserMediaFeed(remoteUsermediaFeed);
1252
- } else if (currentUserMediaFeed && remoteUsermediaFeed) {
1253
- this.replaceUserMediaFeed(currentUserMediaFeed, remoteUsermediaFeed);
1254
- } else if (currentUserMediaFeed && !remoteUsermediaFeed) {
1255
- this.removeUserMediaFeed(currentUserMediaFeed);
1256
- }
1257
- }
1258
-
1259
- const currentScreenshareFeed = this.getScreenshareFeed(opponentMemberId, opponentDeviceId);
1260
- const remoteScreensharingFeed = call.remoteScreensharingFeed;
1261
- const remoteScreenshareFeedChanged = remoteScreensharingFeed !== currentScreenshareFeed;
1262
-
1263
- if (remoteScreenshareFeedChanged) {
1264
- if (!currentScreenshareFeed && remoteScreensharingFeed) {
1265
- this.addScreenshareFeed(remoteScreensharingFeed);
1266
- } else if (currentScreenshareFeed && remoteScreensharingFeed) {
1267
- this.replaceScreenshareFeed(currentScreenshareFeed, remoteScreensharingFeed);
1268
- } else if (currentScreenshareFeed && !remoteScreensharingFeed) {
1269
- this.removeScreenshareFeed(currentScreenshareFeed);
1270
- }
1271
- }
1272
- };
1273
-
1274
- private onCallStateChanged = (call: MatrixCall, state: CallState, _oldState: CallState | undefined): void => {
1275
- if (state === CallState.Ended) return;
1276
-
1277
- const audioMuted = this.localCallFeed!.isAudioMuted();
1278
-
1279
- if (call.localUsermediaStream && call.isMicrophoneMuted() !== audioMuted) {
1280
- call.setMicrophoneMuted(audioMuted);
1281
- }
1282
-
1283
- const videoMuted = this.localCallFeed!.isVideoMuted();
1284
-
1285
- if (call.localUsermediaStream && call.isLocalVideoMuted() !== videoMuted) {
1286
- call.setLocalVideoMuted(videoMuted);
1287
- }
1288
-
1289
- const opponentUserId = call.getOpponentMember()?.userId;
1290
- if (state === CallState.Connected && opponentUserId) {
1291
- const retriesMap = this.retryCallCounts.get(opponentUserId);
1292
- retriesMap?.delete(call.getOpponentDeviceId()!);
1293
- if (retriesMap?.size === 0) this.retryCallCounts.delete(opponentUserId);
1294
- }
1295
- };
1296
-
1297
- private onCallHangup = (call: MatrixCall): void => {
1298
- if (call.hangupReason === CallErrorCode.Replaced) return;
1299
-
1300
- const opponentUserId = call.getOpponentMember()?.userId ?? this.room.getMember(call.invitee!)!.userId;
1301
- const deviceMap = this.calls.get(opponentUserId);
1302
-
1303
- // Sanity check that this call is in fact in the map
1304
- if (deviceMap?.get(call.getOpponentDeviceId()!) === call) {
1305
- this.disposeCall(call, call.hangupReason as CallErrorCode);
1306
- deviceMap.delete(call.getOpponentDeviceId()!);
1307
- if (deviceMap.size === 0) this.calls.delete(opponentUserId);
1308
- this.emit(GroupCallEvent.CallsChanged, this.calls);
1309
- }
1310
- };
1311
-
1312
- private onCallReplaced = (prevCall: MatrixCall, newCall: MatrixCall): void => {
1313
- const opponentUserId = prevCall.getOpponentMember()!.userId;
1314
-
1315
- let deviceMap = this.calls.get(opponentUserId);
1316
- if (deviceMap === undefined) {
1317
- deviceMap = new Map();
1318
- this.calls.set(opponentUserId, deviceMap);
1319
- }
1320
-
1321
- prevCall.hangup(CallErrorCode.Replaced, false);
1322
- this.initCall(newCall);
1323
- deviceMap.set(prevCall.getOpponentDeviceId()!, newCall);
1324
- this.emit(GroupCallEvent.CallsChanged, this.calls);
1325
- };
1326
-
1327
- /*
1328
- * UserMedia CallFeed Event Handlers
1329
- */
1330
-
1331
- public getUserMediaFeed(userId: string, deviceId: string): CallFeed | undefined {
1332
- return this.userMediaFeeds.find((f) => f.userId === userId && f.deviceId! === deviceId);
1333
- }
1334
-
1335
- private addUserMediaFeed(callFeed: CallFeed): void {
1336
- this.userMediaFeeds.push(callFeed);
1337
- callFeed.measureVolumeActivity(true);
1338
- this.emit(GroupCallEvent.UserMediaFeedsChanged, this.userMediaFeeds);
1339
- }
1340
-
1341
- private replaceUserMediaFeed(existingFeed: CallFeed, replacementFeed: CallFeed): void {
1342
- const feedIndex = this.userMediaFeeds.findIndex(
1343
- (f) => f.userId === existingFeed.userId && f.deviceId! === existingFeed.deviceId,
1344
- );
1345
-
1346
- if (feedIndex === -1) {
1347
- throw new Error("Couldn't find user media feed to replace");
1348
- }
1349
-
1350
- this.userMediaFeeds.splice(feedIndex, 1, replacementFeed);
1351
-
1352
- existingFeed.dispose();
1353
- replacementFeed.measureVolumeActivity(true);
1354
- this.emit(GroupCallEvent.UserMediaFeedsChanged, this.userMediaFeeds);
1355
- }
1356
-
1357
- private removeUserMediaFeed(callFeed: CallFeed): void {
1358
- const feedIndex = this.userMediaFeeds.findIndex(
1359
- (f) => f.userId === callFeed.userId && f.deviceId! === callFeed.deviceId,
1360
- );
1361
-
1362
- if (feedIndex === -1) {
1363
- throw new Error("Couldn't find user media feed to remove");
1364
- }
1365
-
1366
- this.userMediaFeeds.splice(feedIndex, 1);
1367
-
1368
- callFeed.dispose();
1369
- this.emit(GroupCallEvent.UserMediaFeedsChanged, this.userMediaFeeds);
1370
-
1371
- if (this.activeSpeaker === callFeed) {
1372
- this.activeSpeaker = this.userMediaFeeds[0];
1373
- this.emit(GroupCallEvent.ActiveSpeakerChanged, this.activeSpeaker);
1374
- }
1375
- }
1376
-
1377
- private onActiveSpeakerLoop = (): void => {
1378
- let topAvg: number | undefined = undefined;
1379
- let nextActiveSpeaker: CallFeed | undefined = undefined;
1380
-
1381
- for (const callFeed of this.userMediaFeeds) {
1382
- if (callFeed.isLocal() && this.userMediaFeeds.length > 1) continue;
1383
-
1384
- const total = callFeed.speakingVolumeSamples.reduce(
1385
- (acc, volume) => acc + Math.max(volume, SPEAKING_THRESHOLD),
1386
- );
1387
- const avg = total / callFeed.speakingVolumeSamples.length;
1388
-
1389
- if (!topAvg || avg > topAvg) {
1390
- topAvg = avg;
1391
- nextActiveSpeaker = callFeed;
1392
- }
1393
- }
1394
-
1395
- if (nextActiveSpeaker && this.activeSpeaker !== nextActiveSpeaker && topAvg && topAvg > SPEAKING_THRESHOLD) {
1396
- this.activeSpeaker = nextActiveSpeaker;
1397
- this.emit(GroupCallEvent.ActiveSpeakerChanged, this.activeSpeaker);
1398
- }
1399
- };
1400
-
1401
- /*
1402
- * Screenshare Call Feed Event Handlers
1403
- */
1404
-
1405
- public getScreenshareFeed(userId: string, deviceId: string): CallFeed | undefined {
1406
- return this.screenshareFeeds.find((f) => f.userId === userId && f.deviceId! === deviceId);
1407
- }
1408
-
1409
- private addScreenshareFeed(callFeed: CallFeed): void {
1410
- this.screenshareFeeds.push(callFeed);
1411
- this.emit(GroupCallEvent.ScreenshareFeedsChanged, this.screenshareFeeds);
1412
- }
1413
-
1414
- private replaceScreenshareFeed(existingFeed: CallFeed, replacementFeed: CallFeed): void {
1415
- const feedIndex = this.screenshareFeeds.findIndex(
1416
- (f) => f.userId === existingFeed.userId && f.deviceId! === existingFeed.deviceId,
1417
- );
1418
-
1419
- if (feedIndex === -1) {
1420
- throw new Error("Couldn't find screenshare feed to replace");
1421
- }
1422
-
1423
- this.screenshareFeeds.splice(feedIndex, 1, replacementFeed);
1424
-
1425
- existingFeed.dispose();
1426
- this.emit(GroupCallEvent.ScreenshareFeedsChanged, this.screenshareFeeds);
1427
- }
1428
-
1429
- private removeScreenshareFeed(callFeed: CallFeed): void {
1430
- const feedIndex = this.screenshareFeeds.findIndex(
1431
- (f) => f.userId === callFeed.userId && f.deviceId! === callFeed.deviceId,
1432
- );
1433
-
1434
- if (feedIndex === -1) {
1435
- throw new Error("Couldn't find screenshare feed to remove");
1436
- }
1437
-
1438
- this.screenshareFeeds.splice(feedIndex, 1);
1439
-
1440
- callFeed.dispose();
1441
- this.emit(GroupCallEvent.ScreenshareFeedsChanged, this.screenshareFeeds);
1442
- }
1443
-
1444
- /**
1445
- * Recalculates and updates the participant map to match the room state.
1446
- */
1447
- private updateParticipants(): void {
1448
- const localMember = this.room.getMember(this.client.getUserId()!)!;
1449
- if (!localMember) {
1450
- // The client hasn't fetched enough of the room state to get our own member
1451
- // event. This probably shouldn't happen, but sanity check & exit for now.
1452
- logger.warn(
1453
- `GroupCall ${this.groupCallId} updateParticipants() tried to update participants before local room member is available`,
1454
- );
1455
- return;
1456
- }
1457
-
1458
- if (this.participantsExpirationTimer !== null) {
1459
- clearTimeout(this.participantsExpirationTimer);
1460
- this.participantsExpirationTimer = null;
1461
- }
1462
-
1463
- if (this.state === GroupCallState.Ended) {
1464
- this.participants = new Map();
1465
- return;
1466
- }
1467
-
1468
- const participants = new Map<RoomMember, Map<string, ParticipantState>>();
1469
- const now = Date.now();
1470
- const entered = this.state === GroupCallState.Entered || this.enteredViaAnotherSession;
1471
- let nextExpiration = Infinity;
1472
-
1473
- for (const e of this.getMemberStateEvents()) {
1474
- const member = this.room.getMember(e.getStateKey()!);
1475
- const content = e.getContent<Record<any, unknown>>();
1476
- const calls: Record<any, unknown>[] = Array.isArray(content["m.calls"]) ? content["m.calls"] : [];
1477
- const call = calls.find((call) => call["m.call_id"] === this.groupCallId);
1478
- const devices: Record<any, unknown>[] = Array.isArray(call?.["m.devices"]) ? call!["m.devices"] : [];
1479
-
1480
- // Filter out invalid and expired devices
1481
- let validDevices = devices.filter(
1482
- (d) =>
1483
- typeof d.device_id === "string" &&
1484
- typeof d.session_id === "string" &&
1485
- typeof d.expires_ts === "number" &&
1486
- d.expires_ts > now &&
1487
- Array.isArray(d.feeds),
1488
- ) as unknown as IGroupCallRoomMemberDevice[];
1489
-
1490
- // Apply local echo for the unentered case
1491
- if (!entered && member?.userId === this.client.getUserId()!) {
1492
- validDevices = validDevices.filter((d) => d.device_id !== this.client.getDeviceId()!);
1493
- }
1494
-
1495
- // Must have a connected device and be joined to the room
1496
- if (validDevices.length > 0 && member?.membership === KnownMembership.Join) {
1497
- const deviceMap = new Map<string, ParticipantState>();
1498
- participants.set(member, deviceMap);
1499
-
1500
- for (const d of validDevices) {
1501
- deviceMap.set(d.device_id, {
1502
- sessionId: d.session_id,
1503
- screensharing: d.feeds.some((f) => f.purpose === SDPStreamMetadataPurpose.Screenshare),
1504
- });
1505
- if (d.expires_ts < nextExpiration) nextExpiration = d.expires_ts;
1506
- }
1507
- }
1508
- }
1509
-
1510
- // Apply local echo for the entered case
1511
- if (entered) {
1512
- let deviceMap = participants.get(localMember);
1513
- if (deviceMap === undefined) {
1514
- deviceMap = new Map();
1515
- participants.set(localMember, deviceMap);
1516
- }
1517
-
1518
- if (!deviceMap.has(this.client.getDeviceId()!)) {
1519
- deviceMap.set(this.client.getDeviceId()!, {
1520
- sessionId: this.client.getSessionId(),
1521
- screensharing: this.getLocalFeeds().some((f) => f.purpose === SDPStreamMetadataPurpose.Screenshare),
1522
- });
1523
- }
1524
- }
1525
-
1526
- this.participants = participants;
1527
- if (nextExpiration < Infinity) {
1528
- this.participantsExpirationTimer = setTimeout(() => this.updateParticipants(), nextExpiration - now);
1529
- }
1530
- }
1531
-
1532
- /**
1533
- * Updates the local user's member state with the devices returned by the given function.
1534
- * @param fn - A function from the current devices to the new devices. If it
1535
- * returns null, the update will be skipped.
1536
- * @param keepAlive - Whether the request should outlive the window.
1537
- */
1538
- private async updateDevices(
1539
- fn: (devices: IGroupCallRoomMemberDevice[]) => IGroupCallRoomMemberDevice[] | null,
1540
- keepAlive = false,
1541
- ): Promise<void> {
1542
- const now = Date.now();
1543
- const localUserId = this.client.getUserId()!;
1544
-
1545
- const event = this.getMemberStateEvents(localUserId);
1546
- const content = event?.getContent<Record<any, unknown>>() ?? {};
1547
- const calls: Record<any, unknown>[] = Array.isArray(content["m.calls"]) ? content["m.calls"] : [];
1548
-
1549
- let call: Record<any, unknown> | null = null;
1550
- const otherCalls: Record<any, unknown>[] = [];
1551
- for (const c of calls) {
1552
- if (c["m.call_id"] === this.groupCallId) {
1553
- call = c;
1554
- } else {
1555
- otherCalls.push(c);
1556
- }
1557
- }
1558
- if (call === null) call = {};
1559
-
1560
- const devices: Record<any, unknown>[] = Array.isArray(call["m.devices"]) ? call["m.devices"] : [];
1561
-
1562
- // Filter out invalid and expired devices
1563
- const validDevices = devices.filter(
1564
- (d) =>
1565
- typeof d.device_id === "string" &&
1566
- typeof d.session_id === "string" &&
1567
- typeof d.expires_ts === "number" &&
1568
- d.expires_ts > now &&
1569
- Array.isArray(d.feeds),
1570
- ) as unknown as IGroupCallRoomMemberDevice[];
1571
-
1572
- const newDevices = fn(validDevices);
1573
- if (newDevices === null) return;
1574
-
1575
- const newCalls = [...(otherCalls as unknown as IGroupCallRoomMemberCallState[])];
1576
- if (newDevices.length > 0) {
1577
- newCalls.push({
1578
- ...call,
1579
- "m.call_id": this.groupCallId,
1580
- "m.devices": newDevices,
1581
- });
1582
- }
1583
-
1584
- const newContent: IGroupCallRoomMemberState = { "m.calls": newCalls };
1585
-
1586
- await this.client.sendStateEvent(this.room.roomId, EventType.GroupCallMemberPrefix, newContent, localUserId, {
1587
- keepAlive,
1588
- });
1589
- }
1590
-
1591
- private async addDeviceToMemberState(): Promise<void> {
1592
- await this.updateDevices((devices) => [
1593
- ...devices.filter((d) => d.device_id !== this.client.getDeviceId()!),
1594
- {
1595
- device_id: this.client.getDeviceId()!,
1596
- session_id: this.client.getSessionId(),
1597
- expires_ts: Date.now() + DEVICE_TIMEOUT,
1598
- feeds: this.getLocalFeeds().map((feed) => ({ purpose: feed.purpose })),
1599
- // TODO: Add data channels
1600
- },
1601
- ]);
1602
- }
1603
-
1604
- private async updateMemberState(): Promise<void> {
1605
- // Clear the old update interval before proceeding
1606
- if (this.resendMemberStateTimer !== null) {
1607
- clearInterval(this.resendMemberStateTimer);
1608
- this.resendMemberStateTimer = null;
1609
- }
1610
-
1611
- if (this.state === GroupCallState.Entered) {
1612
- // Add the local device
1613
- await this.addDeviceToMemberState();
1614
-
1615
- // Resend the state event every so often so it doesn't become stale
1616
- this.resendMemberStateTimer = setInterval(
1617
- async () => {
1618
- logger.log(`GroupCall ${this.groupCallId} updateMemberState() resending call member state"`);
1619
- try {
1620
- await this.addDeviceToMemberState();
1621
- } catch (e) {
1622
- logger.error(
1623
- `GroupCall ${this.groupCallId} updateMemberState() failed to resend call member state`,
1624
- e,
1625
- );
1626
- }
1627
- },
1628
- (DEVICE_TIMEOUT * 3) / 4,
1629
- );
1630
- } else {
1631
- // Remove the local device
1632
- await this.updateDevices(
1633
- (devices) => devices.filter((d) => d.device_id !== this.client.getDeviceId()!),
1634
- true,
1635
- );
1636
- }
1637
- }
1638
-
1639
- /**
1640
- * Cleans up our member state by filtering out logged out devices, inactive
1641
- * devices, and our own device (if we know we haven't entered).
1642
- */
1643
- public async cleanMemberState(): Promise<void> {
1644
- const { devices: myDevices } = await this.client.getDevices();
1645
- const deviceMap = new Map<string, IMyDevice>(myDevices.map((d) => [d.device_id, d]));
1646
-
1647
- // updateDevices takes care of filtering out inactive devices for us
1648
- await this.updateDevices((devices) => {
1649
- const newDevices = devices.filter((d) => {
1650
- const device = deviceMap.get(d.device_id);
1651
- return (
1652
- device?.last_seen_ts !== undefined &&
1653
- !(
1654
- d.device_id === this.client.getDeviceId()! &&
1655
- this.state !== GroupCallState.Entered &&
1656
- !this.enteredViaAnotherSession
1657
- )
1658
- );
1659
- });
1660
-
1661
- // Skip the update if the devices are unchanged
1662
- return newDevices.length === devices.length ? null : newDevices;
1663
- });
1664
- }
1665
-
1666
- private onRoomState = (): void => this.updateParticipants();
1667
-
1668
- private onParticipantsChanged = (): void => {
1669
- // Re-run setTracksEnabled on all calls, so that participants that just
1670
- // left get denied access to our media, and participants that just
1671
- // joined get granted access
1672
- this.forEachCall((call) => {
1673
- const expected = this.callExpected(call);
1674
- for (const feed of call.getLocalFeeds()) {
1675
- setTracksEnabled(feed.stream.getAudioTracks(), !feed.isAudioMuted() && expected);
1676
- setTracksEnabled(feed.stream.getVideoTracks(), !feed.isVideoMuted() && expected);
1677
- }
1678
- });
1679
-
1680
- if (this.state === GroupCallState.Entered && !this.useLivekit) this.placeOutgoingCalls();
1681
-
1682
- // Update the participants stored in the stats object
1683
- };
1684
-
1685
- private onStateChanged = (newState: GroupCallState, oldState: GroupCallState): void => {
1686
- if (
1687
- newState === GroupCallState.Entered ||
1688
- oldState === GroupCallState.Entered ||
1689
- newState === GroupCallState.Ended
1690
- ) {
1691
- // We either entered, left, or ended the call
1692
- this.updateParticipants();
1693
- this.updateMemberState().catch((e) =>
1694
- logger.error(
1695
- `GroupCall ${this.groupCallId} onStateChanged() failed to update member state devices"`,
1696
- e,
1697
- ),
1698
- );
1699
- }
1700
- };
1701
-
1702
- private onLocalFeedsChanged = (): void => {
1703
- if (this.state === GroupCallState.Entered) {
1704
- this.updateMemberState().catch((e) =>
1705
- logger.error(
1706
- `GroupCall ${this.groupCallId} onLocalFeedsChanged() failed to update member state feeds`,
1707
- e,
1708
- ),
1709
- );
1710
- }
1711
- };
1712
-
1713
- public getGroupCallStats(): GroupCallStats {
1714
- if (this.stats === undefined) {
1715
- const userID = this.client.getUserId() || "unknown";
1716
- this.stats = new GroupCallStats(this.groupCallId, userID, this.statsCollectIntervalTime);
1717
- this.stats.reports.on(StatsReport.CONNECTION_STATS, this.onConnectionStats);
1718
- this.stats.reports.on(StatsReport.BYTE_SENT_STATS, this.onByteSentStats);
1719
- this.stats.reports.on(StatsReport.SUMMARY_STATS, this.onSummaryStats);
1720
- this.stats.reports.on(StatsReport.CALL_FEED_REPORT, this.onCallFeedReport);
1721
- }
1722
- return this.stats;
1723
- }
1724
-
1725
- public setGroupCallStatsInterval(interval: number): void {
1726
- this.statsCollectIntervalTime = interval;
1727
- if (this.stats !== undefined) {
1728
- this.stats.stop();
1729
- this.stats.setInterval(interval);
1730
- if (interval > 0) {
1731
- this.stats.start();
1732
- }
1733
- }
1734
- }
1735
- }