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

[NEW] VoIP Input/Output Device Selection #25966

Merged
merged 25 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
877f0b1
[NEW] Enable outbound calling for EE (#25843)
amolghode1981 Jun 21, 2022
0dcb8b7
Merge remote-tracking branch 'origin/develop' into new/voip-outbound
amolghode1981 Jun 22, 2022
055b8b1
[NEW] Device Selection
MartinSchoeler Jun 8, 2022
5185103
Import stream files from https://github.com/RocketChat/Rocket.Chat/pu…
MartinSchoeler Jun 23, 2022
bf7487c
Merge remote-tracking branch 'origin/develop' into new/device-selection
MartinSchoeler Jun 24, 2022
302cfc2
Fix errors introduced in conflict resolution
MartinSchoeler Jun 24, 2022
07870d5
I18n translations
MartinSchoeler Jun 24, 2022
472e699
Remove Logs
MartinSchoeler Jun 24, 2022
3502b7d
Fix set input
MartinSchoeler Jun 27, 2022
96f5f1e
Get the constraitns from the correct place
MartinSchoeler Jun 27, 2022
7c61b1b
Lint
MartinSchoeler Jun 27, 2022
a1de57c
Fix icon && translation on option
MartinSchoeler Jun 27, 2022
adb16dc
I need more glasses
MartinSchoeler Jun 27, 2022
d049950
Merge remote-tracking branch 'origin/develop' into new/device-selection
MartinSchoeler Jun 28, 2022
7821988
Merge branch 'develop' into new/device-selection
KevLehman Jun 28, 2022
3be75eb
Fix typecheck
KevLehman Jun 28, 2022
236750c
Chore: Replacing React.FC
aleksandernsilva Jun 28, 2022
17e4e5b
[FIX] Changed licence to voip-enterprise
aleksandernsilva Jun 28, 2022
87bab35
Chore: Refactored voip menu hooks
aleksandernsilva Jun 28, 2022
549ba8d
Merge remote-tracking branch 'origin/develop' into new/device-selection
MartinSchoeler Jun 28, 2022
3b5157e
Remove MakeCall Again
MartinSchoeler Jun 28, 2022
0f45435
Merge branch 'new/device-selection' of https://github.com/RocketChat/…
MartinSchoeler Jun 28, 2022
c6c6710
title :|
MartinSchoeler Jun 28, 2022
12a0dc1
cmon
KevLehman Jun 28, 2022
699d936
[FIX] Adjusted the voip menu theme
aleksandernsilva Jun 28, 2022
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
23 changes: 23 additions & 0 deletions apps/meteor/client/contexts/CallContext.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { IVoipRoom } from '@rocket.chat/core-typings';
import { ICallerInfo, VoIpCallerInfo } from '@rocket.chat/core-typings';
import { Device } from '@rocket.chat/ui-contexts';
import { createContext, useContext, useMemo } from 'react';
import { useSyncExternalStore } from 'use-sync-external-store/shim';

Expand Down Expand Up @@ -29,6 +30,8 @@ type CallContextReady = {
openRoom: (rid: IVoipRoom['_id']) => void;
createRoom: (caller: ICallerInfo) => IVoipRoom['_id'];
closeRoom: (data?: { comment?: string; tags?: string[] }) => void;
changeAudioOutputDevice: (selectedAudioDevices: Device) => void;
changeAudioInputDevice: (selectedAudioDevices: Device) => void;
};
type CallContextError = {
enabled: true;
Expand Down Expand Up @@ -188,3 +191,23 @@ export const useOpenedRoomInfo = (): CallContextReady['openedRoomInfo'] => {

return context.openedRoomInfo;
};

export const useChangeAudioOutputDevice = (): CallContextReady['changeAudioOutputDevice'] => {
const context = useContext(CallContext);

if (!isCallContextReady(context)) {
throw new Error('useChangeAudioOutputDevice only if Calls are enabled and ready');
}

return context.changeAudioOutputDevice;
};

export const useChangeAudioInputDevice = (): CallContextReady['changeAudioOutputDevice'] => {
const context = useContext(CallContext);

if (!isCallContextReady(context)) {
throw new Error('useChangeAudioInputDevice only if Calls are enabled and ready');
}

return context.changeAudioInputDevice;
};
105 changes: 105 additions & 0 deletions apps/meteor/client/lib/voip/LocalStream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* This class is used for local stream manipulation.
* @remarks
* This class does not really store any local stream for the reason
* that the local stream tracks are stored in the peer connection.
*
* This simply provides necessary methods for stream manipulation.
*
* Currently it does not use any of its base functionality. Nevertheless
* there might be a need that we may want to do some stream operations
* such as closing of tracks, in future. For that purpose, it is written
* this way.
*
*/

import { Session } from 'sip.js';
import { defaultMediaStreamFactory, MediaStreamFactory, SessionDescriptionHandler } from 'sip.js/lib/platform/web';

import Stream from './Stream';

export default class LocalStream extends Stream {
static async requestNewStream(constraints: MediaStreamConstraints, session: Session): Promise<MediaStream | undefined> {
const factory: MediaStreamFactory = defaultMediaStreamFactory();
if (session?.sessionDescriptionHandler) {
return factory(constraints, session.sessionDescriptionHandler as SessionDescriptionHandler);
}
}

static async replaceTrack(peerConnection: RTCPeerConnection, newStream: MediaStream, mediaType?: 'audio' | 'video'): Promise<boolean> {
const senders = peerConnection.getSenders();
if (!senders) {
return false;
}
/**
* This will be called when media device change happens.
* This needs to be called externally when the device change occurs.
* This function first acquires the new stream based on device selection
* and then replaces the track in the sender of existing stream by track acquired
* by caputuring new stream.
*
* Notes:
* Each sender represents a track in the RTCPeerConnection.
* Peer connection will contain single track for
* each, audio, video and data.
* Furthermore, We are assuming that
* newly captured stream will have a single track for each media type. i.e
* audio video and data. But this assumption may not be true atleast in theory. One may see multiple
* audio track in the captured stream or multiple senders for same kind in the peer connection
* If/When such situation arrives in future, we may need to revisit the track replacement logic.
* */

switch (mediaType) {
case 'audio': {
let replaced = false;
const newTracks = newStream.getAudioTracks();
if (!newTracks) {
console.warn('replaceTrack() : No audio tracks in the stream. Returning');
return false;
}
for (let i = 0; i < senders?.length; i++) {
if (senders[i].track?.kind === 'audio') {
senders[i].replaceTrack(newTracks[0]);
replaced = true;
break;
}
}
return replaced;
}
case 'video': {
let replaced = false;
const newTracks = newStream.getVideoTracks();
if (!newTracks) {
console.warn('replaceTrack() : No video tracks in the stream. Returning');
return false;
}
for (let i = 0; i < senders?.length; i++) {
if (senders[i].track?.kind === 'video') {
senders[i].replaceTrack(newTracks[0]);
replaced = true;
break;
}
}
return replaced;
}
default: {
let replaced = false;
const newTracks = newStream.getVideoTracks();
if (!newTracks) {
console.warn('replaceTrack() : No tracks in the stream. Returning');
return false;
}
for (let i = 0; i < senders?.length; i++) {
for (let j = 0; j < newTracks.length; j++) {
if (senders[i].track?.kind === newTracks[j].kind) {
senders[i].replaceTrack(newTracks[j]);
replaced = true;
break;
}
}
}
return replaced;
}
}
}
}
75 changes: 75 additions & 0 deletions apps/meteor/client/lib/voip/RemoteStream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* This class is used for local stream manipulation.
* @remarks
* This class wraps up browser media stream and HTMLMedia element
* and takes care of rendering the media on a given element.
* This provides enough abstraction so that the higher level
* classes do not need to know about the browser specificities for
* media.
* This will also provide stream related functionalities such as
* mixing of 2 streams in to 2, adding/removing tracks, getting a track information
* detecting voice energy etc. Which will be implemented as when needed
*/

import Stream from './Stream';

export default class RemoteStream extends Stream {
private renderingMediaElement: HTMLMediaElement | undefined;

constructor(mediaStream: MediaStream) {
super(mediaStream);
}

/**
* Called for initializing the class
* @remarks
*/

init(rmElement: HTMLMediaElement): void {
if (this.renderingMediaElement) {
// Someone already has setup the stream and initializing it once again
// Clear the existing stream object
this.renderingMediaElement.pause();
this.renderingMediaElement.srcObject = null;
}
this.renderingMediaElement = rmElement;
}

/**
* Called for playing the stream
* @remarks
* Plays the stream on media element. Stream will be autoplayed and muted based on the settings.
* throws and error if the play fails.
*/

play(autoPlay = true, muteAudio = false): void {
if (this.renderingMediaElement && this.mediaStream) {
this.renderingMediaElement.autoplay = autoPlay;
this.renderingMediaElement.srcObject = this.mediaStream;
if (autoPlay) {
this.renderingMediaElement.play().catch((error: Error) => {
throw error;
});
}
if (muteAudio) {
this.renderingMediaElement.volume = 0;
}
}
}

/**
* Called for pausing the stream
* @remarks
*/
pause(): void {
this.renderingMediaElement?.pause();
}

clear(): void {
super.clear();
if (this.renderingMediaElement) {
this.renderingMediaElement.pause();
this.renderingMediaElement.srcObject = null;
}
}
}
53 changes: 2 additions & 51 deletions apps/meteor/client/lib/voip/Stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@
* mixing of 2 streams in to 2, adding/removing tracks, getting a track information
* detecting voice energy etc. Which will be implemented as when needed
*/

export default class Stream {
private mediaStream: MediaStream | undefined;

private renderingMediaElement: HTMLMediaElement | undefined;
protected mediaStream: MediaStream | undefined;

constructor(mediaStream: MediaStream) {
this.mediaStream = mediaStream;
Expand Down Expand Up @@ -52,50 +49,6 @@ export default class Stream {
this.mediaStream?.onremovetrack?.(callBack);
}

/**
* Called for initializing the class
* @remarks
*/

init(rmElement: HTMLMediaElement): void {
if (this.renderingMediaElement) {
// Someone already has setup the stream and initializing it once again
// Clear the existing stream object
this.renderingMediaElement.pause();
this.renderingMediaElement.srcObject = null;
}
this.renderingMediaElement = rmElement;
}
/**
* Called for playing the stream
* @remarks
* Plays the stream on media element. Stream will be autoplayed and muted based on the settings.
* throws and error if the play fails.
*/

play(autoPlay = true, muteAudio = false): void {
if (this.renderingMediaElement && this.mediaStream) {
this.renderingMediaElement.autoplay = autoPlay;
this.renderingMediaElement.srcObject = this.mediaStream;
if (autoPlay) {
this.renderingMediaElement.play().catch((error: Error) => {
throw error;
});
}
if (muteAudio) {
this.renderingMediaElement.volume = 0;
}
}
}

/**
* Called for pausing the stream
* @remarks
*/
pause(): void {
this.renderingMediaElement?.pause();
}

/**
* Called for clearing the streams and media element.
* @remarks
Expand All @@ -106,9 +59,7 @@ export default class Stream {
*/

clear(): void {
if (this.renderingMediaElement && this.mediaStream) {
this.renderingMediaElement.pause();
this.renderingMediaElement.srcObject = null;
if (this.mediaStream) {
this.stopTracks();
this.mediaStream = undefined;
}
Expand Down
51 changes: 48 additions & 3 deletions apps/meteor/client/lib/voip/VoIPUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,17 @@ import { OutgoingByeRequest, OutgoingRequestDelegate, URI } from 'sip.js/lib/cor
import { SessionDescriptionHandler, SessionDescriptionHandlerOptions } from 'sip.js/lib/platform/web';

import { toggleMediaStreamTracks } from './Helper';
import LocalStream from './LocalStream';
import { QueueAggregator } from './QueueAggregator';
import Stream from './Stream';
import RemoteStream from './RemoteStream';

export class VoIPUser extends Emitter<VoipEvents> {
state: IState = {
isReady: false,
enableVideo: false,
};

private remoteStream: Stream | undefined;
private remoteStream: RemoteStream | undefined;

userAgentOptions: UserAgentOptions = {};

Expand Down Expand Up @@ -408,7 +409,7 @@ export class VoIPUser extends Emitter<VoipEvents> {
throw new Error('Remote media stream is undefined.');
}

this.remoteStream = new Stream(remoteStream);
this.remoteStream = new RemoteStream(remoteStream);
const mediaElement = this.mediaStreamRendered?.remoteMediaElement;
if (mediaElement) {
this.remoteStream.init(mediaElement);
Expand Down Expand Up @@ -999,7 +1000,51 @@ export class VoIPUser extends Emitter<VoipEvents> {
});
}

async changeAudioInputDevice(constraints: MediaStreamConstraints): Promise<boolean> {
if (!this.session) {
console.warn('changeAudioInputDevice() : No session. Returning');
return false;
}
const newStream = await LocalStream.requestNewStream(constraints, this.session);
if (!newStream) {
console.warn('changeAudioInputDevice() : Unable to get local stream. Returning');
return false;
}
const { peerConnection } = this.session?.sessionDescriptionHandler as SessionDescriptionHandler;
if (!peerConnection) {
console.warn('changeAudioInputDevice() : No peer connection. Returning');
return false;
}
LocalStream.replaceTrack(peerConnection, newStream, 'audio');
return true;
}

// Commenting this as Video Configuration is not part of the scope for now
// async changeVideoInputDevice(selectedVideoDevices: IDevice): Promise<boolean> {
// if (!this.session) {
// console.warn('changeVideoInputDevice() : No session. Returning');
// return false;
// }
// if (!this.config.enableVideo || this.deviceManager.hasVideoInputDevice()) {
// console.warn('changeVideoInputDevice() : Unable change video device. Returning');
// return false;
// }
// this.deviceManager.changeVideoInputDevice(selectedVideoDevices);
// const newStream = await LocalStream.requestNewStream(this.deviceManager.getConstraints('video'), this.session);
// if (!newStream) {
// console.warn('changeVideoInputDevice() : Unable to get local stream. Returning');
// return false;
// }
// const { peerConnection } = this.session?.sessionDescriptionHandler as SessionDescriptionHandler;
// if (!peerConnection) {
// console.warn('changeVideoInputDevice() : No peer connection. Returning');
// return false;
// }
// LocalStream.replaceTrack(peerConnection, newStream, 'video');
// return true;
// }
// eslint-disable-next-line @typescript-eslint/no-unused-vars

async makeCall(_callee: string, _mediaRenderer?: IMediaStreamRenderer): Promise<void> {
throw new Error('Not implemented');
}
Expand Down
Loading