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 2 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
20 changes: 18 additions & 2 deletions 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,16 +1889,23 @@ 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,
undefined,
dataChannelsEnabled,
dataChannelsEnabled || this.isVoipWithNoMediaAllowed,
dataChannelOptions,
).create();
)
.create()
.then((groupCall) => {
groupCall.initCallWithoutVideoAndAudio = this.isVoipWithNoMediaAllowed;
return groupCall;
});
}

/**
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
32 changes: 30 additions & 2 deletions src/webrtc/groupCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ export class GroupCall extends TypedEventEmitter<
private initWithAudioMuted = false;
private initWithVideoMuted = false;
private initCallFeedPromise?: Promise<void>;
private allowCallWithOutVideoAndAudio = false;

public constructor(
private client: MatrixClient,
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.allowCallWithOutVideoAndAudio) {
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.allowCallWithOutVideoAndAudio && this.localCallFeed?.stream.getVideoTracks().length === 0) {
return false;
}
// 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 Expand Up @@ -1461,4 +1474,19 @@ export class GroupCall extends TypedEventEmitter<
);
}
};

/**
* allowCallWithOutVideoAndAudio should be a GroupCall property. However, I make it as runtime configurable
* property because this additional behavior has a big impact on other features and is a big API change.
* Joining without a camera and video should only be explicitly allowed, because there can be unexpected behavior
* when later querying the media access rights from browser that the client cannot intercept. Join without came
* and audio should be a property like PTT.
*/
public get initCallWithoutVideoAndAudio(): boolean {
return this.allowCallWithOutVideoAndAudio;
}

public set initCallWithoutVideoAndAudio(allowCallWithOutVideoAndAudio: boolean) {
this.allowCallWithOutVideoAndAudio = allowCallWithOutVideoAndAudio;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not put this into opts and make it readonly and public?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the beginning createGroupCall from the MatrixClient stopped me from doing that. But since the configuration is now done via the Matrix Client, there is no reason not to do it via the constructor. Thx 👍

}
}
5 changes: 4 additions & 1 deletion src/webrtc/groupCallEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,12 @@ export class GroupCallEventHandler {
isPtt,
callIntent,
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,
);
groupCall.initCallWithoutVideoAndAudio = this.client.isVoipWithNoMediaAllowed;

this.groupCalls.set(room.roomId, groupCall);
this.client.emit(GroupCallEventHandlerEvent.Incoming, groupCall);
Expand Down