Skip to content

Commit aef89e4

Browse files
authored
Merge pull request #1812 from SimonBrandner/feature/muting
Support for MSC3291: Muting in VoIP calls
2 parents 8a1bc98 + e6696f7 commit aef89e4

File tree

8 files changed

+178
-32
lines changed

8 files changed

+178
-32
lines changed

spec/unit/utils.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,4 +493,68 @@ describe("utils", function() {
493493
expect(deepSortedObjectEntries(input)).toMatchObject(output);
494494
});
495495
});
496+
497+
describe("recursivelyAssign", () => {
498+
it("doesn't override with null/undefined", () => {
499+
const result = utils.recursivelyAssign(
500+
{
501+
string: "Hello world",
502+
object: {},
503+
float: 0.1,
504+
}, {
505+
string: null,
506+
object: undefined,
507+
},
508+
true,
509+
);
510+
511+
expect(result).toStrictEqual({
512+
string: "Hello world",
513+
object: {},
514+
float: 0.1,
515+
});
516+
});
517+
518+
it("assigns recursively", () => {
519+
const result = utils.recursivelyAssign(
520+
{
521+
number: 42,
522+
object: {
523+
message: "Hello world",
524+
day: "Monday",
525+
langs: {
526+
compiled: ["c++"],
527+
},
528+
},
529+
thing: "string",
530+
}, {
531+
number: 2,
532+
object: {
533+
message: "How are you",
534+
day: "Friday",
535+
langs: {
536+
compiled: ["c++", "c"],
537+
},
538+
},
539+
thing: {
540+
aSubThing: "something",
541+
},
542+
},
543+
);
544+
545+
expect(result).toStrictEqual({
546+
number: 2,
547+
object: {
548+
message: "How are you",
549+
day: "Friday",
550+
langs: {
551+
compiled: ["c++", "c"],
552+
},
553+
},
554+
thing: {
555+
aSubThing: "something",
556+
},
557+
});
558+
});
559+
});
496560
});

spec/unit/webrtc/call.spec.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -321,16 +321,19 @@ describe('Call', function() {
321321
[SDPStreamMetadataKey]: {
322322
"stream_id": {
323323
purpose: SDPStreamMetadataPurpose.Usermedia,
324+
audio_muted: true,
325+
video_muted: false,
324326
},
325327
},
326328
};
327329
},
328330
});
329331

330-
call.pushRemoteFeed({ id: "stream_id" });
331-
expect(call.getFeeds().find((feed) => {
332-
return feed.stream.id === "stream_id";
333-
})?.purpose).toBe(SDPStreamMetadataPurpose.Usermedia);
332+
call.pushRemoteFeed({ id: "stream_id", getAudioTracks: () => ["track1"], getVideoTracks: () => ["track1"] });
333+
const feed = call.getFeeds().find((feed) => feed.stream.id === "stream_id");
334+
expect(feed?.purpose).toBe(SDPStreamMetadataPurpose.Usermedia);
335+
expect(feed?.isAudioMuted()).toBeTruthy();
336+
expect(feed?.isVideoMuted()).not.toBeTruthy();
334337
});
335338

336339
it("should fallback to replaceTrack() if the other side doesn't support SPDStreamMetadata", async () => {

src/@types/event.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ export enum EventType {
5353
CallReject = "m.call.reject",
5454
CallSelectAnswer = "m.call.select_answer",
5555
CallNegotiate = "m.call.negotiate",
56+
CallSDPStreamMetadataChanged = "m.call.sdp_stream_metadata_changed",
57+
CallSDPStreamMetadataChangedPrefix = "org.matrix.call.sdp_stream_metadata_changed",
5658
CallReplaces = "m.call.replaces",
5759
CallAssertedIdentity = "m.call.asserted_identity",
5860
CallAssertedIdentityPrefix = "org.matrix.call.asserted_identity",

src/utils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,3 +694,25 @@ const collator = new Intl.Collator();
694694
export function compare(a: string, b: string): number {
695695
return collator.compare(a, b);
696696
}
697+
698+
/**
699+
* This function is similar to Object.assign() but it assigns recursively and
700+
* allows you to ignore nullish values from the source
701+
*
702+
* @param {Object} target
703+
* @param {Object} source
704+
* @returns the target object
705+
*/
706+
export function recursivelyAssign(target: Object, source: Object, ignoreNullish = false): any {
707+
for (const [sourceKey, sourceValue] of Object.entries(source)) {
708+
if (target[sourceKey] instanceof Object && sourceValue) {
709+
recursivelyAssign(target[sourceKey], sourceValue);
710+
continue;
711+
}
712+
if ((sourceValue !== null && sourceValue !== undefined) || !ignoreNullish) {
713+
target[sourceKey] = sourceValue;
714+
continue;
715+
}
716+
}
717+
return target;
718+
}

src/webrtc/call.ts

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
SDPStreamMetadataPurpose,
3737
SDPStreamMetadata,
3838
SDPStreamMetadataKey,
39+
MCallSDPStreamMetadataChanged,
3940
} from './callEventTypes';
4041
import { CallFeed } from './callFeed';
4142

@@ -353,8 +354,6 @@ export class MatrixCall extends EventEmitter {
353354
this.makingOffer = false;
354355

355356
this.remoteOnHold = false;
356-
this.micMuted = false;
357-
this.vidMuted = false;
358357

359358
this.feeds = [];
360359

@@ -402,6 +401,14 @@ export class MatrixCall extends EventEmitter {
402401
return this.remoteAssertedIdentity;
403402
}
404403

404+
public get localUsermediaFeed(): CallFeed {
405+
return this.getLocalFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Usermedia);
406+
}
407+
408+
private getFeedByStreamId(streamId: string): CallFeed {
409+
return this.getFeeds().find((feed) => feed.stream.id === streamId);
410+
}
411+
405412
/**
406413
* Returns an array of all CallFeeds
407414
* @returns {Array<CallFeed>} CallFeeds
@@ -431,10 +438,12 @@ export class MatrixCall extends EventEmitter {
431438
* @returns {SDPStreamMetadata} localSDPStreamMetadata
432439
*/
433440
private getLocalSDPStreamMetadata(): SDPStreamMetadata {
434-
const metadata = {};
441+
const metadata: SDPStreamMetadata = {};
435442
for (const localFeed of this.getLocalFeeds()) {
436443
metadata[localFeed.stream.id] = {
437444
purpose: localFeed.purpose,
445+
audio_muted: localFeed.isAudioMuted(),
446+
video_muted: localFeed.isVideoMuted(),
438447
};
439448
}
440449
logger.debug("Got local SDPStreamMetadata", metadata);
@@ -459,6 +468,8 @@ export class MatrixCall extends EventEmitter {
459468

460469
const userId = this.getOpponentMember().userId;
461470
const purpose = this.remoteSDPStreamMetadata[stream.id].purpose;
471+
const audioMuted = this.remoteSDPStreamMetadata[stream.id].audio_muted;
472+
const videoMuted = this.remoteSDPStreamMetadata[stream.id].video_muted;
462473

463474
if (!purpose) {
464475
logger.warn(`Ignoring stream with id ${stream.id} because we didn't get any metadata about it`);
@@ -471,7 +482,7 @@ export class MatrixCall extends EventEmitter {
471482
if (existingFeed) {
472483
existingFeed.setNewStream(stream);
473484
} else {
474-
this.feeds.push(new CallFeed(stream, userId, purpose, this.client, this.roomId));
485+
this.feeds.push(new CallFeed(stream, userId, purpose, this.client, this.roomId, audioMuted, videoMuted));
475486
this.emit(CallEvent.FeedsChanged, this.feeds);
476487
}
477488

@@ -498,11 +509,11 @@ export class MatrixCall extends EventEmitter {
498509

499510
// Try to find a feed with the same stream id as the new stream,
500511
// if we find it replace the old stream with the new one
501-
const feed = this.feeds.find((feed) => feed.stream.id === stream.id);
512+
const feed = this.getFeedByStreamId(stream.id);
502513
if (feed) {
503514
feed.setNewStream(stream);
504515
} else {
505-
this.feeds.push(new CallFeed(stream, userId, purpose, this.client, this.roomId));
516+
this.feeds.push(new CallFeed(stream, userId, purpose, this.client, this.roomId, false, false));
506517
this.emit(CallEvent.FeedsChanged, this.feeds);
507518
}
508519

@@ -517,7 +528,7 @@ export class MatrixCall extends EventEmitter {
517528
if (existingFeed) {
518529
existingFeed.setNewStream(stream);
519530
} else {
520-
this.feeds.push(new CallFeed(stream, userId, purpose, this.client, this.roomId));
531+
this.feeds.push(new CallFeed(stream, userId, purpose, this.client, this.roomId, false, false));
521532
this.emit(CallEvent.FeedsChanged, this.feeds);
522533
}
523534

@@ -555,7 +566,7 @@ export class MatrixCall extends EventEmitter {
555566
private deleteFeedByStream(stream: MediaStream) {
556567
logger.debug(`Removing feed with stream id ${stream.id}`);
557568

558-
const feed = this.feeds.find((feed) => feed.stream.id === stream.id);
569+
const feed = this.getFeedByStreamId(stream.id);
559570
if (!feed) {
560571
logger.warn(`Didn't find the feed with stream id ${stream.id} to delete`);
561572
return;
@@ -605,7 +616,7 @@ export class MatrixCall extends EventEmitter {
605616

606617
const sdpStreamMetadata = invite[SDPStreamMetadataKey];
607618
if (sdpStreamMetadata) {
608-
this.remoteSDPStreamMetadata = sdpStreamMetadata;
619+
this.updateRemoteSDPStreamMetadata(sdpStreamMetadata);
609620
} else {
610621
logger.debug("Did not get any SDPStreamMetadata! Can not send/receive multiple streams");
611622
}
@@ -891,7 +902,7 @@ export class MatrixCall extends EventEmitter {
891902
* @param {boolean} muted True to mute the outbound video.
892903
*/
893904
setLocalVideoMuted(muted: boolean) {
894-
this.vidMuted = muted;
905+
this.localUsermediaFeed?.setVideoMuted(muted);
895906
this.updateMuteStatus();
896907
}
897908

@@ -905,16 +916,15 @@ export class MatrixCall extends EventEmitter {
905916
* (including if the call is not set up yet).
906917
*/
907918
isLocalVideoMuted(): boolean {
908-
if (this.type === CallType.Voice) return true;
909-
return this.vidMuted;
919+
return this.localUsermediaFeed?.isVideoMuted();
910920
}
911921

912922
/**
913923
* Set whether the microphone should be muted or not.
914924
* @param {boolean} muted True to mute the mic.
915925
*/
916926
setMicrophoneMuted(muted: boolean) {
917-
this.micMuted = muted;
927+
this.localUsermediaFeed?.setAudioMuted(muted);
918928
this.updateMuteStatus();
919929
}
920930

@@ -928,7 +938,7 @@ export class MatrixCall extends EventEmitter {
928938
* is not set up yet).
929939
*/
930940
isMicrophoneMuted(): boolean {
931-
return this.micMuted;
941+
return this.localUsermediaFeed?.isAudioMuted();
932942
}
933943

934944
/**
@@ -991,14 +1001,14 @@ export class MatrixCall extends EventEmitter {
9911001
}
9921002

9931003
private updateMuteStatus() {
994-
if (!this.localAVStream) {
995-
return;
996-
}
1004+
this.sendVoipEvent(EventType.CallSDPStreamMetadataChangedPrefix, {
1005+
[SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(),
1006+
});
9971007

998-
const micShouldBeMuted = this.micMuted || this.remoteOnHold;
999-
setTracksEnabled(this.localAVStream.getAudioTracks(), !micShouldBeMuted);
1008+
const micShouldBeMuted = this.localUsermediaFeed?.isAudioMuted() || this.remoteOnHold;
1009+
const vidShouldBeMuted = this.localUsermediaFeed?.isVideoMuted() || this.remoteOnHold;
10001010

1001-
const vidShouldBeMuted = this.vidMuted || this.remoteOnHold;
1011+
setTracksEnabled(this.localAVStream.getAudioTracks(), !micShouldBeMuted);
10021012
setTracksEnabled(this.localAVStream.getVideoTracks(), !vidShouldBeMuted);
10031013
}
10041014

@@ -1214,7 +1224,7 @@ export class MatrixCall extends EventEmitter {
12141224

12151225
const sdpStreamMetadata = event.getContent()[SDPStreamMetadataKey];
12161226
if (sdpStreamMetadata) {
1217-
this.remoteSDPStreamMetadata = sdpStreamMetadata;
1227+
this.updateRemoteSDPStreamMetadata(sdpStreamMetadata);
12181228
} else {
12191229
logger.warn("Did not get any SDPStreamMetadata! Can not send/receive multiple streams");
12201230
}
@@ -1289,9 +1299,9 @@ export class MatrixCall extends EventEmitter {
12891299

12901300
const prevLocalOnHold = this.isLocalOnHold();
12911301

1292-
const metadata = event.getContent()[SDPStreamMetadataKey];
1293-
if (metadata) {
1294-
this.remoteSDPStreamMetadata = metadata;
1302+
const sdpStreamMetadata = event.getContent()[SDPStreamMetadataKey];
1303+
if (sdpStreamMetadata) {
1304+
this.updateRemoteSDPStreamMetadata(sdpStreamMetadata);
12951305
} else {
12961306
logger.warn("Received negotiation event without SDPStreamMetadata!");
12971307
}
@@ -1321,6 +1331,22 @@ export class MatrixCall extends EventEmitter {
13211331
}
13221332
}
13231333

1334+
private updateRemoteSDPStreamMetadata(metadata: SDPStreamMetadata): void {
1335+
this.remoteSDPStreamMetadata = utils.recursivelyAssign(this.remoteSDPStreamMetadata || {}, metadata, true);
1336+
for (const feed of this.getRemoteFeeds()) {
1337+
const streamId = feed.stream.id;
1338+
feed.setAudioMuted(this.remoteSDPStreamMetadata[streamId]?.audio_muted);
1339+
feed.setVideoMuted(this.remoteSDPStreamMetadata[streamId]?.video_muted);
1340+
feed.purpose = this.remoteSDPStreamMetadata[streamId]?.purpose;
1341+
}
1342+
}
1343+
1344+
public onSDPStreamMetadataChangedReceived(event: MatrixEvent): void {
1345+
const content = event.getContent<MCallSDPStreamMetadataChanged>();
1346+
const metadata = content[SDPStreamMetadataKey];
1347+
this.updateRemoteSDPStreamMetadata(metadata);
1348+
}
1349+
13241350
async onAssertedIdentityReceived(event: MatrixEvent) {
13251351
if (!event.getContent().asserted_identity) return;
13261352

src/webrtc/callEventHandler.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,18 @@ export class CallEventHandler {
297297
}
298298

299299
call.onAssertedIdentityReceived(event);
300+
} else if (
301+
event.getType() === EventType.CallSDPStreamMetadataChanged ||
302+
event.getType() === EventType.CallSDPStreamMetadataChangedPrefix
303+
) {
304+
if (!call) return;
305+
306+
if (event.getContent().party_id === call.ourPartyId) {
307+
// Ignore remote echo
308+
return;
309+
}
310+
311+
call.onSDPStreamMetadataChangedReceived(event);
300312
}
301313
}
302314
}

src/webrtc/callEventTypes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export enum SDPStreamMetadataPurpose {
1111

1212
export interface SDPStreamMetadataObject {
1313
purpose: SDPStreamMetadataPurpose;
14+
audio_muted: boolean;
15+
video_muted: boolean;
1416
}
1517

1618
export interface SDPStreamMetadata {
@@ -41,6 +43,10 @@ export interface MCallOfferNegotiate {
4143
[SDPStreamMetadataKey]: SDPStreamMetadata;
4244
}
4345

46+
export interface MCallSDPStreamMetadataChanged {
47+
[SDPStreamMetadataKey]: SDPStreamMetadata;
48+
}
49+
4450
export interface MCallReplacesTarget {
4551
id: string;
4652
display_name: string;

0 commit comments

Comments
 (0)