ca1213c32f
Fix chat date blinking Fix displaying sent messages to new dialog Scroll to date bubble if message is bigger than viewport Fix releasing keyboard by inline helper Fix clearing self user Fix displaying sent public poll Update contacts counter in dialogs placeholder Improve multiselect animation Disable lottie icon animations if they're disabled Fix changing mtproto transport during authorization
372 lines
12 KiB
TypeScript
372 lines
12 KiB
TypeScript
/*
|
|
* https://github.com/morethanwords/tweb
|
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
|
*/
|
|
|
|
import { forEachReverse } from "../../helpers/array";
|
|
import throttle from "../../helpers/schedulers/throttle";
|
|
import { Updates, PhoneJoinGroupCall, PhoneJoinGroupCallPresentation, Update } from "../../layer";
|
|
import apiUpdatesManager from "../appManagers/apiUpdatesManager";
|
|
import appGroupCallsManager, { GroupCallConnectionType, JoinGroupCallJsonPayload } from "../appManagers/appGroupCallsManager";
|
|
import apiManager from "../mtproto/apiManager";
|
|
import rootScope from "../rootScope";
|
|
import CallConnectionInstanceBase, { CallConnectionInstanceOptions } from "./callConnectionInstanceBase";
|
|
import GroupCallInstance from "./groupCallInstance";
|
|
import filterServerCodecs from "./helpers/filterServerCodecs";
|
|
import fixLocalOffer from "./helpers/fixLocalOffer";
|
|
import processMediaSection from "./helpers/processMediaSection";
|
|
import { ConferenceEntry } from "./localConferenceDescription";
|
|
import SDP from "./sdp";
|
|
import SDPMediaSection from "./sdp/mediaSection";
|
|
import { WebRTCLineType } from "./sdpBuilder";
|
|
import { UpdateGroupCallConnectionData } from "./types";
|
|
|
|
export default class GroupCallConnectionInstance extends CallConnectionInstanceBase {
|
|
private groupCall: GroupCallInstance;
|
|
public updateConstraints?: boolean;
|
|
private type: GroupCallConnectionType;
|
|
private options: {
|
|
type: Extract<GroupCallConnectionType, 'main'>,
|
|
isMuted?: boolean,
|
|
joinVideo?: boolean,
|
|
rejoin?: boolean
|
|
} | {
|
|
type: Extract<GroupCallConnectionType, 'presentation'>,
|
|
};
|
|
|
|
private updateConstraintsInterval: number;
|
|
public negotiateThrottled: () => void;
|
|
|
|
constructor(options: CallConnectionInstanceOptions & {
|
|
groupCall: GroupCallConnectionInstance['groupCall'],
|
|
type: GroupCallConnectionInstance['type'],
|
|
options: GroupCallConnectionInstance['options'],
|
|
}) {
|
|
super(options);
|
|
|
|
this.negotiateThrottled = throttle(this.negotiate.bind(this), 0, false);
|
|
}
|
|
|
|
public createPeerConnection() {
|
|
return this.connection || super.createPeerConnection({
|
|
iceServers: [],
|
|
iceTransportPolicy: 'all',
|
|
bundlePolicy: 'max-bundle',
|
|
rtcpMuxPolicy: 'require',
|
|
iceCandidatePoolSize: 0,
|
|
// sdpSemantics: "unified-plan",
|
|
// extmapAllowMixed: true,
|
|
});
|
|
}
|
|
|
|
public createDataChannel() {
|
|
if(this.dataChannel) {
|
|
return this.dataChannel;
|
|
}
|
|
|
|
const dataChannel = super.createDataChannel();
|
|
|
|
dataChannel.addEventListener('open', () => {
|
|
this.maybeUpdateRemoteVideoConstraints();
|
|
});
|
|
|
|
dataChannel.addEventListener('close', () => {
|
|
if(this.updateConstraintsInterval) {
|
|
clearInterval(this.updateConstraintsInterval);
|
|
this.updateConstraintsInterval = undefined;
|
|
}
|
|
});
|
|
|
|
return dataChannel;
|
|
}
|
|
|
|
public createDescription() {
|
|
if(this.description) {
|
|
return this.description;
|
|
}
|
|
|
|
const description = super.createDescription();
|
|
|
|
/* const perType = 0;
|
|
const types = ['audio' as const, 'video' as const];
|
|
const count = types.length * perType;
|
|
const init: RTCRtpTransceiverInit = {direction: 'recvonly'};
|
|
types.forEach(type => {
|
|
for(let i = 0; i < perType; ++i) {
|
|
description.createEntry(type).createTransceiver(connection, init);
|
|
}
|
|
}); */
|
|
|
|
return description;
|
|
}
|
|
|
|
public appendStreamToConference() {
|
|
super.appendStreamToConference();/* .then(() => {
|
|
currentGroupCall.connections.main.negotiating = false;
|
|
this.startNegotiation({
|
|
type: type,
|
|
isMuted: muted,
|
|
rejoin
|
|
});
|
|
}); */
|
|
}
|
|
|
|
private async invokeJoinGroupCall(localSdp: SDP, mainChannels: SDPMediaSection[], options: GroupCallConnectionInstance['options']) {
|
|
const {groupCall, description} = this;
|
|
const groupCallId = groupCall.id;
|
|
|
|
const processedChannels = mainChannels.map(section => {
|
|
const processed = processMediaSection(localSdp, section);
|
|
|
|
this.sources[processed.entry.type as 'video' | 'audio'] = processed.entry;
|
|
|
|
return processed;
|
|
});
|
|
|
|
let promise: Promise<Updates>;
|
|
const audioChannel = processedChannels.find(channel => channel.media.mediaType === 'audio');
|
|
const videoChannel = processedChannels.find(channel => channel.media.mediaType === 'video');
|
|
let {source, params} = audioChannel || {};
|
|
const useChannel = videoChannel || audioChannel;
|
|
|
|
const channels: {[type in WebRTCLineType]?: typeof audioChannel} = {
|
|
audio: audioChannel,
|
|
video: videoChannel
|
|
};
|
|
|
|
description.entries.forEach(entry => {
|
|
if(entry.direction === 'sendonly') {
|
|
const channel = channels[entry.type];
|
|
if(!channel) return;
|
|
|
|
description.setEntrySource(entry, channel.sourceGroups || channel.source);
|
|
description.setEntryPeerId(entry, rootScope.myId);
|
|
}
|
|
});
|
|
|
|
// overwrite ssrc with audio in video params
|
|
if(params !== useChannel.params) {
|
|
const data: JoinGroupCallJsonPayload = JSON.parse(useChannel.params.data);
|
|
// data.ssrc = source || data.ssrc - 1; // audio channel can be missed in screensharing
|
|
if(source) data.ssrc = source;
|
|
else delete data.ssrc;
|
|
params = {
|
|
_: 'dataJSON',
|
|
data: JSON.stringify(data)
|
|
};
|
|
}
|
|
|
|
const groupCallInput = appGroupCallsManager.getGroupCallInput(groupCallId);
|
|
if(options.type === 'main') {
|
|
const request: PhoneJoinGroupCall = {
|
|
call: groupCallInput,
|
|
join_as: {_: 'inputPeerSelf'},
|
|
params,
|
|
muted: options.isMuted,
|
|
video_stopped: !options.joinVideo
|
|
};
|
|
|
|
promise = apiManager.invokeApi('phone.joinGroupCall', request);
|
|
this.log(`[api] joinGroupCall id=${groupCallId}`, request);
|
|
} else {
|
|
const request: PhoneJoinGroupCallPresentation = {
|
|
call: groupCallInput,
|
|
params,
|
|
};
|
|
|
|
promise = apiManager.invokeApi('phone.joinGroupCallPresentation', request);
|
|
this.log(`[api] joinGroupCallPresentation id=${groupCallId}`, request);
|
|
}
|
|
|
|
const updates = await promise;
|
|
apiUpdatesManager.processUpdateMessage(updates);
|
|
const update = (updates as Updates.updates).updates.find(update => update._ === 'updateGroupCallConnection') as Update.updateGroupCallConnection;
|
|
|
|
const data: UpdateGroupCallConnectionData = JSON.parse(update.params.data);
|
|
|
|
data.audio = data.audio || groupCall.connections.main.description.audio;
|
|
description.setData(data);
|
|
filterServerCodecs(mainChannels, data);
|
|
|
|
return data;
|
|
}
|
|
|
|
protected async negotiateInternal() {
|
|
const {connection, description} = this;
|
|
const isNewConnection = connection.iceConnectionState === 'new' && !description.getEntryByMid('0').source;
|
|
const log = this.log.bindPrefix('startNegotiation');
|
|
log('start');
|
|
|
|
const originalOffer = await connection.createOffer({iceRestart: false});
|
|
|
|
if(isNewConnection && this.dataChannel) {
|
|
const dataChannelEntry = description.createEntry('application');
|
|
dataChannelEntry.setDirection('sendrecv');
|
|
}
|
|
|
|
const {sdp: localSdp, offer} = fixLocalOffer({
|
|
offer: originalOffer,
|
|
data: description
|
|
});
|
|
|
|
log('[sdp] setLocalDescription', offer.sdp);
|
|
await connection.setLocalDescription(offer);
|
|
|
|
const mainChannels = localSdp.media.filter(media => {
|
|
return media.mediaType !== 'application' && media.isSending;
|
|
});
|
|
|
|
if(isNewConnection) {
|
|
try {
|
|
await this.invokeJoinGroupCall(localSdp, mainChannels, this.options);
|
|
} catch(e) {
|
|
this.log.error('[tdweb] joinGroupCall error', e);
|
|
}
|
|
}
|
|
|
|
/* if(!data) {
|
|
log('abort 0');
|
|
this.closeConnectionAndStream(connection, streamManager);
|
|
return;
|
|
} */
|
|
|
|
/* if(connection.iceConnectionState !== 'new') {
|
|
log(`abort 1 connectionState=${connection.iceConnectionState}`);
|
|
this.closeConnectionAndStream(connection, streamManager);
|
|
return;
|
|
} */
|
|
/* if(this.currentGroupCall !== currentGroupCall || connectionHandler.connection !== connection) {
|
|
log('abort', this.currentGroupCall, currentGroupCall);
|
|
this.closeConnectionAndStream(connection, streamManager);
|
|
return;
|
|
} */
|
|
|
|
const isAnswer = true;
|
|
// const _bundleMids = bundleMids.slice();
|
|
const entriesToDelete: ConferenceEntry[] = [];
|
|
const bundle = localSdp.bundle;
|
|
forEachReverse(bundle, (mid, idx, arr) => {
|
|
const entry = description.getEntryByMid(mid);
|
|
if(entry.shouldBeSkipped(isAnswer)) {
|
|
arr.splice(idx, 1);
|
|
entriesToDelete.push(entry);
|
|
}
|
|
});
|
|
|
|
/* forEachReverse(description.entries, (entry, idx, arr) => {
|
|
const mediaSection = _parsedSdp.media.find(section => section.oa.get('mid').oa === entry.mid);
|
|
const deleted = !mediaSection;
|
|
// const deleted = !_bundleMids.includes(entry.mid); // ! can't use it because certain mid can be missed in bundle
|
|
if(deleted) {
|
|
arr.splice(idx, 1);
|
|
}
|
|
}); */
|
|
|
|
const entries = localSdp.media.map((section) => {
|
|
const mid = section.mid;
|
|
let entry = description.getEntryByMid(mid);
|
|
if(!entry) {
|
|
entry = new ConferenceEntry(mid, section.mediaType);
|
|
entry.setDirection('inactive');
|
|
}
|
|
|
|
return entry;
|
|
});
|
|
|
|
const answerDescription: RTCSessionDescriptionInit = {
|
|
type: 'answer',
|
|
sdp: description.generateSdp({
|
|
bundle,
|
|
entries,
|
|
isAnswer
|
|
})
|
|
};
|
|
|
|
entriesToDelete.forEach(entry => {
|
|
description.deleteEntry(entry);
|
|
});
|
|
|
|
log(`[sdp] setRemoteDescription signaling=${connection.signalingState} ice=${connection.iceConnectionState} gathering=${connection.iceGatheringState} connection=${connection.connectionState}`, answerDescription.sdp);
|
|
await connection.setRemoteDescription(answerDescription);
|
|
|
|
log('end');
|
|
}
|
|
|
|
public negotiate() {
|
|
let promise = this.negotiating;
|
|
if(promise) {
|
|
return promise;
|
|
}
|
|
|
|
promise = super.negotiate();
|
|
|
|
if(this.updateConstraints) {
|
|
promise.then(() => {
|
|
this.maybeUpdateRemoteVideoConstraints();
|
|
this.updateConstraints = false;
|
|
});
|
|
}
|
|
|
|
return promise;
|
|
}
|
|
|
|
public maybeUpdateRemoteVideoConstraints() {
|
|
if(this.dataChannel.readyState !== 'open') {
|
|
return;
|
|
}
|
|
|
|
this.log('maybeUpdateRemoteVideoConstraints');
|
|
|
|
// * https://github.com/TelegramMessenger/tgcalls/blob/6f2746e04c9b040f8c8dfc64d916a1853d09c4ce/tgcalls/group/GroupInstanceCustomImpl.cpp#L2549
|
|
type VideoConstraints = {minHeight?: number, maxHeight: number};
|
|
const obj: {
|
|
colibriClass: 'ReceiverVideoConstraints',
|
|
constraints: {[endpoint: string]: VideoConstraints},
|
|
defaultConstraints: VideoConstraints,
|
|
onStageEndpoints: string[]
|
|
} = {
|
|
colibriClass: 'ReceiverVideoConstraints',
|
|
constraints: {},
|
|
defaultConstraints: {maxHeight: 0},
|
|
onStageEndpoints: []
|
|
};
|
|
|
|
for(const entry of this.description.entries) {
|
|
if(entry.direction !== 'recvonly' || entry.type !== 'video') {
|
|
continue;
|
|
}
|
|
|
|
const {endpoint} = entry;
|
|
obj.onStageEndpoints.push(endpoint);
|
|
obj.constraints[endpoint] = {
|
|
minHeight: 180,
|
|
maxHeight: 720
|
|
};
|
|
}
|
|
|
|
this.sendDataChannelData(obj);
|
|
|
|
if(!obj.onStageEndpoints.length) {
|
|
if(this.updateConstraintsInterval) {
|
|
clearInterval(this.updateConstraintsInterval);
|
|
this.updateConstraintsInterval = undefined;
|
|
}
|
|
} else if(!this.updateConstraintsInterval) {
|
|
this.updateConstraintsInterval = window.setInterval(this.maybeUpdateRemoteVideoConstraints.bind(this), 5000);
|
|
}
|
|
}
|
|
|
|
public addInputVideoStream(stream: MediaStream) {
|
|
// const {sources} = this;
|
|
// if(sources?.video) {
|
|
// const source = this.sources.video.source;
|
|
// stream.source = '' + source;
|
|
this.groupCall.saveInputVideoStream(stream, this.type);
|
|
// }
|
|
|
|
this.streamManager.addStream(stream, 'input');
|
|
this.appendStreamToConference(); // replace sender track
|
|
}
|
|
}
|