From 558ec9453741300ea4bafc6d9b3c50bdbb50ff6f Mon Sep 17 00:00:00 2001 From: Murat Dogan Date: Sat, 24 May 2025 18:12:21 +0200 Subject: [PATCH] Types --- src/polyfill/RTCDataChannel.ts | 395 ++++++++------- src/polyfill/RTCDtlsTransport.ts | 75 ++- src/polyfill/RTCIceCandidate.ts | 18 +- src/polyfill/RTCIceTransport.ts | 125 +++-- src/polyfill/RTCPeerConnection.ts | 807 +++++++++++++++--------------- src/polyfill/RTCSctpTransport.ts | 93 ++-- 6 files changed, 752 insertions(+), 761 deletions(-) diff --git a/src/polyfill/RTCDataChannel.ts b/src/polyfill/RTCDataChannel.ts index 517b8e7a..cfbaf4e7 100644 --- a/src/polyfill/RTCDataChannel.ts +++ b/src/polyfill/RTCDataChannel.ts @@ -3,204 +3,203 @@ import * as exceptions from './Exception'; import { DataChannel } from '../lib/index'; export default class RTCDataChannel extends EventTarget implements globalThis.RTCDataChannel { - #dataChannel: DataChannel; - #readyState: globalThis.RTCDataChannelState; - #bufferedAmountLowThreshold: number; - #binaryType: BinaryType; - #maxPacketLifeTime: number | null; - #maxRetransmits: number | null; - #negotiated: boolean; - #ordered: boolean; - #id: number - #label: string - #protocol: string - - #closeRequested = false; - - // events - onbufferedamountlow: ((this: globalThis.RTCDataChannel, ev: Event) => any) | null; - onclose: ((this: globalThis.RTCDataChannel, ev: Event) => any) | null; - onclosing: ((this: globalThis.RTCDataChannel, ev: Event) => any) | null; - onerror: ((this: globalThis.RTCDataChannel, ev: Event) => any) | null; - onmessage: ((this: globalThis.RTCDataChannel, ev: MessageEvent) => any) | null; - onopen: ((this: globalThis.RTCDataChannel, ev: Event) => any) | null; - - constructor(dataChannel: DataChannel, opts: globalThis.RTCDataChannelInit = {}) { - super(); - - this.#dataChannel = dataChannel; - this.#binaryType = 'blob'; - this.#readyState = this.#dataChannel.isOpen() ? 'open' : 'connecting'; - this.#bufferedAmountLowThreshold = 0; - this.#maxPacketLifeTime = opts.maxPacketLifeTime || null; - this.#maxRetransmits = opts.maxRetransmits || null; - this.#negotiated = opts.negotiated || false; - this.#ordered = opts.ordered || true; - this.#id = this.#dataChannel.getId(); - this.#label = this.#dataChannel.getLabel(); - this.#protocol = this.#dataChannel.getProtocol(); - - // forward dataChannel events - this.#dataChannel.onOpen(() => { - this.#readyState = 'open'; - this.dispatchEvent(new Event('open', {})); - }); - - this.#dataChannel.onClosed(() => { - // Simulate closing event - if (!this.#closeRequested) { - this.#readyState = 'closing'; - this.dispatchEvent(new Event('closing')); - } - - setImmediate(() => { - this.#readyState = 'closed'; - this.dispatchEvent(new Event('close')); - }); - }); - - this.#dataChannel.onError((msg) => { - this.dispatchEvent( - new globalThis.RTCErrorEvent('error', { - error: new RTCError( - { - errorDetail: 'data-channel-failure', - }, - msg, - ), - }), - ); - }); - - this.#dataChannel.onBufferedAmountLow(() => { - this.dispatchEvent(new Event('bufferedamountlow')); - }); - - this.#dataChannel.onMessage((data) => { - if (ArrayBuffer.isView(data)) { - if (this.binaryType == 'arraybuffer') - data = data.buffer; - else - data = Buffer.from(data.buffer); - } - - this.dispatchEvent(new MessageEvent('message', { data })); - }); - - // forward events to properties - this.addEventListener('message', (e) => { - if (this.onmessage) this.onmessage(e as MessageEvent); - }); - this.addEventListener('bufferedamountlow', (e) => { - if (this.onbufferedamountlow) this.onbufferedamountlow(e); - }); - this.addEventListener('error', (e) => { - if (this.onerror) this.onerror(e); - }); - this.addEventListener('close', (e) => { - if (this.onclose) this.onclose(e); - }); - this.addEventListener('closing', (e) => { - if (this.onclosing) this.onclosing(e); - }); - this.addEventListener('open', (e) => { - if (this.onopen) this.onopen(e); - }); - } - - set binaryType(type) { - if (type !== 'blob' && type !== 'arraybuffer') { - throw new DOMException( - "Failed to set the 'binaryType' property on 'RTCDataChannel': Unknown binary type : " + type, - 'TypeMismatchError', - ); - } - this.#binaryType = type; - } - - get binaryType(): BinaryType { - return this.#binaryType; - } - - get bufferedAmount(): number { - return this.#dataChannel.bufferedAmount(); - } - - get bufferedAmountLowThreshold(): number { - return this.#bufferedAmountLowThreshold; - } - - set bufferedAmountLowThreshold(value) { - const number = Number(value) || 0; - this.#bufferedAmountLowThreshold = number; - this.#dataChannel.setBufferedAmountLowThreshold(number); - } - - get id(): number | null { - return this.#id; - } - - get label(): string { - return this.#label; - } - - get maxPacketLifeTime(): number | null { - return this.#maxPacketLifeTime; - } - - get maxRetransmits(): number | null { - return this.#maxRetransmits; - } - - get negotiated(): boolean { - return this.#negotiated; - } - - get ordered(): boolean { - return this.#ordered; - } - - get protocol(): string { - return this.#protocol - } - - get readyState(): globalThis.RTCDataChannelState { - return this.#readyState; - } - - send(data): void { - if (this.#readyState !== 'open') { - throw new exceptions.InvalidStateError( - "Failed to execute 'send' on 'RTCDataChannel': RTCDataChannel.readyState is not 'open'", - ); - } - - // Needs network error, type error implemented - if (typeof data === 'string') { - this.#dataChannel.sendMessage(data); - } else if (data instanceof Blob) { - data.arrayBuffer().then((ab) => { - if (process?.versions?.bun) { - this.#dataChannel.sendMessageBinary(Buffer.from(ab)); - } else { - this.#dataChannel.sendMessageBinary(new Uint8Array(ab)); - } - }); - } else if (data instanceof Uint8Array) { - this.#dataChannel.sendMessageBinary(data); + #dataChannel: DataChannel; + #readyState: globalThis.RTCDataChannelState; + #bufferedAmountLowThreshold: number; + #binaryType: BinaryType; + #maxPacketLifeTime: number | null; + #maxRetransmits: number | null; + #negotiated: boolean; + #ordered: boolean; + #id: number; + #label: string; + #protocol: string; + + #closeRequested = false; + + // events + onbufferedamountlow: globalThis.RTCDataChannel['onbufferedamountlow'] = null; + onclose: globalThis.RTCDataChannel['onclose'] = null; + onclosing: globalThis.RTCDataChannel['onclosing'] = null; + onerror: globalThis.RTCDataChannel['onerror'] = null; + onmessage: globalThis.RTCDataChannel['onmessage'] = null; + onopen: globalThis.RTCDataChannel['onopen'] = null; + + constructor(dataChannel: DataChannel, opts: globalThis.RTCDataChannelInit = {}) { + super(); + + this.#dataChannel = dataChannel; + this.#binaryType = 'blob'; + this.#readyState = this.#dataChannel.isOpen() ? 'open' : 'connecting'; + this.#bufferedAmountLowThreshold = 0; + this.#maxPacketLifeTime = opts.maxPacketLifeTime ?? null; + this.#maxRetransmits = opts.maxRetransmits ?? null; + this.#negotiated = opts.negotiated ?? false; + this.#ordered = opts.ordered ?? true; + this.#id = this.#dataChannel.getId(); + this.#label = this.#dataChannel.getLabel(); + this.#protocol = this.#dataChannel.getProtocol(); + + // forward dataChannel events + this.#dataChannel.onOpen(() => { + this.#readyState = 'open'; + this.dispatchEvent(new Event('open', {})); + }); + + this.#dataChannel.onClosed(() => { + // Simulate closing event + if (!this.#closeRequested) { + this.#readyState = 'closing'; + this.dispatchEvent(new Event('closing')); + } + + setImmediate(() => { + this.#readyState = 'closed'; + this.dispatchEvent(new Event('close')); + }); + }); + + this.#dataChannel.onError((msg) => { + this.dispatchEvent( + new globalThis.RTCErrorEvent('error', { + error: new RTCError( + { + errorDetail: 'data-channel-failure', + }, + msg, + ), + }), + ); + }); + + this.#dataChannel.onBufferedAmountLow(() => { + this.dispatchEvent(new Event('bufferedamountlow')); + }); + + this.#dataChannel.onMessage((data) => { + if (ArrayBuffer.isView(data)) { + if (this.binaryType == 'arraybuffer') data = data.buffer; + else data = Buffer.from(data.buffer); + } + + this.dispatchEvent(new MessageEvent('message', { data })); + }); + + // forward events to properties + this.addEventListener('message', (e) => { + if (this.onmessage) this.onmessage(e as MessageEvent); + }); + this.addEventListener('bufferedamountlow', (e) => { + if (this.onbufferedamountlow) this.onbufferedamountlow(e); + }); + this.addEventListener('error', (e) => { + if (this.onerror) this.onerror(e); + }); + this.addEventListener('close', (e) => { + if (this.onclose) this.onclose(e); + }); + this.addEventListener('closing', (e) => { + if (this.onclosing) this.onclosing(e); + }); + this.addEventListener('open', (e) => { + if (this.onopen) this.onopen(e); + }); + } + + set binaryType(type) { + if (type !== 'blob' && type !== 'arraybuffer') { + throw new DOMException( + "Failed to set the 'binaryType' property on 'RTCDataChannel': Unknown binary type : " + + type, + 'TypeMismatchError', + ); + } + this.#binaryType = type; + } + + get binaryType(): BinaryType { + return this.#binaryType; + } + + get bufferedAmount(): number { + return this.#dataChannel.bufferedAmount(); + } + + get bufferedAmountLowThreshold(): number { + return this.#bufferedAmountLowThreshold; + } + + set bufferedAmountLowThreshold(value) { + const number = Number(value) || 0; + this.#bufferedAmountLowThreshold = number; + this.#dataChannel.setBufferedAmountLowThreshold(number); + } + + get id(): number | null { + return this.#id; + } + + get label(): string { + return this.#label; + } + + get maxPacketLifeTime(): number | null { + return this.#maxPacketLifeTime; + } + + get maxRetransmits(): number | null { + return this.#maxRetransmits; + } + + get negotiated(): boolean { + return this.#negotiated; + } + + get ordered(): boolean { + return this.#ordered; + } + + get protocol(): string { + return this.#protocol; + } + + get readyState(): globalThis.RTCDataChannelState { + return this.#readyState; + } + + send(data): void { + if (this.#readyState !== 'open') { + throw new exceptions.InvalidStateError( + "Failed to execute 'send' on 'RTCDataChannel': RTCDataChannel.readyState is not 'open'", + ); + } + + // Needs network error, type error implemented + if (typeof data === 'string') { + this.#dataChannel.sendMessage(data); + } else if (data instanceof Blob) { + data.arrayBuffer().then((ab) => { + if (process?.versions?.bun) { + this.#dataChannel.sendMessageBinary(Buffer.from(ab)); } else { - if (process?.versions?.bun) { - this.#dataChannel.sendMessageBinary(Buffer.from(data)); - } else { - this.#dataChannel.sendMessageBinary(new Uint8Array(data)); - } + this.#dataChannel.sendMessageBinary(new Uint8Array(ab)); } - } - - close(): void { - this.#closeRequested = true; - setImmediate(() => { - this.#dataChannel.close(); - }); - } + }); + } else if (data instanceof Uint8Array) { + this.#dataChannel.sendMessageBinary(data); + } else { + if (process?.versions?.bun) { + this.#dataChannel.sendMessageBinary(Buffer.from(data)); + } else { + this.#dataChannel.sendMessageBinary(new Uint8Array(data)); + } + } + } + + close(): void { + this.#closeRequested = true; + setImmediate(() => { + this.#dataChannel.close(); + }); + } } diff --git a/src/polyfill/RTCDtlsTransport.ts b/src/polyfill/RTCDtlsTransport.ts index 8dd4d9ed..3b5c77c1 100644 --- a/src/polyfill/RTCDtlsTransport.ts +++ b/src/polyfill/RTCDtlsTransport.ts @@ -2,45 +2,42 @@ import RTCIceTransport from './RTCIceTransport'; export default class RTCDtlsTransport extends EventTarget implements globalThis.RTCDtlsTransport { - #pc: globalThis.RTCPeerConnection = null; - #iceTransport = null; - - onstatechange: ((this: globalThis.RTCDtlsTransport, ev: Event) => any) | null = null; - onerror: ((this: globalThis.RTCDtlsTransport, ev: Event) => any) | null = null; - - constructor(init: { pc: globalThis.RTCPeerConnection, extraFunctions }) { - super(); - this.#pc = init.pc; - - this.#iceTransport = new RTCIceTransport({ pc: init.pc, extraFunctions: init.extraFunctions }); - - // forward peerConnection events - this.#pc.addEventListener('connectionstatechange', () => { - this.dispatchEvent(new Event('statechange')); - }); - - // forward events to properties - this.addEventListener('statechange', (e) => { - if (this.onstatechange) this.onstatechange(e); - }); + #pc: globalThis.RTCPeerConnection = null; + #iceTransport = null; + + onstatechange: globalThis.RTCDtlsTransport['onstatechange'] = null; + onerror: globalThis.RTCDtlsTransport['onstatechange'] = null; + + constructor(init: { pc: globalThis.RTCPeerConnection; extraFunctions }) { + super(); + this.#pc = init.pc; + + this.#iceTransport = new RTCIceTransport({ pc: init.pc, extraFunctions: init.extraFunctions }); + + // forward peerConnection events + this.#pc.addEventListener('connectionstatechange', () => { + const e = new Event('statechange'); + this.dispatchEvent(e); + this.onstatechange?.(e); + }); + } + + get iceTransport(): globalThis.RTCIceTransport { + return this.#iceTransport; + } + + get state(): globalThis.RTCDtlsTransportState { + // reduce state from new, connecting, connected, disconnected, failed, closed, unknown + // to RTCDtlsTRansport states new, connecting, connected, closed, failed + let state = this.#pc ? this.#pc.connectionState : 'new'; + if (state === 'disconnected') { + state = 'closed'; } + return state; + } - get iceTransport(): globalThis.RTCIceTransport { - return this.#iceTransport; - } - - get state(): globalThis.RTCDtlsTransportState { - // reduce state from new, connecting, connected, disconnected, failed, closed, unknown - // to RTCDtlsTRansport states new, connecting, connected, closed, failed - let state = this.#pc ? this.#pc.connectionState : 'new'; - if (state === 'disconnected') { - state = 'closed'; - } - return state; - } - - getRemoteCertificates(): ArrayBuffer[] { - // TODO: implement - return [new ArrayBuffer(0)]; - } + getRemoteCertificates(): ArrayBuffer[] { + // TODO: implement + return [new ArrayBuffer(0)]; + } } diff --git a/src/polyfill/RTCIceCandidate.ts b/src/polyfill/RTCIceCandidate.ts index 3f94cad2..149d8a5f 100644 --- a/src/polyfill/RTCIceCandidate.ts +++ b/src/polyfill/RTCIceCandidate.ts @@ -23,14 +23,14 @@ export default class RTCIceCandidate implements globalThis.RTCIceCandidate { if (sdpMLineIndex == null && sdpMid == null) throw new TypeError('At least one of sdpMLineIndex or sdpMid must be specified'); - this.#candidate = candidate === null ? 'null' : candidate ?? ''; + this.#candidate = candidate === null ? 'null' : (candidate ?? ''); this.#sdpMLineIndex = sdpMLineIndex ?? null; this.#sdpMid = sdpMid ?? null; this.#usernameFragment = usernameFragment ?? null; if (candidate) { const fields = candidate.split(' '); - this.#foundation = fields[0].replace('candidate:', ''); // remove text candidate: + this.#foundation = fields[0]!.replace('candidate:', ''); // remove text candidate: this.#component = fields[1] == '1' ? 'rtp' : 'rtcp'; this.#protocol = fields[2] as globalThis.RTCIceProtocol; this.#priority = parseInt(fields[3], 10); @@ -58,7 +58,7 @@ export default class RTCIceCandidate implements globalThis.RTCIceCandidate { } get address(): string | null { - return this.#address || null; + return this.#address ?? null; } get candidate(): string { @@ -70,19 +70,19 @@ export default class RTCIceCandidate implements globalThis.RTCIceCandidate { } get foundation(): string | null { - return this.#foundation || null; + return this.#foundation ?? null; } get port(): number | null { - return this.#port || null; + return this.#port ?? null; } get priority(): number | null { - return this.#priority || null; + return this.#priority ?? null; } get protocol(): globalThis.RTCIceProtocol | null { - return this.#protocol || null; + return this.#protocol ?? null; } get relatedAddress(): string | null { @@ -90,7 +90,7 @@ export default class RTCIceCandidate implements globalThis.RTCIceCandidate { } get relatedPort(): number | null { - return this.#relatedPort || null; + return this.#relatedPort ?? null; } get sdpMLineIndex(): number | null { @@ -106,7 +106,7 @@ export default class RTCIceCandidate implements globalThis.RTCIceCandidate { } get type(): globalThis.RTCIceCandidateType | null { - return this.#type || null; + return this.#type ?? null; } get usernameFragment(): string | null { diff --git a/src/polyfill/RTCIceTransport.ts b/src/polyfill/RTCIceTransport.ts index cd0db0f7..8fb0ccad 100644 --- a/src/polyfill/RTCIceTransport.ts +++ b/src/polyfill/RTCIceTransport.ts @@ -2,81 +2,76 @@ import RTCIceCandidate from './RTCIceCandidate'; export default class RTCIceTransport extends EventTarget implements globalThis.RTCIceTransport { - #pc: globalThis.RTCPeerConnection = null; - #extraFunctions = null; + #pc: globalThis.RTCPeerConnection = null; + #extraFunctions = null; - ongatheringstatechange: ((this: globalThis.RTCIceTransport, ev: Event) => any) | null = null; - onselectedcandidatepairchange: ((this: globalThis.RTCIceTransport, ev: Event) => any) | null = null; - onstatechange: ((this: globalThis.RTCIceTransport, ev: Event) => any) | null = null; + ongatheringstatechange: globalThis.RTCIceTransport['ongatheringstatechange'] = null; + onselectedcandidatepairchange: globalThis.RTCIceTransport['onselectedcandidatepairchange'] = null; + onstatechange: globalThis.RTCIceTransport['onstatechange'] = null; - constructor(init: { pc: globalThis.RTCPeerConnection, extraFunctions }) { - super(); - this.#pc = init.pc; - this.#extraFunctions = init.extraFunctions; + constructor(init: { pc: globalThis.RTCPeerConnection; extraFunctions }) { + super(); + this.#pc = init.pc; + this.#extraFunctions = init.extraFunctions; - // forward peerConnection events - this.#pc.addEventListener('icegatheringstatechange', () => { - this.dispatchEvent(new Event('gatheringstatechange')); - }); - this.#pc.addEventListener('iceconnectionstatechange', () => { - this.dispatchEvent(new Event('statechange')); - }); + this.#pc.addEventListener('icegatheringstatechange', () => { + const e = new Event('gatheringstatechange'); + this.dispatchEvent(e); + this.ongatheringstatechange?.(e); + }); + this.#pc.addEventListener('iceconnectionstatechange', () => { + const e = new Event('statechange'); + this.dispatchEvent(e); + this.onstatechange?.(e); + }); + } - // forward events to properties - this.addEventListener('gatheringstatechange', (e) => { - if (this.ongatheringstatechange) this.ongatheringstatechange(e); - }); - this.addEventListener('statechange', (e) => { - if (this.onstatechange) this.onstatechange(e); - }); - } + get component(): globalThis.RTCIceComponent { + const cp = this.getSelectedCandidatePair(); + if (!cp) return null; + return cp.local.component; + } - get component(): globalThis.RTCIceComponent { - const cp = this.getSelectedCandidatePair(); - if (!cp) return null; - return cp.local.component; - } + get gatheringState(): globalThis.RTCIceGatheringState { + return this.#pc ? this.#pc.iceGatheringState : 'new'; + } - get gatheringState(): globalThis.RTCIceGatheringState { - return this.#pc ? this.#pc.iceGatheringState : 'new'; - } + get role(): string { + return this.#pc.localDescription.type == 'offer' ? 'controlling' : 'controlled'; + } - get role(): string { - return this.#pc.localDescription.type == 'offer' ? 'controlling' : 'controlled'; - } + get state(): globalThis.RTCIceTransportState { + return this.#pc ? this.#pc.iceConnectionState : 'new'; + } - get state(): globalThis.RTCIceTransportState { - return this.#pc ? this.#pc.iceConnectionState : 'new'; - } + getLocalCandidates(): globalThis.RTCIceCandidate[] { + return this.#pc ? this.#extraFunctions.localCandidates() : []; + } - getLocalCandidates(): globalThis.RTCIceCandidate[] { - return this.#pc ? this.#extraFunctions.localCandidates() : []; - } + getLocalParameters(): any { + /** */ + } - getLocalParameters(): any { - /** */ - } + getRemoteCandidates(): globalThis.RTCIceCandidate[] { + return this.#pc ? this.#extraFunctions.remoteCandidates() : []; + } - getRemoteCandidates(): globalThis.RTCIceCandidate[] { - return this.#pc ? this.#extraFunctions.remoteCandidates() : []; - } + getRemoteParameters(): any { + /** */ + } - getRemoteParameters(): any { - /** */ - } - - getSelectedCandidatePair(): globalThis.RTCIceCandidatePair | null { - const cp = this.#extraFunctions.selectedCandidatePair(); - if (!cp) return null; - return { - local: new RTCIceCandidate({ - candidate: cp.local.candidate, - sdpMid: cp.local.mid, - }), - remote: new RTCIceCandidate({ - candidate: cp.remote.candidate, - sdpMid: cp.remote.mid, - }), - }; - } + getSelectedCandidatePair(): globalThis.RTCIceCandidatePair | null { + const cp = this.#extraFunctions.selectedCandidatePair(); + if (!cp) return null; + return { + local: new RTCIceCandidate({ + candidate: cp.local.candidate, + sdpMid: cp.local.mid, + }), + remote: new RTCIceCandidate({ + candidate: cp.remote.candidate, + sdpMid: cp.remote.mid, + }), + }; + } } diff --git a/src/polyfill/RTCPeerConnection.ts b/src/polyfill/RTCPeerConnection.ts index db89632a..27eeec38 100644 --- a/src/polyfill/RTCPeerConnection.ts +++ b/src/polyfill/RTCPeerConnection.ts @@ -16,467 +16,468 @@ interface RTCConfiguration extends globalThis.RTCConfiguration { } export default class RTCPeerConnection extends EventTarget implements globalThis.RTCPeerConnection { - static async generateCertificate(): Promise { - throw new DOMException('Not implemented'); - } + static async generateCertificate(): Promise { + throw new DOMException('Not implemented'); + } + + #peerConnection: PeerConnection; + #localOffer: any; + #localAnswer: any; + #dataChannels: Set; + #dataChannelsClosed = 0; + #config: globalThis.RTCConfiguration; + #canTrickleIceCandidates: boolean | null; + #sctp: globalThis.RTCSctpTransport; + + #localCandidates: globalThis.RTCIceCandidate[] = []; + #remoteCandidates: globalThis.RTCIceCandidate[] = []; + + // events + onconnectionstatechange: globalThis.RTCPeerConnection['onconnectionstatechange'] = null; + ondatachannel: globalThis.RTCPeerConnection['ondatachannel'] = null; + onicecandidate: globalThis.RTCPeerConnection['onicecandidate'] = null; + onicecandidateerror: globalThis.RTCPeerConnection['onicecandidateerror'] = null; + oniceconnectionstatechange: globalThis.RTCPeerConnection['oniceconnectionstatechange'] = null; + onicegatheringstatechange: globalThis.RTCPeerConnection['onicegatheringstatechange'] = null; + onnegotiationneeded: globalThis.RTCPeerConnection['onnegotiationneeded'] = null; + onsignalingstatechange: globalThis.RTCPeerConnection['onsignalingstatechange'] = null; + ontrack: globalThis.RTCPeerConnection['ontrack'] = null; + + private _checkConfiguration(config: globalThis.RTCConfiguration): void { + if (config && config.iceServers === undefined) config.iceServers = []; + if (config && config.iceTransportPolicy === undefined) config.iceTransportPolicy = 'all'; + + if (config?.iceServers === null) throw new TypeError('IceServers cannot be null'); + + // Check for all the properties of iceServers + if (Array.isArray(config?.iceServers)) { + for (let i = 0; i < config.iceServers.length; i++) { + if (config.iceServers[i] === null) throw new TypeError('IceServers cannot be null'); + if (config.iceServers[i] === undefined) + throw new TypeError('IceServers cannot be undefined'); + if (Object.keys(config.iceServers[i]).length === 0) + throw new TypeError('IceServers cannot be empty'); + + // If iceServers is string convert to array + if (typeof config.iceServers[i].urls === 'string') + config.iceServers[i].urls = [config.iceServers[i].urls as string]; + + // urls can not be empty + if ((config.iceServers[i].urls as string[])?.some((url) => url == '')) + throw new exceptions.SyntaxError('IceServers urls cannot be empty'); + + // urls should be valid URLs and match the protocols "stun:|turn:|turns:" + if ( + (config.iceServers[i].urls as string[])?.some((url) => { + try { + const parsedURL = new URL(url); - #peerConnection: PeerConnection; - #localOffer: any; - #localAnswer: any; - #dataChannels: Set; - #dataChannelsClosed = 0; - #config: globalThis.RTCConfiguration; - #canTrickleIceCandidates: boolean | null; - #sctp: globalThis.RTCSctpTransport; - - #localCandidates: globalThis.RTCIceCandidate[] = []; - #remoteCandidates: globalThis.RTCIceCandidate[] = []; - - // events - onconnectionstatechange: ((this: globalThis.RTCPeerConnection, ev: Event) => any) | null; - ondatachannel: ((this: globalThis.RTCPeerConnection, ev: globalThis.RTCDataChannelEvent) => any) | null; - onicecandidate: ((this: globalThis.RTCPeerConnection, ev: globalThis.RTCPeerConnectionIceEvent) => any) | null; - onicecandidateerror: ((this: globalThis.RTCPeerConnection, ev: Event) => any) | null; - oniceconnectionstatechange: ((this: globalThis.RTCPeerConnection, ev: Event) => any) | null; - onicegatheringstatechange: ((this: globalThis.RTCPeerConnection, ev: Event) => any) | null; - onnegotiationneeded: ((this: globalThis.RTCPeerConnection, ev: Event) => any) | null; - onsignalingstatechange: ((this: globalThis.RTCPeerConnection, ev: Event) => any) | null; - ontrack: ((this: globalThis.RTCPeerConnection, ev: globalThis.RTCTrackEvent) => any) | null; - - private _checkConfiguration(config: globalThis.RTCConfiguration): void { - if (config && config.iceServers === undefined) config.iceServers = []; - if (config && config.iceTransportPolicy === undefined) config.iceTransportPolicy = 'all'; - - if (config?.iceServers === null) throw new TypeError('IceServers cannot be null'); - - // Check for all the properties of iceServers - if (Array.isArray(config?.iceServers)) { - for (let i = 0; i < config.iceServers.length; i++) { - if (config.iceServers[i] === null) throw new TypeError('IceServers cannot be null'); - if (config.iceServers[i] === undefined) throw new TypeError('IceServers cannot be undefined'); - if (Object.keys(config.iceServers[i]).length === 0) throw new TypeError('IceServers cannot be empty'); - - // If iceServers is string convert to array - if (typeof config.iceServers[i].urls === 'string') - config.iceServers[i].urls = [config.iceServers[i].urls as string]; - - // urls can not be empty - if ((config.iceServers[i].urls as string[])?.some((url) => url == '')) - throw new exceptions.SyntaxError('IceServers urls cannot be empty'); - - // urls should be valid URLs and match the protocols "stun:|turn:|turns:" - if ( - (config.iceServers[i].urls as string[])?.some( - (url) => { - try { - const parsedURL = new URL(url) - - return !/^(stun:|turn:|turns:)$/.test(parsedURL.protocol) - } catch (error) { - return true - } - }, - ) - ) - throw new exceptions.SyntaxError('IceServers urls wrong format'); - - // If this is a turn server check for username and credential - if ((config.iceServers[i].urls as string[])?.some((url) => url.startsWith('turn'))) { - if (!config.iceServers[i].username) - throw new exceptions.InvalidAccessError('IceServers username cannot be null'); - if (!config.iceServers[i].credential) - throw new exceptions.InvalidAccessError('IceServers username cannot be undefined'); - } - - // length of urls can not be 0 - if (config.iceServers[i].urls?.length === 0) - throw new exceptions.SyntaxError('IceServers urls cannot be empty'); + return !/^(stun:|turn:|turns:)$/.test(parsedURL.protocol); + } catch (error) { + return true; } - } - - if ( - config && - config.iceTransportPolicy && - config.iceTransportPolicy !== 'all' && - config.iceTransportPolicy !== 'relay' + }) ) - throw new TypeError('IceTransportPolicy must be either "all" or "relay"'); - } - - setConfiguration(config: globalThis.RTCConfiguration): void { - this._checkConfiguration(config); - this.#config = config; - } - - - - constructor(config: RTCConfiguration = { iceServers: [], iceTransportPolicy: 'all' }) { - super(); - - this._checkConfiguration(config); - this.#config = config; - this.#localOffer = createDeferredPromise(); - this.#localAnswer = createDeferredPromise(); - this.#dataChannels = new Set(); - this.#canTrickleIceCandidates = null; - - try { - const peerIdentity = (config as any)?.peerIdentity ?? `peer-${getRandomString(7)}`; - this.#peerConnection = config?.peerConnection ?? new PeerConnection(peerIdentity, - { - ...config, - iceServers: - config?.iceServers - ?.map((server) => { - const urls = Array.isArray(server.urls) ? server.urls : [server.urls]; - - return urls.map((url) => { - if (server.username && server.credential) { - const [protocol, rest] = url.split(/:(.*)/); - return `${protocol}:${server.username}:${server.credential}@${rest}`; - } - return url; - }); - }) - .flat() ?? [], - }, - ); - } catch (error) { - if (!error || !error.message) throw new exceptions.NotFoundError('Unknown error'); - throw new exceptions.SyntaxError(error.message); + throw new exceptions.SyntaxError('IceServers urls wrong format'); + + // If this is a turn server check for username and credential + if ((config.iceServers[i].urls as string[])?.some((url) => url.startsWith('turn'))) { + if (!config.iceServers[i].username) + throw new exceptions.InvalidAccessError('IceServers username cannot be null'); + if (!config.iceServers[i].credential) + throw new exceptions.InvalidAccessError('IceServers username cannot be undefined'); } - // forward peerConnection events - this.#peerConnection.onStateChange(() => { - this.dispatchEvent(new Event('connectionstatechange')); - }); - - this.#peerConnection.onIceStateChange(() => { - this.dispatchEvent(new Event('iceconnectionstatechange')); - }); - - this.#peerConnection.onSignalingStateChange(() => { - this.dispatchEvent(new Event('signalingstatechange')); - }); - - this.#peerConnection.onGatheringStateChange(() => { - this.dispatchEvent(new Event('icegatheringstatechange')); - }); - - this.#peerConnection.onDataChannel((channel) => { - const dc = new RTCDataChannel(channel); - this.#dataChannels.add(dc); - this.dispatchEvent(new RTCDataChannelEvent('datachannel', { channel: dc })); - }); - - this.#peerConnection.onLocalDescription((sdp, type) => { - if (type === 'offer') { - this.#localOffer.resolve({ sdp, type }); - } - - if (type === 'answer') { - this.#localAnswer.resolve({ sdp, type }); - } + // length of urls can not be 0 + if (config.iceServers[i].urls?.length === 0) + throw new exceptions.SyntaxError('IceServers urls cannot be empty'); + } + } + + if ( + config && + config.iceTransportPolicy && + config.iceTransportPolicy !== 'all' && + config.iceTransportPolicy !== 'relay' + ) + throw new TypeError('IceTransportPolicy must be either "all" or "relay"'); + } + + setConfiguration(config: globalThis.RTCConfiguration): void { + this._checkConfiguration(config); + this.#config = config; + } + + constructor(config: RTCConfiguration = { iceServers: [], iceTransportPolicy: 'all' }) { + super(); + + this._checkConfiguration(config); + this.#config = config; + this.#localOffer = createDeferredPromise(); + this.#localAnswer = createDeferredPromise(); + this.#dataChannels = new Set(); + this.#canTrickleIceCandidates = null; + + try { + const peerIdentity = (config as any)?.peerIdentity ?? `peer-${getRandomString(7)}`; + this.#peerConnection = + config?.peerConnection ?? + new PeerConnection(peerIdentity, { + ...config, + iceServers: + config?.iceServers + ?.map((server) => { + const urls = Array.isArray(server.urls) ? server.urls : [server.urls]; + + return urls.map((url) => { + if (server.username && server.credential) { + const [protocol, rest] = url.split(/:(.*)/); + return `${protocol}:${server.username}:${server.credential}@${rest}`; + } + return url; + }); + }) + .flat() ?? [], }); + } catch (error) { + if (!error || !error.message) throw new exceptions.NotFoundError('Unknown error'); + throw new exceptions.SyntaxError(error.message); + } - this.#peerConnection.onLocalCandidate((candidate, sdpMid) => { - if (sdpMid === 'unspec') { - this.#localAnswer.reject(new Error(`Invalid description type ${sdpMid}`)); - return; - } + // forward peerConnection events + this.#peerConnection.onStateChange(() => { + this.dispatchEvent(new Event('connectionstatechange')); + }); - this.#localCandidates.push(new RTCIceCandidate({ candidate, sdpMid })); - this.dispatchEvent(new RTCPeerConnectionIceEvent(new RTCIceCandidate({ candidate, sdpMid }))); - }); + this.#peerConnection.onIceStateChange(() => { + this.dispatchEvent(new Event('iceconnectionstatechange')); + }); - // forward events to properties - this.addEventListener('connectionstatechange', (e) => { - if (this.onconnectionstatechange) this.onconnectionstatechange(e); - }); - this.addEventListener('signalingstatechange', (e) => { - if (this.onsignalingstatechange) this.onsignalingstatechange(e); - }); - this.addEventListener('iceconnectionstatechange', (e) => { - if (this.oniceconnectionstatechange) this.oniceconnectionstatechange(e); - }); - this.addEventListener('icegatheringstatechange', (e) => { - if (this.onicegatheringstatechange) this.onicegatheringstatechange(e); - }); - this.addEventListener('datachannel', (e) => { - if (this.ondatachannel) this.ondatachannel(e as globalThis.RTCDataChannelEvent); - }); - this.addEventListener('icecandidate', (e) => { - if (this.onicecandidate) this.onicecandidate(e as globalThis.RTCPeerConnectionIceEvent); - }); + this.#peerConnection.onSignalingStateChange(() => { + this.dispatchEvent(new Event('signalingstatechange')); + }); - this.#sctp = new RTCSctpTransport({ - pc: this, - extraFunctions: { - maxDataChannelId: (): number => { - return this.#peerConnection.maxDataChannelId(); - }, - maxMessageSize: (): number => { - return this.#peerConnection.maxMessageSize(); - }, - localCandidates: (): globalThis.RTCIceCandidate[] => { - return this.#localCandidates; - }, - remoteCandidates: (): globalThis.RTCIceCandidate[] => { - return this.#remoteCandidates; - }, - selectedCandidatePair: (): { local: SelectedCandidateInfo; remote: SelectedCandidateInfo } | null => { - return this.#peerConnection.getSelectedCandidatePair(); - }, - }, - }); - } + this.#peerConnection.onGatheringStateChange(() => { + this.dispatchEvent(new Event('icegatheringstatechange')); + }); - get canTrickleIceCandidates(): boolean | null { - return this.#canTrickleIceCandidates; - } + this.#peerConnection.onDataChannel((channel) => { + const dc = new RTCDataChannel(channel); + this.#dataChannels.add(dc); + this.dispatchEvent(new RTCDataChannelEvent('datachannel', { channel: dc })); + }); - get connectionState(): globalThis.RTCPeerConnectionState { - return this.#peerConnection.state(); - } + this.#peerConnection.onLocalDescription((sdp, type) => { + if (type === 'offer') { + this.#localOffer.resolve({ sdp, type }); + } - get iceConnectionState(): globalThis.RTCIceConnectionState { - let state = this.#peerConnection.iceState(); - // libdatachannel uses 'completed' instead of 'connected' - // see /webrtc/getstats.html - if (state == 'completed') state = 'connected'; - return state; - } + if (type === 'answer') { + this.#localAnswer.resolve({ sdp, type }); + } + }); - get iceGatheringState(): globalThis.RTCIceGatheringState { - return this.#peerConnection.gatheringState(); - } + this.#peerConnection.onLocalCandidate((candidate, sdpMid) => { + if (sdpMid === 'unspec') { + this.#localAnswer.reject(new Error(`Invalid description type ${sdpMid}`)); + return; + } - get currentLocalDescription(): globalThis.RTCSessionDescription { - return new RTCSessionDescription(this.#peerConnection.localDescription() as any); - } + this.#localCandidates.push(new RTCIceCandidate({ candidate, sdpMid })); + this.dispatchEvent(new RTCPeerConnectionIceEvent(new RTCIceCandidate({ candidate, sdpMid }))); + }); - get currentRemoteDescription(): globalThis.RTCSessionDescription { - return new RTCSessionDescription(this.#peerConnection.remoteDescription() as any); - } + // forward events to properties + this.addEventListener('connectionstatechange', (e) => { + this.onconnectionstatechange?.(e); + }); + this.addEventListener('signalingstatechange', (e) => { + this.onsignalingstatechange?.(e); + }); + this.addEventListener('iceconnectionstatechange', (e) => { + this.oniceconnectionstatechange?.(e); + }); + this.addEventListener('icegatheringstatechange', (e) => { + this.onicegatheringstatechange?.(e); + }); + this.addEventListener('datachannel', (e) => { + this.ondatachannel?.(e as globalThis.RTCDataChannelEvent); + }); + this.addEventListener('icecandidate', (e) => { + this.onicecandidate?.(e as globalThis.RTCPeerConnectionIceEvent); + }); - get localDescription(): globalThis.RTCSessionDescription { - return new RTCSessionDescription(this.#peerConnection.localDescription() as any); - } + this.#sctp = new RTCSctpTransport({ + pc: this, + extraFunctions: { + maxDataChannelId: (): number => { + return this.#peerConnection.maxDataChannelId(); + }, + maxMessageSize: (): number => { + return this.#peerConnection.maxMessageSize(); + }, + localCandidates: (): globalThis.RTCIceCandidate[] => { + return this.#localCandidates; + }, + remoteCandidates: (): globalThis.RTCIceCandidate[] => { + return this.#remoteCandidates; + }, + selectedCandidatePair: (): { + local: SelectedCandidateInfo; + remote: SelectedCandidateInfo; + } | null => { + return this.#peerConnection.getSelectedCandidatePair(); + }, + }, + }); + } - get pendingLocalDescription(): globalThis.RTCSessionDescription { - return new RTCSessionDescription(this.#peerConnection.localDescription() as any); - } + get canTrickleIceCandidates(): boolean | null { + return this.#canTrickleIceCandidates; + } - get pendingRemoteDescription():globalThis.RTCSessionDescription { - return new RTCSessionDescription(this.#peerConnection.remoteDescription() as any); - } + get connectionState(): globalThis.RTCPeerConnectionState { + return this.#peerConnection.state(); + } - get remoteDescription(): globalThis.RTCSessionDescription { - return new RTCSessionDescription(this.#peerConnection.remoteDescription() as any); - } + get iceConnectionState(): globalThis.RTCIceConnectionState { + let state = this.#peerConnection.iceState(); + // libdatachannel uses 'completed' instead of 'connected' + // see /webrtc/getstats.html + if (state == 'completed') state = 'connected'; + return state; + } - get sctp(): globalThis.RTCSctpTransport { - return this.#sctp; - } + get iceGatheringState(): globalThis.RTCIceGatheringState { + return this.#peerConnection.gatheringState(); + } - get signalingState(): globalThis.RTCSignalingState { - return this.#peerConnection.signalingState(); - } + get currentLocalDescription(): globalThis.RTCSessionDescription { + return new RTCSessionDescription(this.#peerConnection.localDescription() as any); + } - async addIceCandidate(candidate?: globalThis.RTCIceCandidateInit | null): Promise { - if (!candidate || !candidate.candidate) { - return; - } + get currentRemoteDescription(): globalThis.RTCSessionDescription { + return new RTCSessionDescription(this.#peerConnection.remoteDescription() as any); + } - if (candidate.sdpMid === null && candidate.sdpMLineIndex === null) { - throw new TypeError('sdpMid must be set'); - } + get localDescription(): globalThis.RTCSessionDescription { + return new RTCSessionDescription(this.#peerConnection.localDescription() as any); + } - if (candidate.sdpMid === undefined && candidate.sdpMLineIndex == undefined) { - throw new TypeError('sdpMid must be set'); - } + get pendingLocalDescription(): globalThis.RTCSessionDescription { + return new RTCSessionDescription(this.#peerConnection.localDescription() as any); + } - // Reject if sdpMid format is not valid - // ?? - if (candidate.sdpMid && candidate.sdpMid.length > 3) { - // console.log(candidate.sdpMid); - throw new exceptions.OperationError('Invalid sdpMid format'); - } + get pendingRemoteDescription(): globalThis.RTCSessionDescription { + return new RTCSessionDescription(this.#peerConnection.remoteDescription() as any); + } - // We don't care about sdpMLineIndex, just for test - if (!candidate.sdpMid && candidate.sdpMLineIndex > 1) { - throw new exceptions.OperationError('This is only for test case.'); - } + get remoteDescription(): globalThis.RTCSessionDescription { + return new RTCSessionDescription(this.#peerConnection.remoteDescription() as any); + } - try { - this.#peerConnection.addRemoteCandidate(candidate.candidate, candidate.sdpMid || '0'); - this.#remoteCandidates.push( - new RTCIceCandidate({ candidate: candidate.candidate, sdpMid: candidate.sdpMid || '0' }), - ); - } catch (error) { - if (!error || !error.message) throw new exceptions.NotFoundError('Unknown error'); + get sctp(): globalThis.RTCSctpTransport { + return this.#sctp; + } - // Check error Message if contains specific message - if (error.message.includes('remote candidate without remote description')) - throw new exceptions.InvalidStateError(error.message); - if (error.message.includes('Invalid candidate format')) throw new exceptions.OperationError(error.message); + get signalingState(): globalThis.RTCSignalingState { + return this.#peerConnection.signalingState(); + } - throw new exceptions.NotFoundError(error.message); - } + async addIceCandidate(candidate?: globalThis.RTCIceCandidateInit | null): Promise { + if (!candidate || !candidate.candidate) { + return; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - addTrack(_track, ..._streams): globalThis.RTCRtpSender { - throw new DOMException('Not implemented'); + if (candidate.sdpMid === null && candidate.sdpMLineIndex === null) { + throw new TypeError('sdpMid must be set'); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - addTransceiver(_trackOrKind, _init): globalThis.RTCRtpTransceiver { - throw new DOMException('Not implemented'); + if (candidate.sdpMid === undefined && candidate.sdpMLineIndex == undefined) { + throw new TypeError('sdpMid must be set'); } - close(): void { - this.#peerConnection.close(); + // Reject if sdpMid format is not valid + // ?? + if (candidate.sdpMid && candidate.sdpMid.length > 3) { + // console.log(candidate.sdpMid); + throw new exceptions.OperationError('Invalid sdpMid format'); } - createAnswer(): Promise { - return this.#localAnswer; + // We don't care about sdpMLineIndex, just for test + if (!candidate.sdpMid && candidate.sdpMLineIndex > 1) { + throw new exceptions.OperationError('This is only for test case.'); } + try { + this.#peerConnection.addRemoteCandidate(candidate.candidate, candidate.sdpMid ?? '0'); + this.#remoteCandidates.push( + new RTCIceCandidate({ candidate: candidate.candidate, sdpMid: candidate.sdpMid ?? '0' }), + ); + } catch (error) { + if (!error || !error.message) throw new exceptions.NotFoundError('Unknown error'); - createDataChannel(label, opts = {}): globalThis.RTCDataChannel { - const channel = this.#peerConnection.createDataChannel(label, opts); - const dataChannel = new RTCDataChannel(channel, opts); + // Check error Message if contains specific message + if (error.message.includes('remote candidate without remote description')) + throw new exceptions.InvalidStateError(error.message); + if (error.message.includes('Invalid candidate format')) + throw new exceptions.OperationError(error.message); - // ensure we can close all channels when shutting down - this.#dataChannels.add(dataChannel); - dataChannel.addEventListener('close', () => { - this.#dataChannels.delete(dataChannel); - this.#dataChannelsClosed++; - }); - - return dataChannel; + throw new exceptions.NotFoundError(error.message); } + } - createOffer(): Promise { - return this.#localOffer; - } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + addTrack(_track, ..._streams): globalThis.RTCRtpSender { + throw new DOMException('Not implemented'); + } - getConfiguration(): globalThis.RTCConfiguration { - return this.#config; - } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + addTransceiver(_trackOrKind, _init): globalThis.RTCRtpTransceiver { + throw new DOMException('Not implemented'); + } - getReceivers(): globalThis.RTCRtpReceiver[] { - throw new DOMException('Not implemented'); - } + close(): void { + this.#peerConnection.close(); + } - getSenders(): globalThis.RTCRtpSender[] { - throw new DOMException('Not implemented'); - } + createAnswer(): Promise { + return this.#localAnswer; + } - getStats(): Promise { - return new Promise((resolve) => { - const report = new Map(); - const cp = this.#peerConnection?.getSelectedCandidatePair(); - const bytesSent = this.#peerConnection?.bytesSent(); - const bytesReceived = this.#peerConnection?.bytesReceived(); - const rtt = this.#peerConnection?.rtt(); - - if(!cp) { - return resolve(report); - } + createDataChannel(label, opts = {}): globalThis.RTCDataChannel { + const channel = this.#peerConnection.createDataChannel(label, opts); + const dataChannel = new RTCDataChannel(channel, opts); - const localIdRs = getRandomString(8); - const localId = 'RTCIceCandidate_' + localIdRs; - report.set(localId, { - id: localId, - type: 'local-candidate', - timestamp: Date.now(), - candidateType: cp.local.type, - ip: cp.local.address, - port: cp.local.port, - }); - - const remoteIdRs = getRandomString(8); - const remoteId = 'RTCIceCandidate_' + remoteIdRs; - report.set(remoteId, { - id: remoteId, - type: 'remote-candidate', - timestamp: Date.now(), - candidateType: cp.remote.type, - ip: cp.remote.address, - port: cp.remote.port, - }); - - const candidateId = 'RTCIceCandidatePair_' + localIdRs + '_' + remoteIdRs; - report.set(candidateId, { - id: candidateId, - type: 'candidate-pair', - timestamp: Date.now(), - localCandidateId: localId, - remoteCandidateId: remoteId, - state: 'succeeded', - nominated: true, - writable: true, - bytesSent: bytesSent, - bytesReceived: bytesReceived, - totalRoundTripTime: rtt, - currentRoundTripTime: rtt, - }); - - const transportId = 'RTCTransport_0_1'; - report.set(transportId, { - id: transportId, - timestamp: Date.now(), - type: 'transport', - bytesSent: bytesSent, - bytesReceived: bytesReceived, - dtlsState: 'connected', - selectedCandidatePairId: candidateId, - selectedCandidatePairChanges: 1, - }); - - // peer-connection' - report.set('P', { - id: 'P', - type: 'peer-connection', - timestamp: Date.now(), - dataChannelsOpened: this.#dataChannels.size, - dataChannelsClosed: this.#dataChannelsClosed, - }); - - return resolve(report); - }); - } + // ensure we can close all channels when shutting down + this.#dataChannels.add(dataChannel); + dataChannel.addEventListener('close', () => { + this.#dataChannels.delete(dataChannel); + this.#dataChannelsClosed++; + }); - getTransceivers(): globalThis.RTCRtpTransceiver[] { - return []; // throw new DOMException('Not implemented'); - } + return dataChannel; + } + + createOffer(): Promise { + return this.#localOffer; + } + + getConfiguration(): globalThis.RTCConfiguration { + return this.#config; + } + + getReceivers(): globalThis.RTCRtpReceiver[] { + throw new DOMException('Not implemented'); + } + + getSenders(): globalThis.RTCRtpSender[] { + throw new DOMException('Not implemented'); + } + + getStats(): Promise { + return new Promise((resolve) => { + const report = new Map(); + const cp = this.#peerConnection?.getSelectedCandidatePair(); + const bytesSent = this.#peerConnection?.bytesSent(); + const bytesReceived = this.#peerConnection?.bytesReceived(); + const rtt = this.#peerConnection?.rtt(); + + if (!cp) { + return resolve(report); + } + + const localIdRs = getRandomString(8); + const localId = 'RTCIceCandidate_' + localIdRs; + report.set(localId, { + id: localId, + type: 'local-candidate', + timestamp: Date.now(), + candidateType: cp.local.type, + ip: cp.local.address, + port: cp.local.port, + }); + + const remoteIdRs = getRandomString(8); + const remoteId = 'RTCIceCandidate_' + remoteIdRs; + report.set(remoteId, { + id: remoteId, + type: 'remote-candidate', + timestamp: Date.now(), + candidateType: cp.remote.type, + ip: cp.remote.address, + port: cp.remote.port, + }); + + const candidateId = 'RTCIceCandidatePair_' + localIdRs + '_' + remoteIdRs; + report.set(candidateId, { + id: candidateId, + type: 'candidate-pair', + timestamp: Date.now(), + localCandidateId: localId, + remoteCandidateId: remoteId, + state: 'succeeded', + nominated: true, + writable: true, + bytesSent: bytesSent, + bytesReceived: bytesReceived, + totalRoundTripTime: rtt, + currentRoundTripTime: rtt, + }); + + const transportId = 'RTCTransport_0_1'; + report.set(transportId, { + id: transportId, + timestamp: Date.now(), + type: 'transport', + bytesSent: bytesSent, + bytesReceived: bytesReceived, + dtlsState: 'connected', + selectedCandidatePairId: candidateId, + selectedCandidatePairChanges: 1, + }); + + // peer-connection' + report.set('P', { + id: 'P', + type: 'peer-connection', + timestamp: Date.now(), + dataChannelsOpened: this.#dataChannels.size, + dataChannelsClosed: this.#dataChannelsClosed, + }); + + return resolve(report); + }); + } - removeTrack(): void { - throw new DOMException('Not implemented'); - } + getTransceivers(): globalThis.RTCRtpTransceiver[] { + return []; // throw new DOMException('Not implemented'); + } - restartIce(): Promise { - throw new DOMException('Not implemented'); - } + removeTrack(): void { + throw new DOMException('Not implemented'); + } - async setLocalDescription(description: globalThis.RTCSessionDescriptionInit): Promise { - if (description?.type !== 'offer') { - // any other type causes libdatachannel to throw - return; - } + restartIce(): Promise { + throw new DOMException('Not implemented'); + } - this.#peerConnection.setLocalDescription(description?.type as any); + async setLocalDescription(description: globalThis.RTCSessionDescriptionInit): Promise { + if (description?.type !== 'offer') { + // any other type causes libdatachannel to throw + return; } - async setRemoteDescription(description: globalThis.RTCSessionDescriptionInit): Promise { - if (description.sdp == null) { - throw new DOMException('Remote SDP must be set'); - } + this.#peerConnection.setLocalDescription(description?.type as any); + } - this.#peerConnection.setRemoteDescription(description.sdp, description.type as any); + async setRemoteDescription(description: globalThis.RTCSessionDescriptionInit): Promise { + if (description.sdp == null) { + throw new DOMException('Remote SDP must be set'); } + + this.#peerConnection.setRemoteDescription(description.sdp, description.type as any); + } } function createDeferredPromise(): any { diff --git a/src/polyfill/RTCSctpTransport.ts b/src/polyfill/RTCSctpTransport.ts index d032a372..39ef5a5c 100644 --- a/src/polyfill/RTCSctpTransport.ts +++ b/src/polyfill/RTCSctpTransport.ts @@ -2,53 +2,52 @@ import RTCDtlsTransport from './RTCDtlsTransport'; export default class RTCSctpTransport extends EventTarget implements globalThis.RTCSctpTransport { - #pc: globalThis.RTCPeerConnection = null; - #extraFunctions = null; - #transport: globalThis.RTCDtlsTransport = null; - - onstatechange: ((this: globalThis.RTCSctpTransport, ev: Event) => any) | null = null; - - constructor(initial: { pc: globalThis.RTCPeerConnection, extraFunctions }) { - super(); - this.#pc = initial.pc; - this.#extraFunctions = initial.extraFunctions; - - this.#transport = new RTCDtlsTransport({ pc: initial.pc, extraFunctions: initial.extraFunctions }); - - // forward peerConnection events - this.#pc.addEventListener('connectionstatechange', () => { - this.dispatchEvent(new Event('statechange')); - }); - - // forward events to properties - this.addEventListener('statechange', (e) => { - if (this.onstatechange) this.onstatechange(e); - }); - } - - get maxChannels(): number | null { - if (this.state !== 'connected') return null; - return this.#pc ? this.#extraFunctions.maxDataChannelId() : 0; - } - - get maxMessageSize(): number { - if (this.state !== 'connected') return null; - return this.#pc ? this.#extraFunctions.maxMessageSize() : 0; + #pc: globalThis.RTCPeerConnection = null; + #extraFunctions = null; + #transport: globalThis.RTCDtlsTransport = null; + + onstatechange: globalThis.RTCSctpTransport['onstatechange'] = null; + + constructor(initial: { pc: globalThis.RTCPeerConnection; extraFunctions }) { + super(); + this.#pc = initial.pc; + this.#extraFunctions = initial.extraFunctions; + + this.#transport = new RTCDtlsTransport({ + pc: initial.pc, + extraFunctions: initial.extraFunctions, + }); + + this.#pc.addEventListener('connectionstatechange', () => { + const e = new Event('statechange'); + this.dispatchEvent(e); + this.onstatechange?.(e); + }); + } + + get maxChannels(): number | null { + if (this.state !== 'connected') return null; + return this.#pc ? this.#extraFunctions.maxDataChannelId() : 0; + } + + get maxMessageSize(): number { + if (this.state !== 'connected') return null; + return this.#pc ? this.#extraFunctions.maxMessageSize() : 0; + } + + get state(): globalThis.RTCSctpTransportState { + // reduce state from new, connecting, connected, disconnected, failed, closed, unknown + // to RTCSctpTransport states connecting, connected, closed + let state = this.#pc.connectionState; + if (state === 'new' || state === 'connecting') { + state = 'connecting'; + } else if (state === 'disconnected' || state === 'failed' || state === 'closed') { + state = 'closed'; } + return state; + } - get state(): globalThis.RTCSctpTransportState { - // reduce state from new, connecting, connected, disconnected, failed, closed, unknown - // to RTCSctpTransport states connecting, connected, closed - let state = this.#pc.connectionState; - if (state === 'new' || state === 'connecting') { - state = 'connecting'; - } else if (state === 'disconnected' || state === 'failed' || state === 'closed') { - state = 'closed'; - } - return state; - } - - get transport(): globalThis.RTCDtlsTransport { - return this.#transport; - } + get transport(): globalThis.RTCDtlsTransport { + return this.#transport; + } }