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.
278 lines
8.7 KiB
278 lines
8.7 KiB
/* |
|
* 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 {IS_FIREFOX} from '../../environment/userAgent'; |
|
import LocalConferenceDescription, {ConferenceEntry} from './localConferenceDescription'; |
|
import StringFromLineBuilder from './stringFromLineBuilder'; |
|
import {GroupCallConnectionTransport, PayloadType, UpdateGroupCallConnectionData} from './types'; |
|
import {fromTelegramSource} from './utils'; |
|
|
|
// screencast is for Peer-to-Peer only |
|
export type WebRTCLineTypeTrue = 'video' | 'audio' | 'application'; |
|
export type WebRTCLineType = WebRTCLineTypeTrue | 'screencast'; |
|
|
|
export const WEBRTC_MEDIA_PORT = '9'; |
|
|
|
export function fixMediaLineType(mediaType: WebRTCLineType) { |
|
return mediaType === 'screencast' ? 'video' : mediaType; |
|
} |
|
|
|
export function performCandidate(c: GroupCallConnectionTransport['candidates'][0]) { |
|
const arr: string[] = []; |
|
arr.push('a=candidate:'); |
|
arr.push(`${c.foundation} ${c.component} ${c.protocol.toUpperCase()} ${c.priority} ${c.ip} ${c.port} typ ${c.type}`); |
|
if(c['rel-addr'] !== undefined) { |
|
arr.push(` raddr ${c['rel-addr']} rport ${c['rel-port']}`); |
|
} |
|
arr.push(` generation ${c.generation}`); |
|
return arr.join(''); |
|
} |
|
|
|
export function getConnectionTypeForMediaType(mediaType: WebRTCLineType) { |
|
// return mediaType === 'application' ? 'DTLS/SCTP' : 'RTP/SAVPF'; |
|
return mediaType === 'application' ? 'DTLS/SCTP' : 'UDP/TLS/RTP/SAVPF'; |
|
} |
|
|
|
export function generateMediaFirstLine(mediaType: WebRTCLineType, port = WEBRTC_MEDIA_PORT, payloadIds: (string | number)[]) { |
|
const connectionType = getConnectionTypeForMediaType(mediaType); |
|
return `m=${fixMediaLineType(mediaType)} ${port} ${connectionType} ${payloadIds.join(' ')}`; |
|
} |
|
|
|
type ConferenceData = UpdateGroupCallConnectionData | LocalConferenceDescription; |
|
|
|
// https://tools.ietf.org/id/draft-ietf-rtcweb-sdp-08.html |
|
// https://datatracker.ietf.org/doc/html/draft-roach-mmusic-unified-plan-00 |
|
export class SDPBuilder extends StringFromLineBuilder { |
|
public addCandidate(c: GroupCallConnectionTransport['candidates'][0]) { |
|
return this.add(performCandidate(c)); |
|
} |
|
|
|
/* public addDataChannel(mid: string, transport: GroupCallConnectionTransport, isAnswer?: boolean) { |
|
this.add( |
|
'm=application 9 UDP/DTLS/SCTP webrtc-datachannel', |
|
'c=IN IP4 0.0.0.0', |
|
'a=ice-options:trickle', |
|
`a=mid:${mid}` |
|
); |
|
|
|
// if(!isAnswer) { |
|
this.add('a=sendrecv'); |
|
// } |
|
|
|
this.addTransport(transport, isAnswer); |
|
|
|
return this.add( |
|
'a=sctp-port:5000', |
|
'a=max-message-size:262144' |
|
); |
|
} */ |
|
|
|
public addHeader(sId: string, bundleMids: string[]) { |
|
const bundle = bundleMids.join(' '); |
|
return this.add( |
|
'v=0', // version |
|
`o=- ${sId} 2 IN IP4 0.0.0.0`, // sessionId, 2=sessionVersion |
|
's=-', // name of the session |
|
't=0 0', // time when session is valid |
|
'a=extmap-allow-mixed', |
|
`a=group:BUNDLE ${bundle}`, |
|
'a=ice-options:trickle', |
|
// 'a=ice-lite', // ice-lite: is a minimal version of the ICE specification, intended for servers running on a public IP address. |
|
'a=msid-semantic:WMS *' |
|
); |
|
} |
|
|
|
public addTransport(transport: GroupCallConnectionTransport, skipCandidates?: boolean) { |
|
this.add( |
|
`a=ice-ufrag:${transport.ufrag}`, |
|
`a=ice-pwd:${transport.pwd}`, |
|
'a=ice-options:trickle' // ! test |
|
); |
|
|
|
for(const fingerprint of transport.fingerprints) { |
|
this.add( |
|
`a=fingerprint:${fingerprint.hash} ${fingerprint.fingerprint}`, |
|
`a=setup:${fingerprint.setup}` |
|
); |
|
} |
|
|
|
if(!skipCandidates && transport.candidates) { |
|
for(const candidate of transport.candidates) { |
|
this.addCandidate(candidate); |
|
} |
|
} |
|
|
|
return this; |
|
} |
|
|
|
public addSsrc(entry: ConferenceEntry) { |
|
let streamName = 'stream'; |
|
let {type, sourceGroups} = entry; |
|
|
|
// let source = ssrc.source ?? ssrc.sourceGroups[0].sources[0]; |
|
// source = fromTelegramSource(source); |
|
const source = fromTelegramSource(entry.source); |
|
|
|
streamName += source; |
|
type += source as any; |
|
|
|
// streamName += mid; |
|
// type += mid as any; |
|
|
|
// streamName = type = entry.transceiver.receiver.track.id as any; |
|
|
|
const addMsid = () => { |
|
this.add(`a=msid:${streamName} ${type}`); |
|
}; |
|
|
|
const addSource = (ssrc: number) => { |
|
this.add( |
|
`a=ssrc:${ssrc} cname:${streamName}`, |
|
`a=ssrc:${ssrc} msid:${streamName} ${type}`, |
|
`a=ssrc:${ssrc} mslabel:${streamName}`, |
|
`a=ssrc:${ssrc} label:${type}` |
|
); |
|
}; |
|
|
|
addMsid(); |
|
if(sourceGroups?.length) { |
|
sourceGroups.forEach((ssrcGroup) => { |
|
if(ssrcGroup.sources.length) { |
|
const sources = ssrcGroup.sources.map(fromTelegramSource); |
|
this.add(`a=ssrc-group:${ssrcGroup.semantics} ${sources.join(' ')}`); |
|
sources.forEach(addSource); |
|
} |
|
}); |
|
} else { |
|
addSource(source); |
|
} |
|
|
|
return this; |
|
} |
|
|
|
public addSsrcEntry(entry: ConferenceEntry, data: ConferenceData, isAnswer?: boolean) { |
|
const add = (...x: string[]) => this.add(...x); |
|
|
|
const {type, mid, direction, port} = entry; |
|
const transport = data.transport; |
|
|
|
/* if(type === 'application') { |
|
return this.addDataChannel(mid, transport, isAnswer); |
|
} */ |
|
|
|
const isApplication = type === 'application'; |
|
const codec = isApplication ? undefined : data[type]; |
|
|
|
const isInactive = direction === 'inactive'; |
|
if(entry.shouldBeSkipped(isAnswer)) { |
|
return add( |
|
`m=${fixMediaLineType(type)} 0 ${getConnectionTypeForMediaType(type)} 0`, |
|
`c=IN IP4 0.0.0.0`, |
|
`a=inactive`, |
|
`a=mid:${mid}` |
|
); |
|
} |
|
|
|
const payloadTypes = !isApplication ? codec['payload-types'] : [{id: 5000} as PayloadType]; |
|
const ids = payloadTypes.map((type) => type.id); |
|
add( |
|
generateMediaFirstLine(type, port, ids), |
|
'c=IN IP4 0.0.0.0', |
|
`a=rtcp:${port} IN IP4 0.0.0.0` |
|
); |
|
|
|
if(transport['rtcp-mux']) { |
|
add('a=rtcp-mux'); |
|
} |
|
|
|
add(`a=mid:${mid}`); |
|
/* if(type === 'video') { |
|
add('b=AS:2500'); |
|
} */ |
|
|
|
let setDirection = direction; |
|
if(direction !== 'sendrecv' && isAnswer && !(isInactive || isApplication)) { |
|
setDirection = direction === 'sendonly' ? 'recvonly' : 'sendonly'; |
|
} |
|
|
|
// a=bundle-only |
|
add(`a=${setDirection}`); |
|
|
|
// this.addTransport(transport, isAnswer); |
|
this.addTransport(transport); |
|
|
|
if(!isApplication) { |
|
const hdrexts = codec['rtp-hdrexts']; |
|
if(hdrexts?.length) { |
|
hdrexts.forEach((hdrext) => { |
|
add(`a=extmap:${hdrext.id} ${hdrext.uri}`); |
|
}); |
|
} |
|
|
|
payloadTypes.forEach((type) => { |
|
add(`a=rtpmap:${type.id} ${type.name}/${type.clockrate}${type.channels && type.channels > 1 ? `/${type.channels}` : ''}`); |
|
|
|
const parameters = type.parameters; |
|
if(Array.isArray(parameters)) { |
|
if(parameters.length) { |
|
console.error('parameters is array???', parameters); |
|
} |
|
} else if(parameters && Object.keys(parameters).length) { |
|
const p: string[] = []; |
|
for(const i in parameters) { |
|
p.push(`${i}=${parameters[i]}`); |
|
} |
|
add(`a=fmtp:${type.id} ${p.join(';')}`); |
|
} |
|
|
|
const fbs = type['rtcp-fbs']; |
|
if(fbs?.length) { |
|
fbs.forEach((fb) => { |
|
add(`a=rtcp-fb:${type.id} ${fb.type}${fb.subtype ? ' ' + fb.subtype : ''}`); |
|
}); |
|
} |
|
}); |
|
} else { |
|
add(`a=sctpmap:${payloadTypes[0].id} webrtc-datachannel 256`); |
|
} |
|
|
|
if(entry.source && (setDirection === 'sendonly' || setDirection === 'sendrecv')) { |
|
this.addSsrc(entry); |
|
} |
|
|
|
return this; |
|
} |
|
|
|
public addConference(options: { |
|
conference: LocalConferenceDescription, |
|
bundle: string[], |
|
entries: ConferenceEntry[], |
|
isAnswer?: boolean, |
|
}) { |
|
const {conference, entries, bundle, isAnswer} = options; |
|
this.addHeader(conference.sessionId, bundle); |
|
|
|
if(IS_FIREFOX) { |
|
this.addTransport(conference.transport); // support Firefox |
|
} |
|
|
|
for(const entry of entries) { |
|
// this.addSsrcEntry(entry, conference, isAnswer); |
|
this.addSsrcEntry((isAnswer ? entry.recvEntry || entry.sendEntry : entry.sendEntry || entry.recvEntry) || entry, conference, isAnswer); |
|
} |
|
|
|
return this; |
|
} |
|
|
|
public static fromConference(options: Parameters<SDPBuilder['addConference']>[0]) { |
|
return new SDPBuilder().addConference(options).finalize(); |
|
} |
|
}
|
|
|