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.

326 lines
9.1 KiB

3 years ago
/*
* 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 { logger } from '../logger';
import rootScope from '../rootScope';
import { GROUP_CALL_AMPLITUDE_ANALYSE_COUNT_MAX } from './constants';
import stopTrack from './helpers/stopTrack';
3 years ago
import LocalConferenceDescription from './localConferenceDescription';
import { getAmplitude, toTelegramSource } from './utils';
export type StreamItemBase = {
type: 'input' | 'output',
track: MediaStreamTrack,
source: string,
stream: MediaStream
};
export type StreamItem = StreamAudioItem | StreamVideoItem;
export type StreamAudioItem = StreamItemBase & {kind: 'audio', streamAnalyser: AudioStreamAnalyser};
export type StreamVideoItem = StreamItemBase & {kind: 'video'};
export type StreamAmplitude = {
type: "input" | "output";
source: string;
stream: MediaStream;
track: MediaStreamTrack;
value: number;
};
class AudioStreamAnalyser {
public analyser: AnalyserNode;
public gain: GainNode;
public streamSource: MediaStreamAudioSourceNode;
constructor(context: AudioContext, stream: MediaStream) {
const streamSource = this.streamSource = context.createMediaStreamSource(stream);
const analyser = this.analyser = context.createAnalyser();
const gain = this.gain = context.createGain();
// const streamDestination = context.createMediaStreamDestination();
analyser.minDecibels = -100;
analyser.maxDecibels = -30;
analyser.smoothingTimeConstant = 0.05;
analyser.fftSize = 1024;
// connect Web Audio API
streamSource.connect(analyser);
// analyser.connect(context.destination);
}
}
export default class StreamManager {
private context: AudioContext;
public outputStream: MediaStream;
public inputStream: MediaStream;
private timer: number;
private counter: number;
private items: StreamItem[];
private log: ReturnType<typeof logger>;
public direction: RTCRtpTransceiver['direction'];
public canCreateConferenceEntry: boolean;
public lol: boolean;
3 years ago
constructor(private interval?: number) {
this.context = new (window.AudioContext || (window as any).webkitAudioContext)();
this.items = [];
this.outputStream = new MediaStream();
this.inputStream = new MediaStream();
3 years ago
this.counter = 0;
this.log = logger('SM');
this.direction = 'sendonly';
this.canCreateConferenceEntry = true;
// this.lol = true;
3 years ago
}
public addStream(stream: MediaStream, type: StreamItem['type']) {
stream.getTracks().forEach(track => {
this.addTrack(stream, track, type);
});
}
public addTrack(stream: MediaStream, track: MediaStreamTrack, type: StreamItem['type']) {
this.log('addTrack', type, track, stream);
const {context, items, inputStream, outputStream} = this;
const kind: StreamItem['kind'] = track.kind as any;
const source = StreamManager.getSource(stream, type);
// this.removeTrack(track);
switch(type) {
case 'input': {
if(!inputStream) {
this.inputStream = stream;
} else {
inputStream.addTrack(track);
}
break;
}
case 'output': {
for(let i = 0; i < items.length; ++i) {
const {track: t, type, source: itemSource} = items[i];
if(itemSource === source && type === 'input') {
items.splice(i, 1);
outputStream.removeTrack(t);
break;
}
}
if(kind !== 'video') {
outputStream.addTrack(track);
}
break;
}
}
this.finalizeAddingTrack({
type,
source,
stream,
track,
kind,
streamAnalyser: kind === 'audio' ? new AudioStreamAnalyser(context, stream) : undefined
});
if(kind === 'audio' && this.interval) {
this.changeTimer();
}
}
private finalizeAddingTrack(item: StreamItem) {
const {track} = item;
track.addEventListener('ended', () => {
this.removeTrack(track);
}, {once: true});
this.items.push(item);
}
public hasInputTrackKind(kind: StreamItem['kind']) {
return this.items.find(item => item.type === 'input' && item.kind === kind);
}
public static getSource(stream: MediaStream, type: StreamItem['type']) {
return type === 'input' ? (stream.source || stream.id) : '' + toTelegramSource(+stream.id.substring(6));
}
public removeTrack(track: MediaStreamTrack) {
this.log('removeTrack', track);
const {items} = this;
let handled = false;
for(let i = 0, length = items.length; !handled && i < length; ++i) {
const {track: t, type} = items[i];
switch(type) {
case 'output': {
if(t === track) {
items.splice(i, 1);
this.outputStream.removeTrack(track);
handled = true;
}
break;
}
case 'input': {
if(t === track) {
items.splice(i, 1);
this.inputStream.removeTrack(track);
handled = true;
}
break;
}
}
}
if(track.kind === 'audio' && this.interval) {
this.changeTimer();
}
}
public replaceInputAudio(stream: MediaStream, oldTrack: MediaStreamTrack) {
this.removeTrack(oldTrack);
this.addStream(stream, 'input');
}
private changeTimer() {
if(this.timer !== undefined) {
clearInterval(this.timer);
}
if(this.items.length) {
this.timer = window.setInterval(this.analyse, this.interval);
}
}
public getAmplitude = (item: StreamAudioItem): StreamAmplitude => {
const {streamAnalyser, stream, track, source, type} = item;
const analyser = streamAnalyser.analyser;
if(!analyser) return;
const array = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(array);
const value = getAmplitude(array);
return {
type,
source,
stream,
track,
value
};
};
public analyse = () => {
const all = this.counter % 3 === 0;
const filteredItems = all ? this.items : this.items.filter(x => x.type === 'input');
const audioItems = filteredItems.filter(x => x.kind === 'audio') as StreamAudioItem[];
const amplitudes = audioItems.slice(0, GROUP_CALL_AMPLITUDE_ANALYSE_COUNT_MAX).map(this.getAmplitude);
if(++this.counter >= 1000) {
this.counter = 0;
}
rootScope.dispatchEvent('group_call_amplitude', {
amplitudes,
type: all ? 'all' : 'input'
});
};
/* public appendToConnection(connection: RTCPeerConnection) {
if(this.inputStream) {
this.inputStream.getTracks().forEach(track => {
connection.log('addTrack', track);
connection.addTrack(track, this.inputStream);
if(track.kind === 'video') {
track.enabled = true;
}
});
}
} */
public appendToConference(conference: LocalConferenceDescription) {
if(this.lol) {
return;
}
// return;
const {inputStream, direction, canCreateConferenceEntry} = this;
// const direction: RTCRtpTransceiverInit['direction'] = 'sendrecv';
// const direction: RTCRtpTransceiverInit['direction'] = 'sendonly';
const transceiverInit: RTCRtpTransceiverInit = {direction, streams: [inputStream]};
3 years ago
const types: ['audio' | 'video', RTCRtpTransceiverInit][] = [
['audio' as const, transceiverInit],
['video' as const, transceiverInit/* {sendEncodings: [{maxBitrate: 2500000}], ...transceiverInit} */]
];
const tracks = inputStream.getTracks();
// const transceivers = conference.connection.getTransceivers();
3 years ago
for(const [type, transceiverInit] of types) {
let entry = conference.findEntry(entry => entry.direction === direction && entry.type === type);
3 years ago
if(!entry) {
if(!canCreateConferenceEntry) {
continue;
}
3 years ago
entry = conference.createEntry(type);
}
/* const entry = conference.findFreeSendRecvEntry(type, true);
if(!entry.transceiver) {
entry.transceiver = transceivers.find(transceiver => transceiver.mid === entry.mid);
} */
let {transceiver} = entry;
if(!transceiver) {
transceiver = entry.createTransceiver(conference.connection, transceiverInit);
}
if(entry.direction !== transceiver.direction) {
transceiver.direction = entry.direction;
}
3 years ago
const track = tracks.find(track => track.kind === type);
const sender = transceiver.sender;
3 years ago
if(sender.track === track) {
continue;
}
// try { // ! don't use await here. it will wait for adding track and fake one won't be visible in startNegotiation.
/* await */sender.replaceTrack(track).catch(err => {
this.log.error(err);
});
// } catch(err) {
// }
}
}
public stop() {
try {
const tracks = this.inputStream.getTracks().concat(this.outputStream.getTracks());
tracks.forEach(track => {
stopTrack(track);
});
} catch(e) {
this.log.error(e);
3 years ago
}
}
}