Skip to content

Commit 8d9cd0f

Browse files
authored
Support for PTT group call mode (#2338)
* Add PTT call mode & mute by default in PTT calls (#2311) No other parts of PTT calls implemented yet * Make the tests pass again (#2316) 3280394 made call use a bunch of methods that weren't mocked in the tests. * Add maximum trasmit time for PTT (#2312) on sender side by muting mic after the max transmit time has elapsed. * Don't allow user to unmute if another user is speaking (#2313) * Add maximum trasmit time for PTT on sender side by muting mic after the max transmit time has elapsed. * Don't allow user to unmute if another user is speaking Based on #2312 For element-hq/element-call#298 * Fix createGroupCall arguments (#2325) Comma instead of a colon...
1 parent 96ba061 commit 8d9cd0f

File tree

4 files changed

+72
-1
lines changed

4 files changed

+72
-1
lines changed

spec/unit/webrtc/call.spec.ts

+18
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,14 @@ describe('Call', function() {
145145
client.client.mediaHandler = new MockMediaHandler;
146146
client.client.getMediaHandler = () => client.client.mediaHandler;
147147
client.httpBackend.when("GET", "/voip/turnServer").respond(200, {});
148+
client.client.getRoom = () => {
149+
return {
150+
getMember: () => {
151+
return {};
152+
},
153+
};
154+
};
155+
148156
call = new MatrixCall({
149157
client: client.client,
150158
roomId: '!foo:bar',
@@ -175,6 +183,7 @@ describe('Call', function() {
175183
},
176184
};
177185
},
186+
getSender: () => "@test:foo",
178187
});
179188

180189
call.peerConn.addIceCandidate = jest.fn();
@@ -192,6 +201,7 @@ describe('Call', function() {
192201
],
193202
};
194203
},
204+
getSender: () => "@test:foo",
195205
});
196206
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
197207

@@ -209,6 +219,7 @@ describe('Call', function() {
209219
],
210220
};
211221
},
222+
getSender: () => "@test:foo",
212223
});
213224
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
214225

@@ -236,6 +247,7 @@ describe('Call', function() {
236247
],
237248
};
238249
},
250+
getSender: () => "@test:foo",
239251
});
240252

241253
call.onRemoteIceCandidatesReceived({
@@ -252,6 +264,7 @@ describe('Call', function() {
252264
],
253265
};
254266
},
267+
getSender: () => "@test:foo",
255268
});
256269

257270
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(0);
@@ -267,6 +280,7 @@ describe('Call', function() {
267280
},
268281
};
269282
},
283+
getSender: () => "@test:foo",
270284
});
271285

272286
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
@@ -291,6 +305,7 @@ describe('Call', function() {
291305
},
292306
};
293307
},
308+
getSender: () => "@test:foo",
294309
});
295310

296311
const identChangedCallback = jest.fn();
@@ -308,6 +323,7 @@ describe('Call', function() {
308323
},
309324
};
310325
},
326+
getSender: () => "@test:foo",
311327
});
312328

313329
expect(identChangedCallback).toHaveBeenCalled();
@@ -347,6 +363,7 @@ describe('Call', function() {
347363
},
348364
};
349365
},
366+
getSender: () => "@test:foo",
350367
});
351368

352369
call.pushRemoteFeed(new MockMediaStream("remote_stream"));
@@ -376,6 +393,7 @@ describe('Call', function() {
376393
},
377394
};
378395
},
396+
getSender: () => "@test:foo",
379397
});
380398

381399
call.setScreensharingEnabledWithoutMetadataSupport = jest.fn();

src/client.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1322,6 +1322,7 @@ export class MatrixClient extends EventEmitter {
13221322
public async createGroupCall(
13231323
roomId: string,
13241324
type: GroupCallType,
1325+
isPtt: boolean,
13251326
intent: GroupCallIntent,
13261327
dataChannelsEnabled?: boolean,
13271328
dataChannelOptions?: IGroupCallDataChannelOptions,
@@ -1340,6 +1341,7 @@ export class MatrixClient extends EventEmitter {
13401341
this,
13411342
room,
13421343
type,
1344+
isPtt,
13431345
intent,
13441346
undefined,
13451347
dataChannelsEnabled,

src/webrtc/groupCall.ts

+49-1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ export class GroupCallError extends Error {
5959
}
6060
}
6161

62+
export class OtherUserSpeakingError extends Error {
63+
constructor() {
64+
super("Cannot unmute: another user is speaking");
65+
}
66+
}
67+
6268
export interface IGroupCallDataChannelOptions {
6369
ordered: boolean;
6470
maxPacketLifeTime: number;
@@ -112,6 +118,7 @@ export class GroupCall extends EventEmitter {
112118
public activeSpeakerInterval = 1000;
113119
public retryCallInterval = 5000;
114120
public participantTimeout = 1000 * 15;
121+
public pttMaxTransmitTime = 1000 * 20;
115122

116123
public state = GroupCallState.LocalCallFeedUninitialized;
117124
public activeSpeaker?: string; // userId
@@ -129,11 +136,13 @@ export class GroupCall extends EventEmitter {
129136
private retryCallLoopTimeout?: number;
130137
private retryCallCounts: Map<string, number> = new Map();
131138
private reEmitter: ReEmitter;
139+
private transmitTimer: number | null = null;
132140

133141
constructor(
134142
private client: MatrixClient,
135143
public room: Room,
136144
public type: GroupCallType,
145+
public isPtt: boolean,
137146
public intent: GroupCallIntent,
138147
groupCallId?: string,
139148
private dataChannelsEnabled?: boolean,
@@ -160,6 +169,7 @@ export class GroupCall extends EventEmitter {
160169
{
161170
"m.intent": this.intent,
162171
"m.type": this.type,
172+
"io.element.ptt": this.isPtt,
163173
// TODO: Specify datachannels
164174
"dataChannelsEnabled": this.dataChannelsEnabled,
165175
"dataChannelOptions": this.dataChannelOptions,
@@ -208,6 +218,11 @@ export class GroupCall extends EventEmitter {
208218
throw error;
209219
}
210220

221+
// start muted on ptt calls
222+
if (this.isPtt) {
223+
setTracksEnabled(stream.getAudioTracks(), false);
224+
}
225+
211226
const userId = this.client.getUserId();
212227

213228
const callFeed = new CallFeed({
@@ -216,7 +231,7 @@ export class GroupCall extends EventEmitter {
216231
userId,
217232
stream,
218233
purpose: SDPStreamMetadataPurpose.Usermedia,
219-
audioMuted: stream.getAudioTracks().length === 0,
234+
audioMuted: stream.getAudioTracks().length === 0 || this.isPtt,
220235
videoMuted: stream.getVideoTracks().length === 0,
221236
});
222237

@@ -318,17 +333,32 @@ export class GroupCall extends EventEmitter {
318333
this.retryCallCounts.clear();
319334
clearTimeout(this.retryCallLoopTimeout);
320335

336+
if (this.transmitTimer !== null) {
337+
clearTimeout(this.transmitTimer);
338+
this.transmitTimer = null;
339+
}
340+
321341
this.client.removeListener("Call.incoming", this.onIncomingCall);
322342
}
323343

324344
public leave() {
345+
if (this.transmitTimer !== null) {
346+
clearTimeout(this.transmitTimer);
347+
this.transmitTimer = null;
348+
}
349+
325350
this.dispose();
326351
this.setState(GroupCallState.LocalCallFeedUninitialized);
327352
}
328353

329354
public async terminate(emitStateEvent = true) {
330355
this.dispose();
331356

357+
if (this.transmitTimer !== null) {
358+
clearTimeout(this.transmitTimer);
359+
this.transmitTimer = null;
360+
}
361+
332362
this.participants = [];
333363
this.client.removeListener(
334364
"RoomState.members",
@@ -382,6 +412,24 @@ export class GroupCall extends EventEmitter {
382412
return false;
383413
}
384414

415+
// set a timer for the maximum transmit time on PTT calls
416+
if (this.isPtt) {
417+
// if anoher user is currently unmuted, we can't unmute
418+
if (!muted && this.userMediaFeeds.some(f => !f.isAudioMuted())) {
419+
throw new OtherUserSpeakingError();
420+
}
421+
422+
// Set or clear the max transmit timer
423+
if (!muted && this.isMicrophoneMuted()) {
424+
this.transmitTimer = setTimeout(() => {
425+
this.setMicrophoneMuted(true);
426+
}, this.pttMaxTransmitTime);
427+
} else if (muted && !this.isMicrophoneMuted()) {
428+
clearTimeout(this.transmitTimer);
429+
this.transmitTimer = null;
430+
}
431+
}
432+
385433
if (this.localCallFeed) {
386434
logger.log(`groupCall ${this.groupCallId} setMicrophoneMuted stream ${
387435
this.localCallFeed.stream.id} muted ${muted}`);

src/webrtc/groupCallEventHandler.ts

+3
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ export class GroupCallEventHandler {
9393
return;
9494
}
9595

96+
const isPtt = Boolean(content["io.element.ptt"]);
97+
9698
let dataChannelOptions: IGroupCallDataChannelOptions | undefined;
9799

98100
if (content?.dataChannelsEnabled && content?.dataChannelOptions) {
@@ -105,6 +107,7 @@ export class GroupCallEventHandler {
105107
this.client,
106108
room,
107109
callType,
110+
isPtt,
108111
callIntent,
109112
groupCallId,
110113
content?.dataChannelsEnabled,

0 commit comments

Comments
 (0)