Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable group calls without video and audio track by configuration of MatrixClient #3162

Merged
merged 16 commits into from
Mar 2, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions spec/unit/webrtc/groupCall.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const mockGetStateEvents = (type: EventType, userId?: string): MatrixEvent[] | M
const ONE_HOUR = 1000 * 60 * 60;

const createAndEnterGroupCall = async (cli: MatrixClient, room: Room): Promise<GroupCall> => {
const groupCall = new GroupCall(cli, room, GroupCallType.Video, false, GroupCallIntent.Prompt, FAKE_CONF_ID);
const groupCall = new GroupCall(cli, room, GroupCallType.Video, false, GroupCallIntent.Prompt, false, FAKE_CONF_ID);

await groupCall.create();
await groupCall.enter();
Expand All @@ -135,7 +135,7 @@ describe("Group Call", function () {
mockClient = typedMockClient as unknown as MatrixClient;

room = new Room(FAKE_ROOM_ID, mockClient, FAKE_USER_ID_1);
groupCall = new GroupCall(mockClient, room, GroupCallType.Video, false, GroupCallIntent.Prompt);
groupCall = new GroupCall(mockClient, room, GroupCallType.Video, false, GroupCallIntent.Prompt, false);
room.currentState.members[FAKE_USER_ID_1] = {
userId: FAKE_USER_ID_1,
membership: "join",
Expand Down Expand Up @@ -484,7 +484,7 @@ describe("Group Call", function () {
describe("PTT calls", () => {
beforeEach(async () => {
// replace groupcall with a PTT one
groupCall = new GroupCall(mockClient, room, GroupCallType.Video, true, GroupCallIntent.Prompt);
groupCall = new GroupCall(mockClient, room, GroupCallType.Video, true, GroupCallIntent.Prompt, false);

await groupCall.create();

Expand Down Expand Up @@ -647,6 +647,7 @@ describe("Group Call", function () {
GroupCallType.Video,
false,
GroupCallIntent.Prompt,
false,
FAKE_CONF_ID,
);

Expand All @@ -656,6 +657,7 @@ describe("Group Call", function () {
GroupCallType.Video,
false,
GroupCallIntent.Prompt,
false,
FAKE_CONF_ID,
);
});
Expand Down Expand Up @@ -1465,6 +1467,7 @@ describe("Group Call", function () {
GroupCallType.Video,
false,
GroupCallIntent.Prompt,
false,
FAKE_CONF_ID,
);
await groupCall.create();
Expand Down
14 changes: 13 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,13 @@ export interface ICreateClientOpts {
* Defaults to a built-in English handler with basic pluralisation.
*/
roomNameGenerator?: (roomId: string, state: RoomNameState) => string | null;

/**
* If true, participant can join group call without video and audio this has to be allowed. By default, a local
* media stream is needed to establish a group call.
* Default: false.
*/
isVoipWithNoMediaAllowed?: boolean;
}

export interface IMatrixClientCreateOpts extends ICreateClientOpts {
Expand Down Expand Up @@ -1169,6 +1176,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public iceCandidatePoolSize = 0; // XXX: Intended private, used in code.
public idBaseUrl?: string;
public baseUrl: string;
public readonly isVoipWithNoMediaAllowed;

// Note: these are all `protected` to let downstream consumers make mistakes if they want to.
// We don't technically support this usage, but have reasons to do this.
Expand Down Expand Up @@ -1313,6 +1321,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.iceCandidatePoolSize = opts.iceCandidatePoolSize === undefined ? 0 : opts.iceCandidatePoolSize;
this.supportsCallTransfer = opts.supportsCallTransfer || false;
this.fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false;
this.isVoipWithNoMediaAllowed = opts.isVoipWithNoMediaAllowed || false;

if (opts.useE2eForGroupCall !== undefined) this.useE2eForGroupCall = opts.useE2eForGroupCall;

Expand Down Expand Up @@ -1880,14 +1889,17 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
throw new Error(`Cannot find room ${roomId}`);
}

// Because without Media section a WebRTC connection is not possible, so need a RTCDataChannel to set up a
// no media WebRTC connection anyway.
return new GroupCall(
this,
room,
type,
isPtt,
intent,
this.isVoipWithNoMediaAllowed,
undefined,
dataChannelsEnabled,
dataChannelsEnabled || this.isVoipWithNoMediaAllowed,
dataChannelOptions,
).create();
}
Expand Down
11 changes: 10 additions & 1 deletion src/webrtc/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,9 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// Used to keep the timer for the delay before actually stopping our
// video track after muting (see setLocalVideoMuted)
private stopVideoTrackTimer?: ReturnType<typeof setTimeout>;
// Used to allow connection without Video and Audio. To establish a webrtc connection without media a Data channel is
// needed At the moment this property is true if we allow MatrixClient with isVoipWithNoMediaAllowed = true
private readonly isOnlyDataChannelAllowed: boolean;

/**
* Construct a new Matrix Call.
Expand Down Expand Up @@ -420,6 +423,8 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
utils.checkObjectHasKeys(server, ["urls"]);
}
this.callId = genCallID();
// If the Client provides calls without audio and video we need a datachannel for a webrtc connection
this.isOnlyDataChannelAllowed = this.client.isVoipWithNoMediaAllowed;
}

/**
Expand Down Expand Up @@ -944,7 +949,11 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
// According to previous comments in this file, firefox at some point did not
// add streams until media started arriving on them. Testing latest firefox
// (81 at time of writing), this is no longer a problem, so let's do it the correct way.
if (!remoteStream || remoteStream.getTracks().length === 0) {
//
// For example in case of no media webrtc connections like screen share only call we have to allow webrtc
// connections without remote media. In this case we always use a data channel. At the moment we allow as well
// only data channel as media in the WebRTC connection with this setup here.
if (!this.isOnlyDataChannelAllowed && (!remoteStream || remoteStream.getTracks().length === 0)) {
logger.error(
`Call ${this.callId} initWithInvite() no remote stream or no tracks after setting remote description!`,
);
Expand Down
17 changes: 15 additions & 2 deletions src/webrtc/groupCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ export class GroupCall extends TypedEventEmitter<
public type: GroupCallType,
public isPtt: boolean,
public intent: GroupCallIntent,
public readonly initCallWithoutVideoAndAudio: boolean,
groupCallId?: string,
private dataChannelsEnabled?: boolean,
private dataChannelOptions?: IGroupCallDataChannelOptions,
Expand Down Expand Up @@ -374,8 +375,15 @@ export class GroupCall extends TypedEventEmitter<
try {
stream = await this.client.getMediaHandler().getUserMediaStream(true, this.type === GroupCallType.Video);
} catch (error) {
this.state = GroupCallState.LocalCallFeedUninitialized;
throw error;
// If is allowed to join a call without a media stream, then we
// don't throw an error here. But we need an empty Local Feed to establish
// a connection later.
if (this.initCallWithoutVideoAndAudio) {
stream = new MediaStream();
} else {
this.state = GroupCallState.LocalCallFeedUninitialized;
throw error;
}
}

// The call could've been disposed while we were waiting, and could
Expand Down Expand Up @@ -609,6 +617,11 @@ export class GroupCall extends TypedEventEmitter<
* @returns Whether muting/unmuting was successful
*/
public async setLocalVideoMuted(muted: boolean): Promise<boolean> {
// Because we need a Local Call Feed to establish a call connection, we avoid muting video in case of empty
// video track. In this way we go sure if a client implements muting we don't raise an error.
if (this.initCallWithoutVideoAndAudio && this.localCallFeed?.stream.getVideoTracks().length === 0) {
return false;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, it's somehow not clear to me what this piece of code does exactly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My implementation allows feeds that does not contain tracks at all. I have to make sure that the user of the SDK doesn't try to mute a feed that doesn't have a video track. This case must be caught. It would be logically correct if we would have no LocalFeed. This case is covert below. But you need a LocalFeed to innit a GroupCall. I can't go the way of PTT either, because I have to assume that no device exists.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to check for this.initCallWithoutVideoAndAudio? Wouldn't it always be an error to try & mute/unmute video if there was no video track?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'll remove it

// hasAudioDevice can block indefinitely if the window has lost focus,
// and it doesn't make much sense to keep a device from being muted, so
// we always allow muted = true changes to go through
Expand Down
5 changes: 4 additions & 1 deletion src/webrtc/groupCallEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,11 @@ export class GroupCallEventHandler {
callType,
isPtt,
callIntent,
this.client.isVoipWithNoMediaAllowed,
groupCallId,
content?.dataChannelsEnabled,
// Because without Media section a WebRTC connection is not possible, so need a RTCDataChannel to set up a
// no media WebRTC connection anyway.
content?.dataChannelsEnabled || this.client.isVoipWithNoMediaAllowed,
dataChannelOptions,
);

Expand Down