Skip to content

Commit

Permalink
Support for PTT group call mode (#2338)
Browse files Browse the repository at this point in the history
* 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...
  • Loading branch information
dbkr authored May 3, 2022
1 parent 96ba061 commit 8d9cd0f
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 1 deletion.
18 changes: 18 additions & 0 deletions spec/unit/webrtc/call.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,14 @@ describe('Call', function() {
client.client.mediaHandler = new MockMediaHandler;
client.client.getMediaHandler = () => client.client.mediaHandler;
client.httpBackend.when("GET", "/voip/turnServer").respond(200, {});
client.client.getRoom = () => {
return {
getMember: () => {
return {};
},
};
};

call = new MatrixCall({
client: client.client,
roomId: '!foo:bar',
Expand Down Expand Up @@ -175,6 +183,7 @@ describe('Call', function() {
},
};
},
getSender: () => "@test:foo",
});

call.peerConn.addIceCandidate = jest.fn();
Expand All @@ -192,6 +201,7 @@ describe('Call', function() {
],
};
},
getSender: () => "@test:foo",
});
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);

Expand All @@ -209,6 +219,7 @@ describe('Call', function() {
],
};
},
getSender: () => "@test:foo",
});
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);

Expand Down Expand Up @@ -236,6 +247,7 @@ describe('Call', function() {
],
};
},
getSender: () => "@test:foo",
});

call.onRemoteIceCandidatesReceived({
Expand All @@ -252,6 +264,7 @@ describe('Call', function() {
],
};
},
getSender: () => "@test:foo",
});

expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(0);
Expand All @@ -267,6 +280,7 @@ describe('Call', function() {
},
};
},
getSender: () => "@test:foo",
});

expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
Expand All @@ -291,6 +305,7 @@ describe('Call', function() {
},
};
},
getSender: () => "@test:foo",
});

const identChangedCallback = jest.fn();
Expand All @@ -308,6 +323,7 @@ describe('Call', function() {
},
};
},
getSender: () => "@test:foo",
});

expect(identChangedCallback).toHaveBeenCalled();
Expand Down Expand Up @@ -347,6 +363,7 @@ describe('Call', function() {
},
};
},
getSender: () => "@test:foo",
});

call.pushRemoteFeed(new MockMediaStream("remote_stream"));
Expand Down Expand Up @@ -376,6 +393,7 @@ describe('Call', function() {
},
};
},
getSender: () => "@test:foo",
});

call.setScreensharingEnabledWithoutMetadataSupport = jest.fn();
Expand Down
2 changes: 2 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1322,6 +1322,7 @@ export class MatrixClient extends EventEmitter {
public async createGroupCall(
roomId: string,
type: GroupCallType,
isPtt: boolean,
intent: GroupCallIntent,
dataChannelsEnabled?: boolean,
dataChannelOptions?: IGroupCallDataChannelOptions,
Expand All @@ -1340,6 +1341,7 @@ export class MatrixClient extends EventEmitter {
this,
room,
type,
isPtt,
intent,
undefined,
dataChannelsEnabled,
Expand Down
50 changes: 49 additions & 1 deletion src/webrtc/groupCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ export class GroupCallError extends Error {
}
}

export class OtherUserSpeakingError extends Error {
constructor() {
super("Cannot unmute: another user is speaking");
}
}

export interface IGroupCallDataChannelOptions {
ordered: boolean;
maxPacketLifeTime: number;
Expand Down Expand Up @@ -112,6 +118,7 @@ export class GroupCall extends EventEmitter {
public activeSpeakerInterval = 1000;
public retryCallInterval = 5000;
public participantTimeout = 1000 * 15;
public pttMaxTransmitTime = 1000 * 20;

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

constructor(
private client: MatrixClient,
public room: Room,
public type: GroupCallType,
public isPtt: boolean,
public intent: GroupCallIntent,
groupCallId?: string,
private dataChannelsEnabled?: boolean,
Expand All @@ -160,6 +169,7 @@ export class GroupCall extends EventEmitter {
{
"m.intent": this.intent,
"m.type": this.type,
"io.element.ptt": this.isPtt,
// TODO: Specify datachannels
"dataChannelsEnabled": this.dataChannelsEnabled,
"dataChannelOptions": this.dataChannelOptions,
Expand Down Expand Up @@ -208,6 +218,11 @@ export class GroupCall extends EventEmitter {
throw error;
}

// start muted on ptt calls
if (this.isPtt) {
setTracksEnabled(stream.getAudioTracks(), false);
}

const userId = this.client.getUserId();

const callFeed = new CallFeed({
Expand All @@ -216,7 +231,7 @@ export class GroupCall extends EventEmitter {
userId,
stream,
purpose: SDPStreamMetadataPurpose.Usermedia,
audioMuted: stream.getAudioTracks().length === 0,
audioMuted: stream.getAudioTracks().length === 0 || this.isPtt,
videoMuted: stream.getVideoTracks().length === 0,
});

Expand Down Expand Up @@ -318,17 +333,32 @@ export class GroupCall extends EventEmitter {
this.retryCallCounts.clear();
clearTimeout(this.retryCallLoopTimeout);

if (this.transmitTimer !== null) {
clearTimeout(this.transmitTimer);
this.transmitTimer = null;
}

this.client.removeListener("Call.incoming", this.onIncomingCall);
}

public leave() {
if (this.transmitTimer !== null) {
clearTimeout(this.transmitTimer);
this.transmitTimer = null;
}

this.dispose();
this.setState(GroupCallState.LocalCallFeedUninitialized);
}

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

if (this.transmitTimer !== null) {
clearTimeout(this.transmitTimer);
this.transmitTimer = null;
}

this.participants = [];
this.client.removeListener(
"RoomState.members",
Expand Down Expand Up @@ -382,6 +412,24 @@ export class GroupCall extends EventEmitter {
return false;
}

// set a timer for the maximum transmit time on PTT calls
if (this.isPtt) {
// if anoher user is currently unmuted, we can't unmute
if (!muted && this.userMediaFeeds.some(f => !f.isAudioMuted())) {
throw new OtherUserSpeakingError();
}

// Set or clear the max transmit timer
if (!muted && this.isMicrophoneMuted()) {
this.transmitTimer = setTimeout(() => {
this.setMicrophoneMuted(true);
}, this.pttMaxTransmitTime);
} else if (muted && !this.isMicrophoneMuted()) {
clearTimeout(this.transmitTimer);
this.transmitTimer = null;
}
}

if (this.localCallFeed) {
logger.log(`groupCall ${this.groupCallId} setMicrophoneMuted stream ${
this.localCallFeed.stream.id} muted ${muted}`);
Expand Down
3 changes: 3 additions & 0 deletions src/webrtc/groupCallEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ export class GroupCallEventHandler {
return;
}

const isPtt = Boolean(content["io.element.ptt"]);

let dataChannelOptions: IGroupCallDataChannelOptions | undefined;

if (content?.dataChannelsEnabled && content?.dataChannelOptions) {
Expand All @@ -105,6 +107,7 @@ export class GroupCallEventHandler {
this.client,
room,
callType,
isPtt,
callIntent,
groupCallId,
content?.dataChannelsEnabled,
Expand Down

0 comments on commit 8d9cd0f

Please sign in to comment.