Telegram Web K with changes to work inside I2P https://web.telegram.i2p/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

255 lines
8.5 KiB

/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import getGroupCallAudioAsset from '../../components/groupCall/getAudioAsset';
import {MOUNT_CLASS_TO} from '../../config/debug';
import EventListenerBase from '../../helpers/eventListenerBase';
import {GroupCallParticipant, GroupCallParticipantVideo, GroupCallParticipantVideoSourceGroup} from '../../layer';
import {GroupCallId, GroupCallConnectionType} from '../appManagers/appGroupCallsManager';
import {AppManagers} from '../appManagers/managers';
import {logger} from '../logger';
import rootScope from '../rootScope';
import GroupCallInstance from './groupCallInstance';
import GROUP_CALL_STATE from './groupCallState';
import createMainStreamManager from './helpers/createMainStreamManager';
import {generateSsrc} from './localConferenceDescription';
import {WebRTCLineType} from './sdpBuilder';
import StreamManager from './streamManager';
import {Ssrc} from './types';
const IS_MUTED = true;
export function makeSsrcsFromParticipant(participant: GroupCallParticipant) {
return [
makeSsrcFromParticipant(participant, 'audio', participant.source),
participant.video?.audio_source && makeSsrcFromParticipant(participant, 'audio', participant.video.audio_source),
participant.video && makeSsrcFromParticipant(participant, 'video', participant.video.source_groups, participant.video.endpoint),
participant.presentation?.audio_source && makeSsrcFromParticipant(participant, 'audio', participant.presentation.audio_source),
participant.presentation && makeSsrcFromParticipant(participant, 'video', participant.presentation.source_groups, participant.presentation.endpoint)
].filter(Boolean);
};
export function makeSsrcFromParticipant(participant: GroupCallParticipant, type: WebRTCLineType, source?: number | GroupCallParticipantVideoSourceGroup[], endpoint?: string): Ssrc {
return generateSsrc(type, source, endpoint);
}
export function generateSelfVideo(source: Ssrc, audioSource?: number): GroupCallParticipantVideo {
return source && {
_: 'groupCallParticipantVideo',
pFlags: {},
endpoint: '',
source_groups: source.sourceGroups,
audio_source: audioSource
};
}
export class GroupCallsController extends EventListenerBase<{
instance: (instance: GroupCallInstance) => void
}> {
private audioAsset: ReturnType<typeof getGroupCallAudioAsset>;
private log: ReturnType<typeof logger>;
private currentGroupCall: GroupCallInstance;
private managers: AppManagers;
public construct(managers: AppManagers) {
this.managers = managers;
this.audioAsset = getGroupCallAudioAsset();
this.log = logger('GCC');
rootScope.addEventListener('group_call_update', (groupCall) => {
const {currentGroupCall} = this;
if(currentGroupCall?.id === groupCall.id) {
currentGroupCall.groupCall = groupCall;
if(groupCall._ === 'groupCallDiscarded') {
currentGroupCall.hangUp(false, false, true);
}
}
});
rootScope.addEventListener('group_call_participant', ({groupCallId, participant}) => {
const {currentGroupCall} = this;
if(currentGroupCall?.id === groupCallId) {
currentGroupCall.onParticipantUpdate(participant/* , this.doNotDispatchParticipantUpdate */);
}
});
}
get groupCall() {
return this.currentGroupCall;
}
public setCurrentGroupCall(groupCall: GroupCallInstance) {
this.currentGroupCall = groupCall;
if(groupCall) {
this.dispatchEvent('instance', groupCall);
}
}
public startConnectingSound() {
this.stopConnectingSound();
this.audioAsset.playSoundWithTimeout('group_call_connect.mp3', true, 2500);
}
public stopConnectingSound() {
this.audioAsset.stopSound();
this.audioAsset.cancelDelayedPlay();
}
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)
.then(() => {
// have to refresh participants because of the new connection
const {currentGroupCall} = this;
currentGroupCall.participants.then((participants) => {
if(this.currentGroupCall !== currentGroupCall || currentGroupCall.state === GROUP_CALL_STATE.CLOSED) {
return;
}
participants.forEach((participant) => {
if(!participant.pFlags.self) {
currentGroupCall.onParticipantUpdate(participant);
}
});
});
});
}
private 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,
managers: this.managers
});
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.managers.appGroupCallsManager.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.managers.appGroupCallsManager.getGroupCallParticipants(groupCallId);
}
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();
}
}
}
const groupCallsController = new GroupCallsController();
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.groupCallController = groupCallsController);
export default groupCallsController;