Compare commits

...

1 Commits

Author SHA1 Message Date
Christopher Poile
2bcea5a6a3 use calls/common rtcpeer; @calls -> @mmcalls 2023-03-06 11:42:13 -05:00
15 changed files with 91 additions and 294 deletions

View File

@@ -43,14 +43,13 @@ import type {
ApiResp, ApiResp,
Call, Call,
CallParticipant, CallParticipant,
CallReactionEmoji,
CallsConnection, CallsConnection,
RecordingState,
ServerCallState, ServerCallState,
ServerChannelState, ServerChannelState,
} from '@calls/types/calls'; } from '@calls/types/calls';
import type {Client} from '@client/rest'; import type {Client} from '@client/rest';
import type ClientError from '@client/rest/error'; import type ClientError from '@client/rest/error';
import type {CallRecordingState, EmojiData} from '@mmcalls/common/lib/types';
import type {IntlShape} from 'react-intl'; import type {IntlShape} from 'react-intl';
let connection: CallsConnection | null = null; let connection: CallsConnection | null = null;
@@ -322,7 +321,7 @@ export const unraiseHand = () => {
} }
}; };
export const sendReaction = (emoji: CallReactionEmoji) => { export const sendReaction = (emoji: EmojiData) => {
if (connection) { if (connection) {
connection.sendReaction(emoji); connection.sendReaction(emoji);
} }
@@ -415,7 +414,7 @@ export const startCallRecording = async (serverUrl: string, callId: string) => {
const client = NetworkManager.getClient(serverUrl); const client = NetworkManager.getClient(serverUrl);
let data: ApiResp | RecordingState; let data: ApiResp | CallRecordingState;
try { try {
data = await client.startCallRecording(callId); data = await client.startCallRecording(callId);
} catch (error) { } catch (error) {
@@ -433,7 +432,7 @@ export const stopCallRecording = async (serverUrl: string, callId: string) => {
const client = NetworkManager.getClient(serverUrl); const client = NetworkManager.getClient(serverUrl);
let data: ApiResp | RecordingState; let data: ApiResp | CallRecordingState;
try { try {
data = await client.stopCallRecording(callId); data = await client.stopCallRecording(callId);
} catch (error) { } catch (error) {

View File

@@ -1,24 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import type { import type {ServerChannelState, ApiResp} from '@calls/types/calls';
ServerChannelState, import type {CallRecordingState, CallsConfig} from '@mmcalls/common/lib/types';
ServerCallsConfig,
ApiResp,
RecordingState,
} from '@calls/types/calls';
import type {RTCIceServer} from 'react-native-webrtc'; import type {RTCIceServer} from 'react-native-webrtc';
export interface ClientCallsMix { export interface ClientCallsMix {
getEnabled: () => Promise<Boolean>; getEnabled: () => Promise<Boolean>;
getCalls: () => Promise<ServerChannelState[]>; getCalls: () => Promise<ServerChannelState[]>;
getCallForChannel: (channelId: string) => Promise<ServerChannelState>; getCallForChannel: (channelId: string) => Promise<ServerChannelState>;
getCallsConfig: () => Promise<ServerCallsConfig>; getCallsConfig: () => Promise<CallsConfig>;
enableChannelCalls: (channelId: string, enable: boolean) => Promise<ServerChannelState>; enableChannelCalls: (channelId: string, enable: boolean) => Promise<ServerChannelState>;
endCall: (channelId: string) => Promise<ApiResp>; endCall: (channelId: string) => Promise<ApiResp>;
genTURNCredentials: () => Promise<RTCIceServer[]>; genTURNCredentials: () => Promise<RTCIceServer[]>;
startCallRecording: (callId: string) => Promise<ApiResp | RecordingState>; startCallRecording: (callId: string) => Promise<ApiResp | CallRecordingState>;
stopCallRecording: (callId: string) => Promise<ApiResp | RecordingState>; stopCallRecording: (callId: string) => Promise<ApiResp | CallRecordingState>;
} }
const ClientCalls = (superclass: any) => class extends superclass { const ClientCalls = (superclass: any) => class extends superclass {
@@ -52,7 +48,7 @@ const ClientCalls = (superclass: any) => class extends superclass {
return this.doFetch( return this.doFetch(
`${this.getCallsRoute()}/config`, `${this.getCallsRoute()}/config`,
{method: 'get'}, {method: 'get'},
) as ServerCallsConfig; ) as CallsConfig;
}; };
enableChannelCalls = async (channelId: string, enable: boolean) => { enableChannelCalls = async (channelId: string, enable: boolean) => {

View File

@@ -8,7 +8,7 @@ import CompassIcon from '@components/compass_icon';
import Emoji from '@components/emoji'; import Emoji from '@components/emoji';
import ProfilePicture from '@components/profile_picture'; import ProfilePicture from '@components/profile_picture';
import type {CallReactionEmoji} from '@calls/types/calls'; import type {EmojiData} from '@mmcalls/common/lib/types';
import type UserModel from '@typings/database/models/servers/user'; import type UserModel from '@typings/database/models/servers/user';
type Props = { type Props = {
@@ -18,7 +18,7 @@ type Props = {
muted?: boolean; muted?: boolean;
sharingScreen?: boolean; sharingScreen?: boolean;
raisedHand?: boolean; raisedHand?: boolean;
reaction?: CallReactionEmoji; reaction?: EmojiData;
size?: 'm' | 'l'; size?: 'm' | 'l';
} }

View File

@@ -1,18 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment import {RTCPeer} from '@mmcalls/common/lib';
// @ts-ignore import {deflate} from 'pako';
import {deflate} from 'pako/lib/deflate.js';
import {DeviceEventEmitter, EmitterSubscription} from 'react-native'; import {DeviceEventEmitter, EmitterSubscription} from 'react-native';
import InCallManager from 'react-native-incall-manager'; import InCallManager from 'react-native-incall-manager';
import { import {
MediaStream, MediaStream,
MediaStreamTrack, MediaStreamTrack,
mediaDevices, mediaDevices,
RTCPeerConnection,
} from 'react-native-webrtc'; } from 'react-native-webrtc';
import RTCPeer from '@calls/rtcpeer';
import {setSpeakerPhone} from '@calls/state'; import {setSpeakerPhone} from '@calls/state';
import {getICEServersConfigs} from '@calls/utils'; import {getICEServersConfigs} from '@calls/utils';
import {WebsocketEvents} from '@constants'; import {WebsocketEvents} from '@constants';
@@ -22,7 +21,9 @@ import {logError, logDebug, logWarning} from '@utils/log';
import {WebSocketClient, wsReconnectionTimeoutErr} from './websocket_client'; import {WebSocketClient, wsReconnectionTimeoutErr} from './websocket_client';
import type {CallReactionEmoji, CallsConnection} from '@calls/types/calls'; import type {CallsConnection} from '@calls/types/calls';
import type {RTCPeerOpts} from '@mmcalls/common/lib/rtc_peer';
import type {EmojiData} from '@mmcalls/common/lib/types';
const peerConnectTimeout = 5000; const peerConnectTimeout = 5000;
@@ -164,7 +165,7 @@ export async function newConnection(
} }
}; };
const sendReaction = (emoji: CallReactionEmoji) => { const sendReaction = (emoji: EmojiData) => {
if (ws) { if (ws) {
ws.send('react', { ws.send('react', {
data: JSON.stringify(emoji), data: JSON.stringify(emoji),
@@ -204,7 +205,14 @@ export async function newConnection(
InCallManager.start({media: 'video'}); InCallManager.start({media: 'video'});
setSpeakerPhone(true); setSpeakerPhone(true);
peer = new RTCPeer({iceServers: iceConfigs || []}); const opts: RTCPeerOpts = {
logDebug,
webrtc: {
MediaStream,
RTCPeerConnection,
},
};
peer = new RTCPeer({iceServers: iceConfigs || []}, opts);
peer.on('offer', (sdp) => { peer.on('offer', (sdp) => {
logDebug(`local offer, sending: ${JSON.stringify(sdp)}`); logDebug(`local offer, sending: ${JSON.stringify(sdp)}`);

View File

@@ -1,210 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable @typescript-eslint/ban-ts-comment */
import {EventEmitter} from 'events';
import {
MediaStream,
MediaStreamTrack,
RTCIceCandidate,
RTCPeerConnection,
RTCPeerConnectionIceEvent,
RTCRtpSender,
RTCSessionDescription,
} from 'react-native-webrtc';
import {logDebug, logError} from '@utils/log';
import type {RTCPeerConfig} from './types';
import type RTCTrackEvent from 'react-native-webrtc/lib/typescript/RTCTrackEvent';
const rtcConnFailedErr = new Error('rtc connection failed');
export default class RTCPeer extends EventEmitter {
private pc: RTCPeerConnection | null;
private readonly senders: { [key: string]: RTCRtpSender };
private candidates: RTCIceCandidate[] = [];
private makingOffer = false;
public connected: boolean;
constructor(config: RTCPeerConfig) {
super();
// We keep a map of track IDs -> RTP sender so that we can easily
// replace tracks when muting/unmuting.
this.senders = {};
this.pc = new RTCPeerConnection(config);
this.pc.onnegotiationneeded = () => this.onNegotiationNeeded();
this.pc.onicecandidate = (ev) => this.onICECandidate(ev);
this.pc.oniceconnectionstatechange = () => this.onICEConnectionStateChange();
this.pc.onconnectionstatechange = () => this.onConnectionStateChange();
this.pc.ontrack = (ev) => this.onTrack(ev);
this.connected = false;
// We create a data channel for two reasons:
// - Initiate a connection without preemptively adding audio/video tracks.
// - Use this communication channel for further negotiation (to be implemented).
this.pc.createDataChannel('calls-dc');
}
private onICECandidate(ev: RTCPeerConnectionIceEvent) {
if (ev.candidate) {
this.emit('candidate', ev.candidate);
}
}
private onConnectionStateChange() {
switch (this.pc?.connectionState) {
case 'connected':
this.connected = true;
break;
case 'failed':
this.emit('close', rtcConnFailedErr);
break;
}
}
private onICEConnectionStateChange() {
switch (this.pc?.iceConnectionState) {
case 'connected':
this.emit('connect');
break;
case 'failed':
this.emit('close', rtcConnFailedErr);
break;
case 'closed':
this.emit('close');
break;
default:
}
}
private async onNegotiationNeeded() {
try {
this.makingOffer = true;
await this.pc?.setLocalDescription();
this.emit('offer', this.pc?.localDescription);
} catch (err) {
this.emit('error', err);
} finally {
this.makingOffer = false;
}
}
private onTrack(ev: RTCTrackEvent) {
if (ev.streams.length === 0) {
this.emit('stream', new MediaStream([ev.track]));
return;
}
this.emit('stream', ev.streams[0]);
}
public async signal(data: string) {
if (!this.pc) {
throw new Error('peer has been destroyed');
}
const msg = JSON.parse(data);
if (msg.type === 'offer' && (this.makingOffer || this.pc?.signalingState !== 'stable')) {
logDebug('signaling conflict, we are polite, proceeding...');
}
try {
switch (msg.type) {
case 'candidate':
// It's possible that ICE candidates are received moments before
// we set the initial remote description which would cause an
// error. In such case we queue them up to be added later.
if (this.pc.remoteDescription && this.pc.remoteDescription.type) {
this.pc.addIceCandidate(msg.candidate).catch((err) => {
logError('failed to add candidate', err);
});
} else {
logDebug('received ice candidate before remote description, queuing...');
this.candidates.push(msg.candidate);
}
break;
case 'offer':
await this.pc.setRemoteDescription(new RTCSessionDescription(msg));
await this.pc.setLocalDescription();
this.emit('answer', this.pc.localDescription);
break;
case 'answer':
await this.pc.setRemoteDescription(msg);
for (const candidate of this.candidates) {
logDebug('adding queued ice candidate');
this.pc.addIceCandidate(candidate).catch((err) => {
logError('failed to add candidate', err);
});
}
break;
default:
this.emit('error', Error('invalid signaling data received'));
}
} catch (err) {
this.emit('error', err);
}
}
public async addTrack(track: MediaStreamTrack, stream?: MediaStream) {
if (!this.pc) {
throw new Error('peer has been destroyed');
}
const sender = await this.pc.addTrack(track, stream!);
if (sender) {
this.senders[track.id] = sender;
}
}
public addStream(stream: MediaStream) {
stream.getTracks().forEach((track) => {
this.addTrack(track, stream);
});
}
public replaceTrack(oldTrackID: string, newTrack: MediaStreamTrack | null) {
const sender = this.senders[oldTrackID];
if (!sender) {
throw new Error('sender for track not found');
}
if (newTrack && newTrack.id !== oldTrackID) {
delete this.senders[oldTrackID];
this.senders[newTrack.id] = sender;
}
sender.replaceTrack(newTrack);
}
public getStats() {
if (!this.pc) {
throw new Error('peer has been destroyed');
}
return this.pc.getStats();
}
public destroy() {
if (!this.pc) {
throw new Error('peer has been destroyed already');
}
this.removeAllListeners('candidate');
this.removeAllListeners('connect');
this.removeAllListeners('error');
this.removeAllListeners('close');
this.removeAllListeners('offer');
this.removeAllListeners('answer');
this.removeAllListeners('stream');
this.pc.onnegotiationneeded = null;
this.pc.onicecandidate = null;
this.pc.oniceconnectionstatechange = null;
this.pc.onconnectionstatechange = null;
this.pc.ontrack = null;
this.pc.close();
this.pc = null;
this.connected = false;
}
}

View File

@@ -1,8 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {RTCIceServer} from 'react-native-webrtc';
export type RTCPeerConfig = {
iceServers: RTCIceServer[];
}

View File

@@ -40,8 +40,6 @@ import {
setPluginEnabled, setPluginEnabled,
setUserVoiceOn, setUserVoiceOn,
} from '@calls/state/actions'; } from '@calls/state/actions';
import {License} from '@constants';
import { import {
Call, Call,
CallsState, CallsState,
@@ -51,8 +49,10 @@ import {
DefaultCurrentCall, DefaultCurrentCall,
DefaultGlobalCallsState, DefaultGlobalCallsState,
GlobalCallsState, GlobalCallsState,
RecordingState, } from '@calls/types/calls';
} from '../types/calls'; import {License} from '@constants';
import type {CallRecordingState} from '@mmcalls/common/lib/types';
jest.mock('@calls/alerts'); jest.mock('@calls/alerts');
@@ -797,6 +797,7 @@ describe('useCallsState', () => {
it('config', () => { it('config', () => {
const newConfig = { const newConfig = {
...DefaultCallsConfig,
ICEServers: [], ICEServers: [],
ICEServersConfigs: [ ICEServersConfigs: [
{ {
@@ -914,7 +915,7 @@ describe('useCallsState', () => {
myUserId: 'myUserId', myUserId: 'myUserId',
...call1, ...call1,
}; };
const recState: RecordingState = { const recState: CallRecordingState = {
init_at: 123, init_at: 123,
start_at: 231, start_at: 231,
end_at: 345, end_at: 345,

View File

@@ -16,17 +16,17 @@ import {
} from '@calls/state'; } from '@calls/state';
import { import {
Call, Call,
CallReaction, CallsConfigState,
CallsConfig,
ChannelsWithCalls, ChannelsWithCalls,
CurrentCall, CurrentCall,
DefaultCall, DefaultCall,
DefaultCurrentCall, DefaultCurrentCall,
ReactionStreamEmoji, ReactionStreamEmoji,
RecordingState,
} from '@calls/types/calls'; } from '@calls/types/calls';
import {REACTION_LIMIT, REACTION_TIMEOUT} from '@constants/calls'; import {REACTION_LIMIT, REACTION_TIMEOUT} from '@constants/calls';
import type {CallRecordingState, UserReactionData} from '@mmcalls/common/lib/types';
export const setCalls = (serverUrl: string, myUserId: string, calls: Dictionary<Call>, enabled: Dictionary<boolean>) => { export const setCalls = (serverUrl: string, myUserId: string, calls: Dictionary<Call>, enabled: Dictionary<boolean>) => {
const channelsWithCalls = Object.keys(calls).reduce( const channelsWithCalls = Object.keys(calls).reduce(
(accum, next) => { (accum, next) => {
@@ -390,7 +390,7 @@ export const setSpeakerPhone = (speakerphoneOn: boolean) => {
} }
}; };
export const setConfig = (serverUrl: string, config: Partial<CallsConfig>) => { export const setConfig = (serverUrl: string, config: Partial<CallsConfigState>) => {
const callsConfig = getCallsConfig(serverUrl); const callsConfig = getCallsConfig(serverUrl);
setCallsConfig(serverUrl, {...callsConfig, ...config}); setCallsConfig(serverUrl, {...callsConfig, ...config});
}; };
@@ -423,7 +423,7 @@ export const setMicPermissionsErrorDismissed = () => {
setCurrentCall(nextCurrentCall); setCurrentCall(nextCurrentCall);
}; };
export const userReacted = (serverUrl: string, channelId: string, reaction: CallReaction) => { export const userReacted = (serverUrl: string, channelId: string, reaction: UserReactionData) => {
// Note: Simplification for performance: // Note: Simplification for performance:
// If you are not in the call with the reaction, ignore it. There could be many calls ongoing in your // If you are not in the call with the reaction, ignore it. There could be many calls ongoing in your
// servers, do we want to be tracking reactions and setting timeouts for all those calls? No. // servers, do we want to be tracking reactions and setting timeouts for all those calls? No.
@@ -474,7 +474,7 @@ export const userReacted = (serverUrl: string, channelId: string, reaction: Call
}, REACTION_TIMEOUT); }, REACTION_TIMEOUT);
}; };
const userReactionTimeout = (serverUrl: string, channelId: string, reaction: CallReaction) => { const userReactionTimeout = (serverUrl: string, channelId: string, reaction: UserReactionData) => {
const currentCall = getCurrentCall(); const currentCall = getCurrentCall();
if (currentCall?.channelId !== channelId) { if (currentCall?.channelId !== channelId) {
return; return;
@@ -498,7 +498,7 @@ const userReactionTimeout = (serverUrl: string, channelId: string, reaction: Cal
setCurrentCall(nextCurrentCall); setCurrentCall(nextCurrentCall);
}; };
export const setRecordingState = (serverUrl: string, channelId: string, recState: RecordingState) => { export const setRecordingState = (serverUrl: string, channelId: string, recState: CallRecordingState) => {
const callsState = getCallsState(serverUrl); const callsState = getCallsState(serverUrl);
if (!callsState.calls[channelId]) { if (!callsState.calls[channelId]) {
return; return;

View File

@@ -4,9 +4,9 @@
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
import {BehaviorSubject} from 'rxjs'; import {BehaviorSubject} from 'rxjs';
import {CallsConfig, DefaultCallsConfig} from '@calls/types/calls'; import {CallsConfigState, DefaultCallsConfig} from '@calls/types/calls';
const callsConfigSubjects: Dictionary<BehaviorSubject<CallsConfig>> = {}; const callsConfigSubjects: Dictionary<BehaviorSubject<CallsConfigState>> = {};
const getCallsConfigSubject = (serverUrl: string) => { const getCallsConfigSubject = (serverUrl: string) => {
if (!callsConfigSubjects[serverUrl]) { if (!callsConfigSubjects[serverUrl]) {
@@ -20,7 +20,7 @@ export const getCallsConfig = (serverUrl: string) => {
return getCallsConfigSubject(serverUrl).value; return getCallsConfigSubject(serverUrl).value;
}; };
export const setCallsConfig = (serverUrl: string, callsConfig: CallsConfig) => { export const setCallsConfig = (serverUrl: string, callsConfig: CallsConfigState) => {
getCallsConfigSubject(serverUrl).next(callsConfig); getCallsConfigSubject(serverUrl).next(callsConfig);
}; };

View File

@@ -1,8 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import type {CallRecordingState, CallsConfig, EmojiData, UserReactionData} from '@mmcalls/common/lib/types';
import type UserModel from '@typings/database/models/servers/user'; import type UserModel from '@typings/database/models/servers/user';
import type {RTCIceServer} from 'react-native-webrtc';
export type GlobalCallsState = { export type GlobalCallsState = {
micPermissionsGranted: boolean; micPermissionsGranted: boolean;
@@ -31,7 +31,7 @@ export type Call = {
screenOn: string; screenOn: string;
threadId: string; threadId: string;
ownerId: string; ownerId: string;
recState?: RecordingState; recState?: CallRecordingState;
hostId: string; hostId: string;
} }
@@ -75,7 +75,7 @@ export type CallParticipant = {
muted: boolean; muted: boolean;
raisedHand: number; raisedHand: number;
userModel?: UserModel; userModel?: UserModel;
reaction?: CallReaction; reaction?: UserReactionData;
} }
export type ChannelsWithCalls = Dictionary<boolean>; export type ChannelsWithCalls = Dictionary<boolean>;
@@ -100,7 +100,7 @@ export type ServerCallState = {
screen_sharing_id: string; screen_sharing_id: string;
owner_id: string; owner_id: string;
host_id: string; host_id: string;
recording: RecordingState; recording: CallRecordingState;
} }
export type CallsConnection = { export type CallsConnection = {
@@ -111,26 +111,16 @@ export type CallsConnection = {
raiseHand: () => void; raiseHand: () => void;
unraiseHand: () => void; unraiseHand: () => void;
initializeVoiceTrack: () => void; initializeVoiceTrack: () => void;
sendReaction: (emoji: CallReactionEmoji) => void; sendReaction: (emoji: EmojiData) => void;
} }
export type ServerCallsConfig = { export type CallsConfigState = CallsConfig & {
ICEServers?: string[]; // deprecated
ICEServersConfigs: RTCIceServer[];
AllowEnableCalls: boolean; AllowEnableCalls: boolean;
DefaultEnabled: boolean;
NeedsTURNCredentials: boolean;
sku_short_name: string;
MaxCallParticipants: number;
EnableRecordings: boolean;
}
export type CallsConfig = ServerCallsConfig & {
pluginEnabled: boolean; pluginEnabled: boolean;
last_retrieved_at: number; last_retrieved_at: number;
} }
export const DefaultCallsConfig: CallsConfig = { export const DefaultCallsConfig: CallsConfigState = {
pluginEnabled: false, pluginEnabled: false,
ICEServers: [], // deprecated ICEServers: [], // deprecated
ICEServersConfigs: [], ICEServersConfigs: [],
@@ -141,6 +131,8 @@ export const DefaultCallsConfig: CallsConfig = {
sku_short_name: '', sku_short_name: '',
MaxCallParticipants: 0, MaxCallParticipants: 0,
EnableRecordings: false, EnableRecordings: false,
MaxRecordingDuration: 60,
AllowScreenSharing: true,
}; };
export type ApiResp = { export type ApiResp = {
@@ -149,18 +141,6 @@ export type ApiResp = {
status_code: number; status_code: number;
} }
export type CallReactionEmoji = {
name: string;
skin?: string;
unified: string;
}
export type CallReaction = {
user_id: string;
emoji: CallReactionEmoji;
timestamp: number;
}
export type ReactionStreamEmoji = { export type ReactionStreamEmoji = {
name: string; name: string;
latestTimestamp: number; latestTimestamp: number;

View File

@@ -3,15 +3,15 @@
import assert from 'assert'; import assert from 'assert';
import {CallsConfigState, DefaultCallsConfig} from '@calls/types/calls';
import {License} from '@constants'; import {License} from '@constants';
import {getICEServersConfigs} from './utils'; import {getICEServersConfigs} from './utils';
import type {CallsConfig} from '@calls/types/calls';
describe('getICEServersConfigs', () => { describe('getICEServersConfigs', () => {
it('backwards compatible case, no ICEServersConfigs present', () => { it('backwards compatible case, no ICEServersConfigs present', () => {
const config: CallsConfig = { const config: CallsConfigState = {
...DefaultCallsConfig,
pluginEnabled: true, pluginEnabled: true,
ICEServers: ['stun:stun.example.com:3478'], ICEServers: ['stun:stun.example.com:3478'],
ICEServersConfigs: [], ICEServersConfigs: [],
@@ -33,7 +33,8 @@ describe('getICEServersConfigs', () => {
}); });
it('ICEServersConfigs set', () => { it('ICEServersConfigs set', () => {
const config: CallsConfig = { const config: CallsConfigState = {
...DefaultCallsConfig,
pluginEnabled: true, pluginEnabled: true,
ICEServersConfigs: [ ICEServersConfigs: [
{ {
@@ -64,7 +65,8 @@ describe('getICEServersConfigs', () => {
}); });
it('Both ICEServers and ICEServersConfigs set', () => { it('Both ICEServers and ICEServersConfigs set', () => {
const config: CallsConfig = { const config: CallsConfigState = {
...DefaultCallsConfig,
pluginEnabled: true, pluginEnabled: true,
ICEServers: ['stun:stuna.example.com:3478'], ICEServers: ['stun:stuna.example.com:3478'],
ICEServersConfigs: [ ICEServersConfigs: [

View File

@@ -8,9 +8,11 @@ import Calls from '@constants/calls';
import {isMinimumServerVersion} from '@utils/helpers'; import {isMinimumServerVersion} from '@utils/helpers';
import {displayUsername} from '@utils/user'; import {displayUsername} from '@utils/user';
import type {CallParticipant, ServerCallsConfig} from '@calls/types/calls'; import type {CallParticipant} from '@calls/types/calls';
import type {CallsConfig} from '@mmcalls/common/lib/types';
import type PostModel from '@typings/database/models/servers/post'; import type PostModel from '@typings/database/models/servers/post';
import type {IntlShape} from 'react-intl'; import type {IntlShape} from 'react-intl';
import type {RTCIceServer} from 'react-native-webrtc';
export function sortParticipants(teammateNameDisplay: string, participants?: Dictionary<CallParticipant>, presenterID?: string): CallParticipant[] { export function sortParticipants(teammateNameDisplay: string, participants?: Dictionary<CallParticipant>, presenterID?: string): CallParticipant[] {
if (!participants) { if (!participants) {
@@ -106,7 +108,7 @@ export function errorAlert(error: string, intl: IntlShape) {
); );
} }
export function getICEServersConfigs(config: ServerCallsConfig) { export function getICEServersConfigs(config: CallsConfig): RTCIceServer[] {
// if ICEServersConfigs is set, we can trust this to be complete and // if ICEServersConfigs is set, we can trust this to be complete and
// coming from an updated API. // coming from an updated API.
if (config.ICEServersConfigs && config.ICEServersConfigs.length > 0) { if (config.ICEServersConfigs && config.ICEServersConfigs.length > 0) {

View File

@@ -19,6 +19,6 @@ module.exports = {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/test/file_transformer.js', '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/test/file_transformer.js',
}, },
transformIgnorePatterns: [ transformIgnorePatterns: [
'node_modules/(?!(@react-native|react-native)|jail-monkey|@sentry/react-native|react-clone-referenced-element|@react-native-community|react-navigation|@react-navigation/.*|validator|react-syntax-highlighter/.*|hast-util-from-selector|hastscript|property-information|hast-util-parse-selector|space-separated-tokens|comma-separated-tokens|zwitch)', 'node_modules/(?!(@react-native|react-native)|jail-monkey|@sentry/react-native|react-clone-referenced-element|@react-native-community|react-navigation|@react-navigation/.*|validator|react-syntax-highlighter/.*|hast-util-from-selector|hastscript|property-information|hast-util-parse-selector|space-separated-tokens|comma-separated-tokens|zwitch|@mmcalls/common)',
], ],
}; };

25
package-lock.json generated
View File

@@ -23,6 +23,7 @@
"@mattermost/react-native-paste-input": "0.6.2", "@mattermost/react-native-paste-input": "0.6.2",
"@mattermost/react-native-turbo-log": "0.2.3", "@mattermost/react-native-turbo-log": "0.2.3",
"@mattermost/react-native-turbo-mailer": "0.2.4", "@mattermost/react-native-turbo-mailer": "0.2.4",
"@mmcalls/common": "https://gitpkg.now.sh/mattermost/mattermost-plugin-calls/webapp/packages/common?b5e89f3",
"@msgpack/msgpack": "2.8.0", "@msgpack/msgpack": "2.8.0",
"@nozbe/watermelondb": "0.25.5", "@nozbe/watermelondb": "0.25.5",
"@nozbe/with-observables": "1.4.1", "@nozbe/with-observables": "1.4.1",
@@ -124,6 +125,7 @@
"@types/jest": "29.4.0", "@types/jest": "29.4.0",
"@types/lodash": "4.14.191", "@types/lodash": "4.14.191",
"@types/mime-db": "1.43.1", "@types/mime-db": "1.43.1",
"@types/pako": "2.0.0",
"@types/querystringify": "2.0.0", "@types/querystringify": "2.0.0",
"@types/react": "18.0.28", "@types/react": "18.0.28",
"@types/react-native-background-timer": "2.0.0", "@types/react-native-background-timer": "2.0.0",
@@ -3286,6 +3288,13 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/@mmcalls/common": {
"name": "@calls/common",
"version": "0.12.0",
"resolved": "https://gitpkg.now.sh/mattermost/mattermost-plugin-calls/webapp/packages/common?b5e89f3",
"integrity": "sha512-lz+T2HHQBfQDpgrI4CkrSNkmBjIaKxJGSxSZHSL0LQ8xAlRUBo3WMG5wJlwwzpxP6rixJjUnoyKBSo8Jn0wWQQ==",
"license": "MIT"
},
"node_modules/@msgpack/msgpack": { "node_modules/@msgpack/msgpack": {
"version": "2.8.0", "version": "2.8.0",
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz", "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz",
@@ -5957,6 +5966,12 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA==" "integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA=="
}, },
"node_modules/@types/pako": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.0.tgz",
"integrity": "sha512-10+iaz93qR5WYxTo+PMifD5TSxiOtdRaxBf7INGGXMQgTCu8Z/7GYWYFUOS3q/G0nE5boj1r4FEB+WSy7s5gbA==",
"dev": true
},
"node_modules/@types/parse-json": { "node_modules/@types/parse-json": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@@ -24325,6 +24340,10 @@
"integrity": "sha512-6W37UvLxg7vhP5YJXZfzKvPy4r9bozqSSuB4gbC2EjvWrGB4LfwKWljgw+Gb/E8x3ceMCib2SPPMz+thzs7DHw==", "integrity": "sha512-6W37UvLxg7vhP5YJXZfzKvPy4r9bozqSSuB4gbC2EjvWrGB4LfwKWljgw+Gb/E8x3ceMCib2SPPMz+thzs7DHw==",
"requires": {} "requires": {}
}, },
"@mmcalls/common": {
"version": "https://gitpkg.now.sh/mattermost/mattermost-plugin-calls/webapp/packages/common?b5e89f3",
"integrity": "sha512-lz+T2HHQBfQDpgrI4CkrSNkmBjIaKxJGSxSZHSL0LQ8xAlRUBo3WMG5wJlwwzpxP6rixJjUnoyKBSo8Jn0wWQQ=="
},
"@msgpack/msgpack": { "@msgpack/msgpack": {
"version": "2.8.0", "version": "2.8.0",
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz", "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz",
@@ -26328,6 +26347,12 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.1.tgz",
"integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA==" "integrity": "sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA=="
}, },
"@types/pako": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.0.tgz",
"integrity": "sha512-10+iaz93qR5WYxTo+PMifD5TSxiOtdRaxBf7INGGXMQgTCu8Z/7GYWYFUOS3q/G0nE5boj1r4FEB+WSy7s5gbA==",
"dev": true
},
"@types/parse-json": { "@types/parse-json": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",

View File

@@ -20,6 +20,7 @@
"@mattermost/react-native-paste-input": "0.6.2", "@mattermost/react-native-paste-input": "0.6.2",
"@mattermost/react-native-turbo-log": "0.2.3", "@mattermost/react-native-turbo-log": "0.2.3",
"@mattermost/react-native-turbo-mailer": "0.2.4", "@mattermost/react-native-turbo-mailer": "0.2.4",
"@mmcalls/common": "https://gitpkg.now.sh/mattermost/mattermost-plugin-calls/webapp/packages/common?b5e89f3",
"@msgpack/msgpack": "2.8.0", "@msgpack/msgpack": "2.8.0",
"@nozbe/watermelondb": "0.25.5", "@nozbe/watermelondb": "0.25.5",
"@nozbe/with-observables": "1.4.1", "@nozbe/with-observables": "1.4.1",
@@ -121,6 +122,7 @@
"@types/jest": "29.4.0", "@types/jest": "29.4.0",
"@types/lodash": "4.14.191", "@types/lodash": "4.14.191",
"@types/mime-db": "1.43.1", "@types/mime-db": "1.43.1",
"@types/pako": "2.0.0",
"@types/querystringify": "2.0.0", "@types/querystringify": "2.0.0",
"@types/react": "18.0.28", "@types/react": "18.0.28",
"@types/react-native-background-timer": "2.0.0", "@types/react-native-background-timer": "2.0.0",