|
|
|
/*
|
|
|
|
* https://github.com/morethanwords/tweb
|
|
|
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
|
|
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
|
|
|
*
|
|
|
|
* Originally from:
|
|
|
|
* https://github.com/evgeny-nadymov/telegram-react
|
|
|
|
* Copyright (C) 2018 Evgeny Nadymov
|
|
|
|
* https://github.com/evgeny-nadymov/telegram-react/blob/master/LICENSE
|
|
|
|
*/
|
|
|
|
|
|
|
|
import { MOUNT_CLASS_TO } from "../../config/debug";
|
|
|
|
import AudioAssetPlayer from "../../helpers/audioAssetPlayer";
|
|
|
|
import safeReplaceObject from "../../helpers/object/safeReplaceObject";
|
|
|
|
import { nextRandomUint } from "../../helpers/random";
|
|
|
|
import tsNow from "../../helpers/tsNow";
|
|
|
|
import { GroupCall, GroupCallParticipant, GroupCallParticipantVideo, GroupCallParticipantVideoSourceGroup, InputGroupCall, Peer, PhoneJoinGroupCall, PhoneJoinGroupCallPresentation, Update, Updates } from "../../layer";
|
|
|
|
import GroupCallInstance from "../calls/groupCallInstance";
|
|
|
|
import GROUP_CALL_STATE from "../calls/groupCallState";
|
|
|
|
import createMainStreamManager from "../calls/helpers/createMainStreamManager";
|
|
|
|
import { generateSsrc } from "../calls/localConferenceDescription";
|
|
|
|
import { WebRTCLineType } from "../calls/sdpBuilder";
|
|
|
|
import StreamManager from "../calls/streamManager";
|
|
|
|
import { Ssrc } from "../calls/types";
|
|
|
|
import { logger } from "../logger";
|
|
|
|
import apiManager from "../mtproto/mtprotoworker";
|
|
|
|
import { NULL_PEER_ID } from "../mtproto/mtproto_config";
|
|
|
|
import rootScope from "../rootScope";
|
|
|
|
import apiUpdatesManager from "./apiUpdatesManager";
|
|
|
|
import appChatsManager from "./appChatsManager";
|
|
|
|
import appPeersManager from "./appPeersManager";
|
|
|
|
import appUsersManager from "./appUsersManager";
|
|
|
|
|
|
|
|
export type GroupCallId = GroupCall['id'];
|
|
|
|
export type MyGroupCall = GroupCall | InputGroupCall;
|
|
|
|
|
|
|
|
export type GroupCallConnectionType = 'main' | 'presentation';
|
|
|
|
|
|
|
|
export type JoinGroupCallJsonPayload = {
|
|
|
|
fingerprints: {
|
|
|
|
fingerprint: string;
|
|
|
|
setup: string;
|
|
|
|
hash: string;
|
|
|
|
}[];
|
|
|
|
pwd: string;
|
|
|
|
ssrc: number;
|
|
|
|
'ssrc-groups': GroupCallParticipantVideoSourceGroup.groupCallParticipantVideoSourceGroup[];
|
|
|
|
ufrag: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
const GET_PARTICIPANTS_LIMIT = 100;
|
|
|
|
|
|
|
|
let IS_MUTED = true;
|
|
|
|
|
|
|
|
export type GroupCallOutputSource = 'main' | 'presentation' | number;
|
|
|
|
|
|
|
|
export type GroupCallAudioAssetName = "group_call_connect.mp3" | "group_call_end.mp3" | "group_call_start.mp3" | "voip_onallowtalk.mp3";
|
|
|
|
|
|
|
|
export class AppGroupCallsManager {
|
|
|
|
private log: ReturnType<typeof logger>;
|
|
|
|
|
|
|
|
private groupCalls: Map<GroupCallId, MyGroupCall>;
|
|
|
|
private participants: Map<GroupCallId, Map<PeerId, GroupCallParticipant>>;
|
|
|
|
private nextOffsets: Map<GroupCallId, string>;
|
|
|
|
|
|
|
|
// private audioAsset: AudioAsset;
|
|
|
|
|
|
|
|
private currentGroupCall: GroupCallInstance;
|
|
|
|
private connectionAudio: HTMLAudioElement;
|
|
|
|
private doNotDispatchParticipantUpdate: PeerId;
|
|
|
|
private audioAsset: AudioAssetPlayer<GroupCallAudioAssetName>;
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
this.log = logger('GROUP-CALLS');
|
|
|
|
|
|
|
|
this.groupCalls = new Map();
|
|
|
|
this.participants = new Map();
|
|
|
|
this.nextOffsets = new Map();
|
|
|
|
|
|
|
|
rootScope.addMultipleEventsListeners({
|
|
|
|
updateGroupCall: (update) => {
|
|
|
|
this.saveGroupCall(update.call, update.chat_id);
|
|
|
|
},
|
|
|
|
|
|
|
|
updateGroupCallParticipants: (update) => {
|
|
|
|
this.saveGroupCall(update.call);
|
|
|
|
|
|
|
|
// this.getGroupCallFull(update.call.id, true); // ! WARNING TEMP
|
|
|
|
|
|
|
|
const groupCallId = update.call.id;
|
|
|
|
this.saveApiParticipants(groupCallId, update.participants);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
rootScope.addEventListener('group_call_update', (groupCall) => {
|
|
|
|
if(groupCall._ === 'groupCallDiscarded') {
|
|
|
|
const {currentGroupCall} = this;
|
|
|
|
if(currentGroupCall?.id === groupCall.id) {
|
|
|
|
currentGroupCall.hangUp(false, false, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.participants.delete(groupCall.id);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
this.audioAsset = new AudioAssetPlayer<GroupCallAudioAssetName>([
|
|
|
|
'group_call_connect.mp3',
|
|
|
|
'group_call_end.mp3',
|
|
|
|
'group_call_start.mp3',
|
|
|
|
'voip_onallowtalk.mp3'
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
get groupCall() {
|
|
|
|
return this.currentGroupCall;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getCachedParticipants(groupCallId: GroupCallId) {
|
|
|
|
let participants = this.participants.get(groupCallId);
|
|
|
|
if(!participants) {
|
|
|
|
this.participants.set(groupCallId, participants = new Map());
|
|
|
|
}
|
|
|
|
|
|
|
|
return participants;
|
|
|
|
}
|
|
|
|
|
|
|
|
private prepareToSavingNextOffset(groupCallId: GroupCallId) {
|
|
|
|
const nextOffsetsMap = this.nextOffsets;
|
|
|
|
|
|
|
|
const setNextOffset = (newNextOffset: string) => {
|
|
|
|
if(nextOffsetsMap.get(groupCallId) === nextOffset) {
|
|
|
|
nextOffsetsMap.set(groupCallId, newNextOffset);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const nextOffset = nextOffsetsMap.get(groupCallId);
|
|
|
|
return {
|
|
|
|
nextOffset,
|
|
|
|
setNextOffset
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public saveApiParticipant(groupCallId: GroupCallId, participant: GroupCallParticipant, skipCounterUpdating?: boolean) {
|
|
|
|
const {currentGroupCall} = this;
|
|
|
|
const participants = this.getCachedParticipants(groupCallId);
|
|
|
|
|
|
|
|
const peerId = appPeersManager.getPeerId(participant.peer);
|
|
|
|
|
|
|
|
const oldParticipant = participants.get(peerId);
|
|
|
|
const hasLeft = participant.pFlags.left;
|
|
|
|
if(!oldParticipant && hasLeft) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// * fix missing flag
|
|
|
|
if(!participant.pFlags.muted && !participant.pFlags.can_self_unmute) {
|
|
|
|
participant.pFlags.can_self_unmute = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
const isCurrentGroupCall = currentGroupCall?.id === groupCallId;
|
|
|
|
|
|
|
|
if(oldParticipant) {
|
|
|
|
safeReplaceObject(oldParticipant, participant);
|
|
|
|
participant = oldParticipant;
|
|
|
|
} else {
|
|
|
|
participants.set(peerId, participant);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(isCurrentGroupCall) {
|
|
|
|
currentGroupCall.onParticipantUpdate(participant, this.doNotDispatchParticipantUpdate);
|
|
|
|
}
|
|
|
|
|
|
|
|
// if(!skipCounterUpdating) {
|
|
|
|
const groupCall = this.getGroupCall(groupCallId);
|
|
|
|
if(groupCall?._ === 'groupCall') {
|
|
|
|
let modified = false;
|
|
|
|
if(hasLeft) {
|
|
|
|
--groupCall.participants_count;
|
|
|
|
modified = true;
|
|
|
|
} else if(participant.pFlags.just_joined && !oldParticipant && !participant.pFlags.self) {
|
|
|
|
++groupCall.participants_count;
|
|
|
|
modified = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(modified) {
|
|
|
|
rootScope.dispatchEvent('group_call_update', groupCall);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// }
|
|
|
|
|
|
|
|
if(hasLeft) {
|
|
|
|
participants.delete(peerId);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(oldParticipant && this.doNotDispatchParticipantUpdate !== peerId) {
|
|
|
|
rootScope.dispatchEvent('group_call_participant', {
|
|
|
|
groupCallId,
|
|
|
|
participant
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public saveApiParticipants(groupCallId: GroupCallId, apiParticipants: GroupCallParticipant[], skipCounterUpdating?: boolean) {
|
|
|
|
if((apiParticipants as any).saved) return;
|
|
|
|
(apiParticipants as any).saved = true;
|
|
|
|
apiParticipants.forEach(p => this.saveApiParticipant(groupCallId, p, skipCounterUpdating));
|
|
|
|
}
|
|
|
|
|
|
|
|
public async editParticipant(groupCallId: GroupCallId, participant: GroupCallParticipant, options: Partial<{
|
|
|
|
muted: boolean,
|
|
|
|
volume: number,
|
|
|
|
raiseHand: boolean,
|
|
|
|
videoStopped: boolean,
|
|
|
|
videoPaused: boolean,
|
|
|
|
presentationPaused: boolean
|
|
|
|
}>) {
|
|
|
|
if(!Object.keys(options).length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let processUpdate = true;
|
|
|
|
if(participant) {
|
|
|
|
const {currentGroupCall} = this;
|
|
|
|
const isCurrentCall = currentGroupCall?.id === groupCallId;
|
|
|
|
const isUpdatingMeInCurrentCall = isCurrentCall && participant.pFlags.self;
|
|
|
|
|
|
|
|
if(isUpdatingMeInCurrentCall) {
|
|
|
|
if(options.muted !== undefined && !currentGroupCall.isSharingAudio) {
|
|
|
|
delete options.muted;
|
|
|
|
|
|
|
|
if(!Object.keys(options).length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// if(isCurrentCall) {
|
|
|
|
const muted = options.muted;
|
|
|
|
if(muted !== undefined) {
|
|
|
|
/* const isAdmin = appChatsManager.hasRights(currentGroupCall.chatId, 'manage_call');
|
|
|
|
if(isAdmin) {
|
|
|
|
if(muted) {
|
|
|
|
participant.pFlags.muted = true;
|
|
|
|
delete participant.pFlags.can_self_unmute;
|
|
|
|
} else {
|
|
|
|
participant.pFlags.can_self_unmute = true;
|
|
|
|
}
|
|
|
|
} else */if(participant.pFlags.self) {
|
|
|
|
if(muted) {
|
|
|
|
participant.pFlags.muted = true;
|
|
|
|
} else if(participant.pFlags.can_self_unmute) {
|
|
|
|
delete participant.pFlags.muted;
|
|
|
|
}
|
|
|
|
}/* else {
|
|
|
|
if(muted) {
|
|
|
|
participant.pFlags.muted_by_you = true;
|
|
|
|
} else {
|
|
|
|
delete participant.pFlags.muted_by_you;
|
|
|
|
}
|
|
|
|
} */
|
|
|
|
}
|
|
|
|
// }
|
|
|
|
|
|
|
|
/* const a: [keyof GroupCallParticipant['pFlags'], keyof typeof options][] = [
|
|
|
|
['muted', 'muted']
|
|
|
|
];
|
|
|
|
|
|
|
|
a.forEach(([key, optionKey]) => {
|
|
|
|
const value = options[optionKey];
|
|
|
|
if(value === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(value) {
|
|
|
|
participant.pFlags[key] = true;
|
|
|
|
} else {
|
|
|
|
delete participant.pFlags[key];
|
|
|
|
}
|
|
|
|
}); */
|
|
|
|
|
|
|
|
if(options.raiseHand !== undefined) {
|
|
|
|
if(options.raiseHand) participant.raise_hand_rating = '1';
|
|
|
|
else delete participant.raise_hand_rating;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(isUpdatingMeInCurrentCall) {
|
|
|
|
if(options.videoStopped !== undefined) {
|
|
|
|
if(options.videoStopped) delete participant.video;
|
|
|
|
else participant.video = this.generateSelfVideo(currentGroupCall.connections.main.sources.video);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!participant.pFlags.muted && participant.pFlags.can_self_unmute) {
|
|
|
|
currentGroupCall.setMuted(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
currentGroupCall.dispatchEvent('state', currentGroupCall.state);
|
|
|
|
}
|
|
|
|
|
|
|
|
rootScope.dispatchEvent('group_call_participant', {groupCallId, participant});
|
|
|
|
|
|
|
|
/* if(participant.pFlags.self) {
|
|
|
|
processUpdate = false;
|
|
|
|
} */
|
|
|
|
}
|
|
|
|
|
|
|
|
const peerId = participant.pFlags.self ? NULL_PEER_ID : appPeersManager.getPeerId(participant.peer);
|
|
|
|
const updates = await apiManager.invokeApiSingle('phone.editGroupCallParticipant', {
|
|
|
|
call: appGroupCallsManager.getGroupCallInput(groupCallId),
|
|
|
|
participant: peerId === NULL_PEER_ID ? appPeersManager.getInputPeerSelf() : appPeersManager.getInputPeerById(peerId),
|
|
|
|
muted: options.muted,
|
|
|
|
volume: options.volume,
|
|
|
|
raise_hand: options.raiseHand,
|
|
|
|
video_paused: options.videoPaused,
|
|
|
|
video_stopped: options.videoStopped,
|
|
|
|
presentation_paused: options.presentationPaused
|
|
|
|
});
|
|
|
|
|
|
|
|
// do not replace with peerId because it can be null
|
|
|
|
if(!processUpdate) this.doNotDispatchParticipantUpdate = appPeersManager.getPeerId(participant.peer);
|
|
|
|
apiUpdatesManager.processUpdateMessage(updates);
|
|
|
|
if(!processUpdate) this.doNotDispatchParticipantUpdate = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getGroupCall(id: GroupCallId) {
|
|
|
|
return this.groupCalls.get(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
public async getGroupCallFull(id: GroupCallId, override?: boolean): Promise<GroupCall> {
|
|
|
|
const call = this.getGroupCall(id);
|
|
|
|
if(call && call._ !== 'inputGroupCall' && !override) {
|
|
|
|
return call;
|
|
|
|
}
|
|
|
|
|
|
|
|
const limit = this.getCachedParticipants(id).size ? 0 : GET_PARTICIPANTS_LIMIT;
|
|
|
|
return apiManager.invokeApiSingleProcess({
|
|
|
|
method: 'phone.getGroupCall',
|
|
|
|
params: {
|
|
|
|
call: this.getGroupCallInput(id),
|
|
|
|
limit
|
|
|
|
},
|
|
|
|
processResult: (groupCall) => {
|
|
|
|
// ? maybe I should save group call after participants so I can avoid passing the 'skipCounterUpdating' flag ?
|
|
|
|
appUsersManager.saveApiUsers(groupCall.users);
|
|
|
|
appChatsManager.saveApiChats(groupCall.chats);
|
|
|
|
this.saveApiParticipants(id, groupCall.participants, true);
|
|
|
|
const call = this.saveGroupCall(groupCall.call) as GroupCall;
|
|
|
|
|
|
|
|
if(limit && this.nextOffsets.get(id) === undefined) {
|
|
|
|
this.nextOffsets.set(id, groupCall.participants_next_offset);
|
|
|
|
}
|
|
|
|
|
|
|
|
return call;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public saveGroupCall(call: MyGroupCall, chatId?: ChatId) {
|
|
|
|
const oldCall = this.groupCalls.get(call.id);
|
|
|
|
const shouldUpdate = call._ !== 'inputGroupCall' && (!oldCall || oldCall._ !== 'groupCallDiscarded');
|
|
|
|
if(oldCall) {
|
|
|
|
if(shouldUpdate) {
|
|
|
|
safeReplaceObject(oldCall, call);
|
|
|
|
}
|
|
|
|
|
|
|
|
call = oldCall;
|
|
|
|
} else {
|
|
|
|
this.groupCalls.set(call.id, call);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(shouldUpdate) {
|
|
|
|
rootScope.dispatchEvent('group_call_update', call as any);
|
|
|
|
}
|
|
|
|
|
|
|
|
return call;
|
|
|
|
}
|
|
|
|
|
|
|
|
public startConnectingSound() {
|
|
|
|
this.stopConnectingSound();
|
|
|
|
this.audioAsset.playSoundWithTimeout('group_call_connect.mp3', true, 2500);
|
|
|
|
}
|
|
|
|
|
|
|
|
public stopConnectingSound() {
|
|
|
|
this.audioAsset.stopSound();
|
|
|
|
this.audioAsset.cancelDelayedPlay();
|
|
|
|
}
|
|
|
|
|
|
|
|
public setCurrentGroupCall(groupCall: GroupCallInstance) {
|
|
|
|
this.currentGroupCall = groupCall;
|
|
|
|
|
|
|
|
if(groupCall) {
|
|
|
|
rootScope.dispatchEvent('group_call_instance', groupCall);
|
|
|
|
}
|
|
|
|
/* TdLibController.clientUpdate({
|
|
|
|
'@type': 'clientUpdateGroupCall',
|
|
|
|
call
|
|
|
|
}); */
|
|
|
|
}
|
|
|
|
|
|
|
|
public async createGroupCall(chatId: ChatId, scheduleDate?: number, title?: string) {
|
|
|
|
const updates = await apiManager.invokeApi('phone.createGroupCall', {
|
|
|
|
peer: appPeersManager.getInputPeerById(chatId.toPeerId(true)),
|
|
|
|
random_id: nextRandomUint(32),
|
|
|
|
schedule_date: scheduleDate,
|
|
|
|
title
|
|
|
|
});
|
|
|
|
|
|
|
|
apiUpdatesManager.processUpdateMessage(updates);
|
|
|
|
|
|
|
|
const update = (updates as Updates.updates).updates.find(update => update._ === 'updateGroupCall') as Update.updateGroupCall;
|
|
|
|
return update.call;
|
|
|
|
}
|
|
|
|
|
|
|
|
public async joinGroupCall(chatId: ChatId, groupCallId: GroupCallId, muted = IS_MUTED, rejoin?: boolean, joinVideo?: boolean) {
|
|
|
|
this.audioAsset.createAudio();
|
|
|
|
|
|
|
|
this.log(`joinGroupCall chatId=${chatId} id=${groupCallId} muted=${muted} rejoin=${rejoin}`);
|
|
|
|
|
|
|
|
let streamManager: StreamManager;
|
|
|
|
if(rejoin) {
|
|
|
|
streamManager = this.currentGroupCall.connections.main.streamManager;
|
|
|
|
} else {
|
|
|
|
streamManager = await createMainStreamManager(muted, joinVideo);
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.joinGroupCallInternal(chatId, groupCallId, streamManager, muted, rejoin, joinVideo);
|
|
|
|
}
|
|
|
|
|
|
|
|
public async joinGroupCallInternal(chatId: ChatId, groupCallId: GroupCallId, streamManager: StreamManager, muted: boolean, rejoin = false, joinVideo?: boolean) {
|
|
|
|
const log = this.log.bindPrefix('joinGroupCallInternal');
|
|
|
|
log('start', groupCallId);
|
|
|
|
|
|
|
|
const type: GroupCallConnectionType = 'main';
|
|
|
|
|
|
|
|
let {currentGroupCall} = this;
|
|
|
|
if(currentGroupCall && rejoin) {
|
|
|
|
// currentGroupCall.connections.main.connection = connection;
|
|
|
|
currentGroupCall.handleUpdateGroupCallParticipants = false;
|
|
|
|
currentGroupCall.updatingSdp = false;
|
|
|
|
log('update currentGroupCall', groupCallId, currentGroupCall);
|
|
|
|
} else {
|
|
|
|
currentGroupCall = new GroupCallInstance({
|
|
|
|
chatId,
|
|
|
|
id: groupCallId
|
|
|
|
});
|
|
|
|
|
|
|
|
currentGroupCall.fixSafariAudio();
|
|
|
|
|
|
|
|
currentGroupCall.addEventListener('state', (state) => {
|
|
|
|
if(this.currentGroupCall === currentGroupCall && state === GROUP_CALL_STATE.CLOSED) {
|
|
|
|
this.setCurrentGroupCall(null);
|
|
|
|
this.stopConnectingSound();
|
|
|
|
this.audioAsset.playSound('group_call_end.mp3');
|
|
|
|
rootScope.dispatchEvent('chat_update', currentGroupCall.chatId);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
currentGroupCall.groupCall = await this.getGroupCallFull(groupCallId);
|
|
|
|
|
|
|
|
const connectionInstance = currentGroupCall.createConnectionInstance({
|
|
|
|
streamManager,
|
|
|
|
type,
|
|
|
|
options: {
|
|
|
|
type,
|
|
|
|
isMuted: muted,
|
|
|
|
joinVideo,
|
|
|
|
rejoin
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
const connection = connectionInstance.createPeerConnection();
|
|
|
|
connection.addEventListener('negotiationneeded', () => {
|
|
|
|
connectionInstance.negotiate();
|
|
|
|
});
|
|
|
|
|
|
|
|
connection.addEventListener('track', (event) => {
|
|
|
|
log('ontrack', event);
|
|
|
|
currentGroupCall.onTrack(event);
|
|
|
|
});
|
|
|
|
|
|
|
|
connection.addEventListener('iceconnectionstatechange', () => {
|
|
|
|
currentGroupCall.dispatchEvent('state', currentGroupCall.state);
|
|
|
|
|
|
|
|
const {iceConnectionState} = connection;
|
|
|
|
if(iceConnectionState === 'disconnected' || iceConnectionState === 'checking' || iceConnectionState === 'new') {
|
|
|
|
this.startConnectingSound();
|
|
|
|
} else {
|
|
|
|
this.stopConnectingSound();
|
|
|
|
}
|
|
|
|
|
|
|
|
switch(iceConnectionState) {
|
|
|
|
case 'checking': {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case 'closed': {
|
|
|
|
currentGroupCall.hangUp();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case 'completed': {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case 'connected': {
|
|
|
|
if(!currentGroupCall.joined) {
|
|
|
|
currentGroupCall.joined = true;
|
|
|
|
this.audioAsset.playSound('group_call_start.mp3');
|
|
|
|
|
|
|
|
this.getGroupCallParticipants(groupCallId).then(({participants}) => {
|
|
|
|
this.saveApiParticipants(groupCallId, [...participants.values()]);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case 'disconnected': {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case 'failed': {
|
|
|
|
//TODO: replace with ICE restart
|
|
|
|
currentGroupCall.hangUp();
|
|
|
|
// connection.restartIce();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case 'new': {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
connectionInstance.createDescription();
|
|
|
|
connectionInstance.createDataChannel();
|
|
|
|
|
|
|
|
connectionInstance.appendStreamToConference();
|
|
|
|
|
|
|
|
this.setCurrentGroupCall(currentGroupCall);
|
|
|
|
log('set currentGroupCall', groupCallId, currentGroupCall);
|
|
|
|
|
|
|
|
this.startConnectingSound();
|
|
|
|
|
|
|
|
return connectionInstance.negotiate();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public getGroupCallInput(id: GroupCallId): InputGroupCall {
|
|
|
|
const groupCall = this.getGroupCall(id);
|
|
|
|
return {
|
|
|
|
_: 'inputGroupCall',
|
|
|
|
id: groupCall.id,
|
|
|
|
access_hash: groupCall.access_hash
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public generateSelfVideo(source: Ssrc, audioSource?: number): GroupCallParticipantVideo {
|
|
|
|
return source && {
|
|
|
|
_: 'groupCallParticipantVideo',
|
|
|
|
pFlags: {},
|
|
|
|
endpoint: '',
|
|
|
|
source_groups: source.sourceGroups,
|
|
|
|
audio_source: audioSource
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public generateSelfParticipant(): GroupCallParticipant {
|
|
|
|
const mainSources = this.currentGroupCall.connections.main.sources;
|
|
|
|
const presentationSources = this.currentGroupCall.connections.presentation?.sources;
|
|
|
|
return {
|
|
|
|
_: 'groupCallParticipant',
|
|
|
|
pFlags: {
|
|
|
|
can_self_unmute: true,
|
|
|
|
self: true
|
|
|
|
},
|
|
|
|
source: mainSources.audio.source,
|
|
|
|
video: this.generateSelfVideo(mainSources.video),
|
|
|
|
presentation: presentationSources && this.generateSelfVideo(presentationSources.video, presentationSources.audio?.source),
|
|
|
|
date: tsNow(true),
|
|
|
|
peer: appPeersManager.getOutputPeer(rootScope.myId)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public makeSsrcsFromParticipant = (participant: GroupCallParticipant) => {
|
|
|
|
return [
|
|
|
|
this.makeSsrcFromParticipant(participant, 'audio', participant.source),
|
|
|
|
participant.video?.audio_source && this.makeSsrcFromParticipant(participant, 'audio', participant.video.audio_source),
|
|
|
|
participant.video && this.makeSsrcFromParticipant(participant, 'video', participant.video.source_groups, participant.video.endpoint),
|
|
|
|
participant.presentation?.audio_source && this.makeSsrcFromParticipant(participant, 'audio', participant.presentation.audio_source),
|
|
|
|
participant.presentation && this.makeSsrcFromParticipant(participant, 'video', participant.presentation.source_groups, participant.presentation.endpoint)
|
|
|
|
].filter(Boolean);
|
|
|
|
};
|
|
|
|
|
|
|
|
public makeSsrcFromParticipant(participant: GroupCallParticipant, type: WebRTCLineType, source?: number | GroupCallParticipantVideoSourceGroup[], endpoint?: string): Ssrc {
|
|
|
|
return generateSsrc(type, source, endpoint);
|
|
|
|
}
|
|
|
|
|
|
|
|
public async getGroupCallParticipants(id: GroupCallId) {
|
|
|
|
const {nextOffset, setNextOffset} = this.prepareToSavingNextOffset(id);
|
|
|
|
|
|
|
|
if(nextOffset !== '') {
|
|
|
|
await apiManager.invokeApiSingleProcess({
|
|
|
|
method: 'phone.getGroupParticipants',
|
|
|
|
params: {
|
|
|
|
call: this.getGroupCallInput(id),
|
|
|
|
ids: [],
|
|
|
|
sources: [],
|
|
|
|
offset: nextOffset || '',
|
|
|
|
limit: GET_PARTICIPANTS_LIMIT
|
|
|
|
},
|
|
|
|
processResult: (groupCallParticipants) => {
|
|
|
|
const newNextOffset = groupCallParticipants.count === groupCallParticipants.participants.length ? '' : groupCallParticipants.next_offset;
|
|
|
|
|
|
|
|
appChatsManager.saveApiChats(groupCallParticipants.chats);
|
|
|
|
appUsersManager.saveApiUsers(groupCallParticipants.users);
|
|
|
|
this.saveApiParticipants(id, groupCallParticipants.participants);
|
|
|
|
|
|
|
|
setNextOffset(newNextOffset);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
participants: this.getCachedParticipants(id),
|
|
|
|
isEnd: this.nextOffsets.get(id) === ''
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public async hangUp(groupCallId: GroupCallId, discard = false, rejoin = false) {
|
|
|
|
this.log(`hangUp start id=${groupCallId} discard=${discard} rejoin=${rejoin}`);
|
|
|
|
const {currentGroupCall} = this;
|
|
|
|
if(currentGroupCall?.id !== groupCallId) return;
|
|
|
|
|
|
|
|
currentGroupCall.hangUp(discard, rejoin);
|
|
|
|
}
|
|
|
|
|
|
|
|
public toggleMuted(muted?: boolean) {
|
|
|
|
return this.changeUserMuted(NULL_PEER_ID, muted);
|
|
|
|
}
|
|
|
|
|
|
|
|
public changeUserMuted(peerId: PeerId, muted?: boolean) {
|
|
|
|
const {currentGroupCall} = this;
|
|
|
|
if(!currentGroupCall) return;
|
|
|
|
|
|
|
|
const participant = currentGroupCall.getParticipantByPeerId(peerId);
|
|
|
|
if(NULL_PEER_ID === peerId && participant.pFlags.can_self_unmute) {
|
|
|
|
muted = muted === undefined ? !participant.pFlags.muted : muted;
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.editParticipant(currentGroupCall.id, participant, {muted});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const appGroupCallsManager = new AppGroupCallsManager();
|
|
|
|
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.appGroupCallsManager = appGroupCallsManager);
|
|
|
|
export default appGroupCallsManager;
|