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.
 
 
 
 
 

572 lines
17 KiB

/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type { GroupCallConnectionType, GroupCallId, GroupCallOutputSource } from "../appManagers/appGroupCallsManager";
import { IS_SAFARI } from "../../environment/userAgent";
import indexOfAndSplice from "../../helpers/array/indexOfAndSplice";
import safeAssign from "../../helpers/object/safeAssign";
import throttle from "../../helpers/schedulers/throttle";
import { GroupCall, GroupCallParticipant } from "../../layer";
import { logger } from "../logger";
import { NULL_PEER_ID } from "../mtproto/mtproto_config";
import rootScope from "../rootScope";
import CallInstanceBase, { TryAddTrackOptions } from "./callInstanceBase";
import GroupCallConnectionInstance from "./groupCallConnectionInstance";
import GROUP_CALL_STATE from "./groupCallState";
import getScreenConstraints from "./helpers/getScreenConstraints";
import getScreenStream from "./helpers/getScreenStream";
import getStream from "./helpers/getStream";
import getVideoConstraints from "./helpers/getVideoConstraints";
import stopTrack from "./helpers/stopTrack";
import localConferenceDescription from "./localConferenceDescription";
import { WebRTCLineType } from "./sdpBuilder";
import StreamManager from "./streamManager";
import { Ssrc } from "./types";
import getPeerId from "../appManagers/utils/peers/getPeerId";
import { AppManagers } from "../appManagers/managers";
import { generateSelfVideo, makeSsrcFromParticipant, makeSsrcsFromParticipant } from "./groupCallsController";
export default class GroupCallInstance extends CallInstanceBase<{
state: (state: GROUP_CALL_STATE) => void,
pinned: (source?: GroupCallOutputSource) => void,
}> {
public id: GroupCallId;
public chatId: ChatId;
public handleUpdateGroupCallParticipants: boolean;
public updatingSdp: boolean;
public isSpeakingMap: Map<any, any>;
public connections: {[k in GroupCallConnectionType]?: GroupCallConnectionInstance};
public groupCall: GroupCall;
public participant: GroupCallParticipant;
// will be set with negotiation
public joined: boolean;
private pinnedSources: Array<GroupCallOutputSource>;
private participantsSsrcs: Map<PeerId, Ssrc[]>;
private hadAutoPinnedSources: Set<GroupCallOutputSource>;
private dispatchPinnedThrottled: () => void;
private startVideoSharingPromise: Promise<void>;
private startScreenSharingPromise: Promise<void>;
private managers: AppManagers;
constructor(options: {
id: GroupCallInstance['id'],
chatId: GroupCallInstance['chatId'],
isSpeakingMap?: GroupCallInstance['isSpeakingMap'],
connections?: GroupCallInstance['connections'],
managers: AppManagers
}) {
super();
safeAssign(this, options);
if(!this.log) {
this.log = logger('GROUP-CALL');
}
if(!this.connections) {
this.connections = {};
}
if(!this.isSpeakingMap) {
this.isSpeakingMap = new Map();
}
this.pinnedSources = [];
this.participantsSsrcs = new Map();
this.hadAutoPinnedSources = new Set();
this.dispatchPinnedThrottled = throttle(() => {
this.dispatchEvent('pinned', this.pinnedSource);
}, 0, false);
this.addEventListener('state', (state) => {
if(state === GROUP_CALL_STATE.CLOSED) {
this.cleanup();
}
});
}
get connectionState() {
return this.connections.main.connection.iceConnectionState;
}
get state() {
const {connectionState} = this;
if(connectionState === 'closed') {
return GROUP_CALL_STATE.CLOSED;
} else if(connectionState !== 'connected' && (!IS_SAFARI || connectionState !== 'completed')) {
return GROUP_CALL_STATE.CONNECTING;
} else {
const {participant} = this;
if(!participant.pFlags.can_self_unmute) {
return GROUP_CALL_STATE.MUTED_BY_ADMIN;
} else if(participant.pFlags.muted) {
return GROUP_CALL_STATE.MUTED;
} else {
return GROUP_CALL_STATE.UNMUTED;
}
}
}
get participants() {
return this.managers.appGroupCallsManager.getCachedParticipants(this.id);
}
get isSharingScreen() {
return !!this.connections.presentation;
}
get pinnedSource() {
return this.pinnedSources[this.pinnedSources.length - 1];
}
public get isMuted() {
return this.state !== GROUP_CALL_STATE.UNMUTED;
}
public get isClosing() {
const {state} = this;
return state === GROUP_CALL_STATE.CLOSED;
}
public get streamManager(): StreamManager {
return this.connections.main.streamManager;
}
public get description(): localConferenceDescription {
return this.connections.main.description;
}
public pinSource(source: GroupCallOutputSource) {
indexOfAndSplice(this.pinnedSources, source);
this.pinnedSources.push(source);
this.dispatchPinnedThrottled();
}
public unpinSource(source: GroupCallOutputSource) {
this.hadAutoPinnedSources.delete(source);
indexOfAndSplice(this.pinnedSources, source);
this.dispatchPinnedThrottled();
}
public unpinAll() {
this.pinnedSources.length = 0;
this.dispatchPinnedThrottled();
}
public async getParticipantByPeerId(peerId: PeerId) {
return NULL_PEER_ID === peerId ? this.participant : (await this.participants).get(peerId);
}
public toggleMuted() {
return this.requestAudioSource(true).then(() => this.changeUserMuted(NULL_PEER_ID));
}
public async changeUserMuted(peerId: PeerId, muted?: boolean) {
const participant = await this.getParticipantByPeerId(peerId);
if(NULL_PEER_ID === peerId && participant.pFlags.can_self_unmute) {
muted = muted === undefined ? !participant.pFlags.muted : muted;
}
return this.editParticipant(participant, {muted});
}
public getElement(endpoint: GroupCallOutputSource) {
return super.getElement(endpoint);
}
public getVideoElementFromParticipantByType(participant: GroupCallParticipant, type: 'video' | 'presentation') {
let source: GroupCallOutputSource;
if(participant.pFlags.self) {
const connectionType: GroupCallConnectionType = type === 'video' ? 'main' : 'presentation';
source = connectionType;
} else {
const codec = participant[type];
source = codec.source_groups[0].sources[0];
}
const element = this.getElement(source) as HTMLVideoElement;
if(!element) return;
const clone = element.cloneNode() as typeof element;
clone.srcObject = element.srcObject;
return {video: clone, source};
}
public createConnectionInstance(options: {
streamManager: StreamManager,
type: GroupCallConnectionType,
options: GroupCallConnectionInstance['options'],
}) {
return this.connections[options.type] = new GroupCallConnectionInstance({
groupCall: this,
log: this.log.bindPrefix(options.type),
managers: this.managers,
...options
});
}
public changeRaiseHand(raise: boolean) {
return this.editParticipant(this.participant, {raiseHand: raise});
}
public async startScreenSharingInternal() {
try {
const type: GroupCallConnectionType = 'presentation';
const stream = await getScreenStream(getScreenConstraints());
const streamManager = new StreamManager();
const connectionInstance = this.createConnectionInstance({
streamManager,
type,
options: {
type
}
});
const connection = connectionInstance.createPeerConnection();
connection.addEventListener('negotiationneeded', () => {
connectionInstance.negotiate();
});
stream.getVideoTracks()[0].addEventListener('ended', () => {
if(this.connections.presentation) { // maybe user has stopped screensharing through browser's ui
this.stopScreenSharing();
}
}, {once: true});
connectionInstance.createDescription();
connectionInstance.addInputVideoStream(stream);
} catch(err) {
this.log.error('start screen sharing error', err);
}
}
public startScreenSharing() {
return this.startScreenSharingPromise ??= this.startScreenSharingInternal().finally(() => {
this.startScreenSharingPromise = undefined;
});
}
public stopScreenSharing() {
const connectionInstance = this.connections.presentation;
if(!connectionInstance) {
return Promise.resolve();
}
delete this.connections.presentation;
this.unpinSource('presentation');
connectionInstance.closeConnectionAndStream(true);
delete this.participant.presentation;
this.managers.appGroupCallsManager.saveApiParticipant(this.id, this.participant);
return this.managers.appGroupCallsManager.leaveGroupCallPresentation(this.id);
}
public toggleScreenSharing() {
if(this.isSharingScreen) {
return this.stopScreenSharing();
} else {
return this.startScreenSharing();
}
}
public async startVideoSharingInternal() {
const constraints: MediaStreamConstraints = {
video: getVideoConstraints()
};
try {
const stream = await getStream(constraints, false);
const connectionInstance = this.connections.main;
connectionInstance.addInputVideoStream(stream);
await this.editParticipant(this.participant, {
videoPaused: false,
videoStopped: false
});
} catch(err) {
this.log.error('startVideoSharing error', err, constraints);
}
}
public startVideoSharing() {
return this.startVideoSharingPromise ??= this.startVideoSharingInternal().finally(() => {
this.startVideoSharingPromise = undefined;
});
}
public async stopVideoSharing() {
const connectionInstance = this.connections.main;
const track = connectionInstance.streamManager.inputStream.getVideoTracks()[0];
if(!track) {
return;
}
stopTrack(track);
connectionInstance.streamManager.appendToConference(connectionInstance.description); // clear sender track
await this.editParticipant(this.participant, {
videoStopped: true
});
}
public toggleVideoSharing() {
if(this.isSharingVideo) {
return this.stopVideoSharing();
} else {
return this.startVideoSharing();
}
}
public async hangUp(discard = false, rejoin = false, isDiscarded = false) {
for(const type in this.connections) {
const connection = this.connections[type as GroupCallConnectionType];
connection.closeConnectionAndStream(!rejoin);
}
this.dispatchEvent('state', this.state);
if(isDiscarded) {
return;
}
if(!rejoin) {
let d = discard || (this.joined ? this.connections.main.sources.audio.source : undefined);
this.managers.appGroupCallsManager.hangUp(this.id, d);
}
}
public tryAddTrack(options: Omit<TryAddTrackOptions, 'streamManager'>) {
const {description} = this;
const source = super.tryAddTrack(options);
if(options.type === 'output') {
const entry = description.getEntryBySource(+source);
this.getParticipantByPeerId(entry.peerId).then((participant) => {
if(participant) {
rootScope.dispatchEvent('group_call_participant', {groupCallId: this.id, participant});
}
});
}
return source;
}
public async editParticipant(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 isCurrentCall = true;
const isUpdatingMeInCurrentCall = isCurrentCall && participant.pFlags.self;
if(isUpdatingMeInCurrentCall) {
if(options.muted !== undefined && !this.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 = generateSelfVideo(this.connections.main.sources.video);
}
if(!participant.pFlags.muted && participant.pFlags.can_self_unmute) {
this.setMuted(false);
}
this.dispatchEvent('state', this.state);
}
// rootScope.dispatchEvent('group_call_participant', {groupCallId, participant});
/* if(participant.pFlags.self) {
processUpdate = false;
} */
}
return this.managers.appGroupCallsManager.editParticipant(this.id, participant, options);
}
public onParticipantUpdate(participant: GroupCallParticipant, doNotDispatchParticipantUpdate?: PeerId) {
const connectionInstance = this.connections.main;
const {connection, description} = connectionInstance;
const peerId = getPeerId(participant.peer);
const hasLeft = !!participant.pFlags.left;
const oldSsrcs = this.participantsSsrcs.get(peerId) || [];
if(participant.presentation && !hasLeft) {
const {source} = makeSsrcFromParticipant(participant, 'video', participant.presentation.source_groups, participant.presentation.endpoint);
if(!this.hadAutoPinnedSources.has(source)) {
this.hadAutoPinnedSources.add(source);
this.pinSource(participant.pFlags.self ? 'presentation' : source);
}
}
if(participant.pFlags.self) {
this.participant = participant;
if(connectionInstance.sources.audio.source !== participant.source) {
this.hangUp();
}
let mute = false;
if(!participant.pFlags.can_self_unmute) {
this.stopScreenSharing();
this.stopVideoSharing();
mute = true;
} else if(participant.pFlags.muted) {
mute = true;
}
if(mute) {
this.setMuted(true);
}
if(doNotDispatchParticipantUpdate !== peerId) {
this.dispatchEvent('state', this.state);
}
return;
}
const ssrcs = hasLeft ? [] : makeSsrcsFromParticipant(participant);
if(!hasLeft) {
this.participantsSsrcs.set(peerId, ssrcs);
} else {
this.participantsSsrcs.delete(peerId);
}
// const TEST_OLD = false;
const modifiedTypes: Set<WebRTCLineType> = new Set();
oldSsrcs.forEach((oldSsrc) => {
const oldSource = oldSsrc.source;
const newSsrc = ssrcs.find((ssrc) => ssrc.source === oldSource);
if(!newSsrc) {
this.unpinSource(oldSource);
const oldEntry = description.getEntryBySource(oldSource);
if(oldEntry && oldEntry.direction !== 'inactive') {
oldEntry.setDirection('inactive');
modifiedTypes.add(oldEntry.type);
}
}
});
ssrcs.forEach((ssrc) => {
let entry = description.getEntryBySource(ssrc.source);
if(entry) {
if(entry.direction === 'inactive') {
entry.setDirection(entry.originalDirection);
modifiedTypes.add(entry.type);
}
return;
}
entry = description.createEntry(ssrc.type);
description.setEntrySource(entry, ssrc.sourceGroups || ssrc.source);
description.setEntryPeerId(entry, peerId);
// if(TEST_OLD) {
// description.bundleMids.push(entry.mid);
// entry.setDirection('recvonly');
// } else {
ssrc.type === 'video' && entry.setEndpoint(ssrc.endpoint);
entry.createTransceiver(connection, {direction: 'recvonly'});
// }
modifiedTypes.add(entry.type);
});
/* if(TEST_OLD) {
this.setRemoteOffer({
connection,
description,
ssrcs
});
} else */if(modifiedTypes.size) {
if(modifiedTypes.has('video')) {
connectionInstance.updateConstraints = true;
}
connectionInstance.negotiateThrottled();
}
}
}