diff --git a/packages/transport-webrtc/package.json b/packages/transport-webrtc/package.json index 7cef6a1fae..4e88e83735 100644 --- a/packages/transport-webrtc/package.json +++ b/packages/transport-webrtc/package.json @@ -67,7 +67,7 @@ "it-to-buffer": "^4.0.2", "multiformats": "^12.1.3", "multihashes": "^4.0.3", - "node-datachannel": "^0.5.0-dev", + "node-datachannel": "^0.5.3", "p-defer": "^4.0.0", "p-event": "^6.0.0", "p-timeout": "^6.1.2", diff --git a/packages/transport-webrtc/src/webrtc/index.ts b/packages/transport-webrtc/src/webrtc/index.ts index 6540ba340b..2487a68022 100644 --- a/packages/transport-webrtc/src/webrtc/index.ts +++ b/packages/transport-webrtc/src/webrtc/index.ts @@ -1,11 +1,6 @@ import node from 'node-datachannel' -import { IceCandidate } from './rtc-ice-candidate.js' -import { PeerConnection } from './rtc-peer-connection.js' -import { SessionDescription } from './rtc-session-description.js' -export { SessionDescription as RTCSessionDescription } -export { IceCandidate as RTCIceCandidate } -export { PeerConnection as RTCPeerConnection } +export { RTCSessionDescription, RTCIceCandidate, RTCPeerConnection } from 'node-datachannel/polyfill' export function cleanup (): void { node.cleanup() diff --git a/packages/transport-webrtc/src/webrtc/rtc-data-channel.ts b/packages/transport-webrtc/src/webrtc/rtc-data-channel.ts deleted file mode 100644 index b5129abe54..0000000000 --- a/packages/transport-webrtc/src/webrtc/rtc-data-channel.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' -import type node from 'node-datachannel' - -export class DataChannel extends EventTarget implements RTCDataChannel { - binaryType: BinaryType - - readonly maxPacketLifeTime: number | null - readonly maxRetransmits: number | null - readonly negotiated: boolean - readonly ordered: boolean - - onbufferedamountlow: ((this: RTCDataChannel, ev: Event) => any) | null - onclose: ((this: RTCDataChannel, ev: Event) => any) | null - onclosing: ((this: RTCDataChannel, ev: Event) => any) | null - onerror: ((this: RTCDataChannel, ev: Event) => any) | null - onmessage: ((this: RTCDataChannel, ev: MessageEvent) => any) | null - onopen: ((this: RTCDataChannel, ev: Event) => any) | null - - #dataChannel: node.DataChannel - #bufferedAmountLowThreshold: number - #readyState: RTCDataChannelState - - constructor (dataChannel: node.DataChannel, dataChannelDict: RTCDataChannelInit = {}) { - super() - - this.#dataChannel = dataChannel - this.#readyState = 'connecting' - this.#bufferedAmountLowThreshold = 0 - - this.binaryType = 'arraybuffer' - - this.#dataChannel.onOpen(() => { - this.#readyState = 'open' - this.dispatchEvent(new Event('open')) - }) - this.#dataChannel.onClosed(() => { - this.#readyState = 'closed' - this.dispatchEvent(new Event('close')) - }) - this.#dataChannel.onError((msg) => { - this.#readyState = 'closed' - this.dispatchEvent(new RTCErrorEvent('error', { - error: new RTCError({ - errorDetail: 'data-channel-failure' - }, msg) - })) - }) - this.#dataChannel.onBufferedAmountLow(() => { - this.dispatchEvent(new Event('bufferedamountlow')) - }) - this.#dataChannel.onMessage((data: string | Uint8Array) => { - if (typeof data === 'string') { - data = uint8ArrayFromString(data) - } - - this.dispatchEvent(new MessageEvent('message', { data })) - }) - - // forward events to properties - this.addEventListener('message', event => { - this.onmessage?.(event as MessageEvent) - }) - this.addEventListener('bufferedamountlow', event => { - this.onbufferedamountlow?.(event) - }) - this.addEventListener('error', event => { - this.onerror?.(event) - }) - this.addEventListener('close', event => { - this.onclose?.(event) - }) - this.addEventListener('closing', event => { - this.onclosing?.(event) - }) - this.addEventListener('open', event => { - this.onopen?.(event) - }) - - this.onbufferedamountlow = null - this.onclose = null - this.onclosing = null - this.onerror = null - this.onmessage = null - this.onopen = null - - this.maxPacketLifeTime = dataChannelDict.maxPacketLifeTime ?? null - this.maxRetransmits = dataChannelDict.maxRetransmits ?? null - this.negotiated = dataChannelDict.negotiated ?? false - this.ordered = dataChannelDict.ordered ?? true - } - - get id (): number { - return this.#dataChannel.getId() - } - - get label (): string { - return this.#dataChannel.getLabel() - } - - get protocol (): string { - return this.#dataChannel.getProtocol() - } - - get bufferedAmount (): number { - return this.#dataChannel.bufferedAmount() - } - - set bufferedAmountLowThreshold (threshold: number) { - this.#bufferedAmountLowThreshold = threshold - this.#dataChannel.setBufferedAmountLowThreshold(threshold) - } - - get bufferedAmountLowThreshold (): number { - return this.#bufferedAmountLowThreshold - } - - get readyState (): RTCDataChannelState { - return this.#readyState - } - - close (): void { - this.#readyState = 'closing' - this.dispatchEvent(new Event('closing')) - - this.#dataChannel.close() - } - - send (data: string): void - send (data: Blob): void - send (data: ArrayBuffer): void - send (data: ArrayBufferView): void - send (data: any): void { - // TODO: sending Blobs - if (typeof data === 'string') { - this.#dataChannel.sendMessage(data) - } else { - this.#dataChannel.sendMessageBinary(data) - } - } -} diff --git a/packages/transport-webrtc/src/webrtc/rtc-events.ts b/packages/transport-webrtc/src/webrtc/rtc-events.ts deleted file mode 100644 index b7a8772139..0000000000 --- a/packages/transport-webrtc/src/webrtc/rtc-events.ts +++ /dev/null @@ -1,19 +0,0 @@ -export class PeerConnectionIceEvent extends Event implements RTCPeerConnectionIceEvent { - readonly candidate: RTCIceCandidate | null - - constructor (candidate: RTCIceCandidate) { - super('icecandidate') - - this.candidate = candidate - } -} - -export class DataChannelEvent extends Event implements RTCDataChannelEvent { - readonly channel: RTCDataChannel - - constructor (channel: RTCDataChannel) { - super('datachannel') - - this.channel = channel - } -} diff --git a/packages/transport-webrtc/src/webrtc/rtc-ice-candidate.ts b/packages/transport-webrtc/src/webrtc/rtc-ice-candidate.ts deleted file mode 100644 index ea02ec99e1..0000000000 --- a/packages/transport-webrtc/src/webrtc/rtc-ice-candidate.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * @see https://developer.mozilla.org/docs/Web/API/RTCIceCandidate - */ -export class IceCandidate implements RTCIceCandidate { - readonly address: string | null - readonly candidate: string - readonly component: RTCIceComponent | null - readonly foundation: string | null - readonly port: number | null - readonly priority: number | null - readonly protocol: RTCIceProtocol | null - readonly relatedAddress: string | null - readonly relatedPort: number | null - readonly sdpMLineIndex: number | null - readonly sdpMid: string | null - readonly tcpType: RTCIceTcpCandidateType | null - readonly type: RTCIceCandidateType | null - readonly usernameFragment: string | null - - constructor (init: RTCIceCandidateInit) { - if (init.candidate == null) { - throw new DOMException('candidate must be specified') - } - - this.candidate = init.candidate - this.sdpMLineIndex = init.sdpMLineIndex ?? null - this.sdpMid = init.sdpMid ?? null - this.usernameFragment = init.usernameFragment ?? null - - this.address = null - this.component = null - this.foundation = null - this.port = null - this.priority = null - this.protocol = null - this.relatedAddress = null - this.relatedPort = null - this.tcpType = null - this.type = null - } - - toJSON (): RTCIceCandidateInit { - return { - candidate: this.candidate, - sdpMLineIndex: this.sdpMLineIndex, - sdpMid: this.sdpMid, - usernameFragment: this.usernameFragment - } - } -} diff --git a/packages/transport-webrtc/src/webrtc/rtc-peer-connection.ts b/packages/transport-webrtc/src/webrtc/rtc-peer-connection.ts deleted file mode 100644 index 7b2b5c6446..0000000000 --- a/packages/transport-webrtc/src/webrtc/rtc-peer-connection.ts +++ /dev/null @@ -1,306 +0,0 @@ -import node from 'node-datachannel' -import defer, { type DeferredPromise } from 'p-defer' -import { DataChannel } from './rtc-data-channel.js' -import { DataChannelEvent, PeerConnectionIceEvent } from './rtc-events.js' -import { IceCandidate } from './rtc-ice-candidate.js' -import { SessionDescription } from './rtc-session-description.js' - -export class PeerConnection extends EventTarget implements RTCPeerConnection { - static async generateCertificate (keygenAlgorithm: AlgorithmIdentifier): Promise { - throw new Error('Not implemented') - } - - canTrickleIceCandidates: boolean | null - sctp: RTCSctpTransport | null - - onconnectionstatechange: ((this: RTCPeerConnection, ev: Event) => any) | null - ondatachannel: ((this: RTCPeerConnection, ev: RTCDataChannelEvent) => any) | null - onicecandidate: ((this: RTCPeerConnection, ev: RTCPeerConnectionIceEvent) => any) | null - onicecandidateerror: ((this: RTCPeerConnection, ev: Event) => any) | null - oniceconnectionstatechange: ((this: RTCPeerConnection, ev: Event) => any) | null - onicegatheringstatechange: ((this: RTCPeerConnection, ev: Event) => any) | null - onnegotiationneeded: ((this: RTCPeerConnection, ev: Event) => any) | null - onsignalingstatechange: ((this: RTCPeerConnection, ev: Event) => any) | null - ontrack: ((this: RTCPeerConnection, ev: RTCTrackEvent) => any) | null - - #peerConnection: node.PeerConnection - #config: RTCConfiguration - #localOffer: DeferredPromise - #localAnswer: DeferredPromise - #dataChannels: Set - - constructor (init: RTCConfiguration = {}) { - super() - - this.#config = init - this.#localOffer = defer() - this.#localAnswer = defer() - this.#dataChannels = new Set() - - const iceServers = init.iceServers ?? [] - - this.#peerConnection = new node.PeerConnection(`peer-${Math.random()}`, { - iceServers: iceServers.map(server => { - const urls = (Array.isArray(server.urls) ? server.urls : [server.urls]).map(str => new URL(str)) - - return urls.map(url => { - /** @type {import('../lib/index.js').IceServer} */ - const iceServer = { - hostname: url.hostname, - port: parseInt(url.port, 10), - username: server.username, - password: server.credential - // relayType - how to specify? - } - - return iceServer - }) - }) - .flat(), - iceTransportPolicy: init?.iceTransportPolicy - }) - - this.#peerConnection.onStateChange(() => { - this.dispatchEvent(new Event('connectionstatechange')) - }) - // https://github.com/murat-dogan/node-datachannel/pull/171 - // this.#peerConnection.onSignalingStateChange(() => { - // this.dispatchEvent(new Event('signalingstatechange')) - // }) - this.#peerConnection.onGatheringStateChange(() => { - this.dispatchEvent(new Event('icegatheringstatechange')) - }) - this.#peerConnection.onDataChannel(channel => { - this.dispatchEvent(new DataChannelEvent(new DataChannel(channel))) - }) - - // forward events to properties - this.addEventListener('connectionstatechange', event => { - this.onconnectionstatechange?.(event) - }) - this.addEventListener('signalingstatechange', event => { - this.onsignalingstatechange?.(event) - }) - this.addEventListener('icegatheringstatechange', event => { - this.onicegatheringstatechange?.(event) - }) - this.addEventListener('datachannel', event => { - this.ondatachannel?.(event as RTCDataChannelEvent) - }) - - this.#peerConnection.onLocalDescription((sdp, type) => { - if (type === 'offer') { - this.#localOffer.resolve({ - sdp, - type - }) - } - - if (type === 'answer') { - this.#localAnswer.resolve({ - sdp, - type - }) - } - }) - - this.#peerConnection.onLocalCandidate((candidate, mid) => { - if (mid === 'unspec') { - this.#localAnswer.reject(new Error(`Invalid description type ${mid}`)) - return - } - - const event = new PeerConnectionIceEvent(new IceCandidate({ candidate })) - - this.onicecandidate?.(event) - }) - - this.canTrickleIceCandidates = null - this.sctp = null - this.onconnectionstatechange = null - this.ondatachannel = null - this.onicecandidate = null - this.onicecandidateerror = null - this.oniceconnectionstatechange = null - this.onicegatheringstatechange = null - this.onnegotiationneeded = null - this.onsignalingstatechange = null - this.ontrack = null - } - - get connectionState (): RTCPeerConnectionState { - return assertState(this.#peerConnection.state(), RTCPeerConnectionStates) - } - - get iceConnectionState (): RTCIceConnectionState { - return assertState(this.#peerConnection.state(), RTCIceConnectionStates) - } - - get iceGatheringState (): RTCIceGatheringState { - return assertState(this.#peerConnection.gatheringState(), RTCIceGatheringStates) - } - - get signalingState (): RTCSignalingState { - return assertState(this.#peerConnection.signalingState(), RTCSignalingStates) - } - - get currentLocalDescription (): RTCSessionDescription | null { - return toSessionDescription(this.#peerConnection.localDescription()) - } - - get localDescription (): RTCSessionDescription | null { - return toSessionDescription(this.#peerConnection.localDescription()) - } - - get pendingLocalDescription (): RTCSessionDescription | null { - return toSessionDescription(this.#peerConnection.localDescription()) - } - - get currentRemoteDescription (): RTCSessionDescription | null { - // not exposed by node-datachannel - return toSessionDescription(null) - } - - get pendingRemoteDescription (): RTCSessionDescription | null { - // not exposed by node-datachannel - return toSessionDescription(null) - } - - get remoteDescription (): RTCSessionDescription | null { - // not exposed by node-datachannel - return toSessionDescription(null) - } - - async addIceCandidate (candidate?: RTCIceCandidateInit): Promise { - if (candidate == null || candidate.candidate == null) { - throw new Error('Candidate invalid') - } - - this.#peerConnection.addRemoteCandidate(candidate.candidate, candidate.sdpMid ?? '0') - } - - addTrack (track: MediaStreamTrack, ...streams: MediaStream[]): RTCRtpSender { - throw new Error('Not implemented') - } - - addTransceiver (trackOrKind: MediaStreamTrack | string, init?: RTCRtpTransceiverInit): RTCRtpTransceiver { - throw new Error('Not implemented') - } - - close (): void { - // close all channels before shutting down - this.#dataChannels.forEach(channel => { - channel.close() - }) - - this.#peerConnection.close() - this.#peerConnection.destroy() - } - - createDataChannel (label: string, dataChannelDict: RTCDataChannelInit = {}): RTCDataChannel { - const channel = this.#peerConnection.createDataChannel(label, dataChannelDict) - const dataChannel = new DataChannel(channel, dataChannelDict) - - // ensure we can close all channels when shutting down - this.#dataChannels.add(dataChannel) - dataChannel.addEventListener('close', () => { - this.#dataChannels.delete(dataChannel) - }) - - return dataChannel - } - - async createOffer (options?: RTCOfferOptions): Promise - async createOffer (successCallback: RTCSessionDescriptionCallback, failureCallback: RTCPeerConnectionErrorCallback, options?: RTCOfferOptions): Promise - async createOffer (...args: any[]): Promise { - return this.#localOffer.promise - } - - async createAnswer (options?: RTCAnswerOptions): Promise - async createAnswer (successCallback: RTCSessionDescriptionCallback, failureCallback: RTCPeerConnectionErrorCallback): Promise - async createAnswer (...args: any[]): Promise { - return this.#localAnswer.promise - } - - getConfiguration (): RTCConfiguration { - return this.#config - } - - getReceivers (): RTCRtpReceiver[] { - throw new Error('Not implemented') - } - - getSenders (): RTCRtpSender[] { - throw new Error('Not implemented') - } - - async getStats (selector?: MediaStreamTrack | null): Promise { - throw new Error('Not implemented') - } - - getTransceivers (): RTCRtpTransceiver[] { - throw new Error('Not implemented') - } - - removeTrack (sender: RTCRtpSender): void { - throw new Error('Not implemented') - } - - restartIce (): void { - throw new Error('Not implemented') - } - - setConfiguration (configuration: RTCConfiguration = {}): void { - this.#config = configuration - } - - async setLocalDescription (description?: RTCLocalSessionDescriptionInit): Promise { - if (description == null || description.type == null) { - throw new Error('Local description type must be set') - } - - if (description.type !== 'offer') { - // any other type causes libdatachannel to throw - return - } - - // @ts-expect-error types are wrong - this.#peerConnection.setLocalDescription(description.type) - } - - async setRemoteDescription (description: RTCSessionDescriptionInit): Promise { - if (description.sdp == null) { - throw new Error('Remote SDP must be set') - } - - // @ts-expect-error types are wrong - this.#peerConnection.setRemoteDescription(description.sdp, description.type) - } -} - -export { PeerConnection as RTCPeerConnection } - -function assertState (state: any, states: T[]): T { - if (state != null && !states.includes(state)) { - throw new Error(`Invalid value encountered - "${state}" must be one of ${states}`) - } - - return state as T -} - -function toSessionDescription (description: { sdp?: string, type: string } | null): RTCSessionDescription | null { - if (description == null) { - return null - } - - return new SessionDescription({ - sdp: description.sdp, - type: assertState(description.type, RTCSdpTypes) - }) -} - -const RTCPeerConnectionStates: RTCPeerConnectionState[] = ['closed', 'connected', 'connecting', 'disconnected', 'failed', 'new'] -const RTCSdpTypes: RTCSdpType[] = ['answer', 'offer', 'pranswer', 'rollback'] -const RTCIceConnectionStates: RTCIceConnectionState[] = ['checking', 'closed', 'completed', 'connected', 'disconnected', 'failed', 'new'] -const RTCIceGatheringStates: RTCIceGatheringState[] = ['complete', 'gathering', 'new'] -const RTCSignalingStates: RTCSignalingState[] = ['closed', 'have-local-offer', 'have-local-pranswer', 'have-remote-offer', 'have-remote-pranswer', 'stable'] diff --git a/packages/transport-webrtc/src/webrtc/rtc-session-description.ts b/packages/transport-webrtc/src/webrtc/rtc-session-description.ts deleted file mode 100644 index ae498cbdaf..0000000000 --- a/packages/transport-webrtc/src/webrtc/rtc-session-description.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @see https://developer.mozilla.org/docs/Web/API/RTCSessionDescription - */ -export class SessionDescription implements RTCSessionDescription { - readonly sdp: string - readonly type: RTCSdpType - - constructor (init: RTCSessionDescriptionInit) { - this.sdp = init.sdp ?? '' - this.type = init.type - } - - toJSON (): RTCSessionDescriptionInit { - return { - sdp: this.sdp, - type: this.type - } - } -}