From fc5ca0a9046107eea2021ee03ef65b8581dc385d Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Wed, 26 Jun 2024 18:23:29 +0200 Subject: [PATCH 01/50] Extract `createVideoTransceiverConfig` to separate file --- packages/ts-client/src/webrtc/transciever.ts | 41 +++++++++++++++++++ .../ts-client/src/webrtc/webRTCEndpoint.ts | 7 +++- 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 packages/ts-client/src/webrtc/transciever.ts diff --git a/packages/ts-client/src/webrtc/transciever.ts b/packages/ts-client/src/webrtc/transciever.ts new file mode 100644 index 00000000..eff04992 --- /dev/null +++ b/packages/ts-client/src/webrtc/transciever.ts @@ -0,0 +1,41 @@ +import { TrackContext, TrackEncoding } from './types'; +import { simulcastTransceiverConfig } from './const'; +import { applyBandwidthLimitation } from './bandwidth'; + +export const createVideoTransceiverConfig = ( + trackContext: TrackContext, + disabledTrackEncodingsMap: Map, +): RTCRtpTransceiverInit => { + let transceiverConfig: RTCRtpTransceiverInit; + if (trackContext.simulcastConfig!.enabled) { + transceiverConfig = simulcastTransceiverConfig; + const trackActiveEncodings = trackContext.simulcastConfig!.activeEncodings; + const disabledTrackEncodings: TrackEncoding[] = []; + transceiverConfig.sendEncodings?.forEach((encoding) => { + if (trackActiveEncodings.includes(encoding.rid! as TrackEncoding)) { + encoding.active = true; + } else { + disabledTrackEncodings.push(encoding.rid! as TrackEncoding); + } + }); + disabledTrackEncodingsMap.set(trackContext.trackId, disabledTrackEncodings); + } else { + transceiverConfig = { + direction: 'sendonly', + sendEncodings: [ + { + active: true, + }, + ], + streams: trackContext.stream ? [trackContext.stream] : [], + }; + } + + if (trackContext.maxBandwidth && transceiverConfig.sendEncodings) + applyBandwidthLimitation( + transceiverConfig.sendEncodings, + trackContext.maxBandwidth, + ); + + return transceiverConfig; +}; diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index cd1ca7a2..8205471e 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -37,6 +37,7 @@ import { getTrackBitrates, getTrackIdToTrackBitrates, } from './bitrate'; +import { createVideoTransceiverConfig } from './transciever'; /** * Main class that is responsible for connecting to the RTC Engine, sending and receiving media. @@ -549,7 +550,6 @@ export class WebRTCEndpoint< /** * Adds track that will be sent to the RTC Engine. * @param track - Audio or video track e.g. from your microphone or camera. - * @param stream - Stream that this track belongs to. * @param trackMetadata - Any information about this track that other endpoints will * receive in {@link WebRTCEndpointEvents.endpointAdded}. E.g. this can source of the track - whether it's * screensharing, webcam or some other media device. @@ -774,7 +774,10 @@ export class WebRTCEndpoint< if (trackContext.track!.kind === 'audio') { transceiverConfig = this.createAudioTransceiverConfig(trackContext); } else { - transceiverConfig = this.createVideoTransceiverConfig(trackContext); + transceiverConfig = createVideoTransceiverConfig( + trackContext, + this.disabledTrackEncodings, + ); } return transceiverConfig; From 5700fe3b6c15c4a674cec4aa6f987c9c1e1e4772 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Wed, 26 Jun 2024 18:33:33 +0200 Subject: [PATCH 02/50] Extract `createAudioTransceiverConfig` to separate file --- packages/ts-client/src/webrtc/transciever.ts | 9 +++++++++ packages/ts-client/src/webrtc/webRTCEndpoint.ts | 16 +++++----------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/ts-client/src/webrtc/transciever.ts b/packages/ts-client/src/webrtc/transciever.ts index eff04992..aebc8c04 100644 --- a/packages/ts-client/src/webrtc/transciever.ts +++ b/packages/ts-client/src/webrtc/transciever.ts @@ -2,6 +2,15 @@ import { TrackContext, TrackEncoding } from './types'; import { simulcastTransceiverConfig } from './const'; import { applyBandwidthLimitation } from './bandwidth'; +export const createAudioTransceiverConfig = ( + trackContext: TrackContext, +): RTCRtpTransceiverInit => { + return { + direction: 'sendonly', + streams: trackContext.stream ? [trackContext.stream] : [], + }; +}; + export const createVideoTransceiverConfig = ( trackContext: TrackContext, disabledTrackEncodingsMap: Map, diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 8205471e..010823c3 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -37,7 +37,10 @@ import { getTrackBitrates, getTrackIdToTrackBitrates, } from './bitrate'; -import { createVideoTransceiverConfig } from './transciever'; +import { + createAudioTransceiverConfig, + createVideoTransceiverConfig, +} from './transciever'; /** * Main class that is responsible for connecting to the RTC Engine, sending and receiving media. @@ -772,7 +775,7 @@ export class WebRTCEndpoint< let transceiverConfig: RTCRtpTransceiverInit; if (trackContext.track!.kind === 'audio') { - transceiverConfig = this.createAudioTransceiverConfig(trackContext); + transceiverConfig = createAudioTransceiverConfig(trackContext); } else { transceiverConfig = createVideoTransceiverConfig( trackContext, @@ -783,15 +786,6 @@ export class WebRTCEndpoint< return transceiverConfig; } - private createAudioTransceiverConfig( - trackContext: TrackContext, - ): RTCRtpTransceiverInit { - return { - direction: 'sendonly', - streams: trackContext.stream ? [trackContext.stream] : [], - }; - } - private createVideoTransceiverConfig( trackContext: TrackContext, ): RTCRtpTransceiverInit { From 60870c686be82f78f9e72ae120bb72525f71a711 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Wed, 26 Jun 2024 18:34:24 +0200 Subject: [PATCH 03/50] Remove `createVideoTransceiverConfig` private function --- .../ts-client/src/webrtc/webRTCEndpoint.ts | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 010823c3..36b161f6 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -786,47 +786,6 @@ export class WebRTCEndpoint< return transceiverConfig; } - private createVideoTransceiverConfig( - trackContext: TrackContext, - ): RTCRtpTransceiverInit { - let transceiverConfig: RTCRtpTransceiverInit; - if (trackContext.simulcastConfig!.enabled) { - transceiverConfig = simulcastTransceiverConfig; - const trackActiveEncodings = - trackContext.simulcastConfig!.activeEncodings; - const disabledTrackEncodings: TrackEncoding[] = []; - transceiverConfig.sendEncodings?.forEach((encoding) => { - if (trackActiveEncodings.includes(encoding.rid! as TrackEncoding)) { - encoding.active = true; - } else { - disabledTrackEncodings.push(encoding.rid! as TrackEncoding); - } - }); - this.disabledTrackEncodings.set( - trackContext.trackId, - disabledTrackEncodings, - ); - } else { - transceiverConfig = { - direction: 'sendonly', - sendEncodings: [ - { - active: true, - }, - ], - streams: trackContext.stream ? [trackContext.stream] : [], - }; - } - - if (trackContext.maxBandwidth && transceiverConfig.sendEncodings) - applyBandwidthLimitation( - transceiverConfig.sendEncodings, - trackContext.maxBandwidth, - ); - - return transceiverConfig; - } - /** * Replaces a track that is being sent to the RTC Engine. * @param trackId - Audio or video track. From 934e78cad9d03de8c3356e0a1c0dd51bf71a6bf2 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Wed, 26 Jun 2024 18:37:59 +0200 Subject: [PATCH 04/50] Move `createTransceiverConfig` to separate file --- packages/ts-client/src/webrtc/transciever.ts | 22 +++++++++++++-- .../ts-client/src/webrtc/webRTCEndpoint.ts | 27 ++++--------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/packages/ts-client/src/webrtc/transciever.ts b/packages/ts-client/src/webrtc/transciever.ts index aebc8c04..8dad09f3 100644 --- a/packages/ts-client/src/webrtc/transciever.ts +++ b/packages/ts-client/src/webrtc/transciever.ts @@ -2,7 +2,25 @@ import { TrackContext, TrackEncoding } from './types'; import { simulcastTransceiverConfig } from './const'; import { applyBandwidthLimitation } from './bandwidth'; -export const createAudioTransceiverConfig = ( +export const createTransceiverConfig = ( + trackContext: TrackContext, + disabledTrackEncodingsMap: Map, +): RTCRtpTransceiverInit => { + let transceiverConfig: RTCRtpTransceiverInit; + + if (trackContext.track!.kind === 'audio') { + transceiverConfig = createAudioTransceiverConfig(trackContext); + } else { + transceiverConfig = createVideoTransceiverConfig( + trackContext, + disabledTrackEncodingsMap, + ); + } + + return transceiverConfig; +} + +const createAudioTransceiverConfig = ( trackContext: TrackContext, ): RTCRtpTransceiverInit => { return { @@ -11,7 +29,7 @@ export const createAudioTransceiverConfig = ( }; }; -export const createVideoTransceiverConfig = ( +const createVideoTransceiverConfig = ( trackContext: TrackContext, disabledTrackEncodingsMap: Map, ): RTCRtpTransceiverInit => { diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 36b161f6..60fb6cae 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -37,10 +37,7 @@ import { getTrackBitrates, getTrackIdToTrackBitrates, } from './bitrate'; -import { - createAudioTransceiverConfig, - createVideoTransceiverConfig, -} from './transciever'; +import { createTransceiverConfig } from './transciever'; /** * Main class that is responsible for connecting to the RTC Engine, sending and receiving media. @@ -764,28 +761,14 @@ export class WebRTCEndpoint< private addTrackToConnection = ( trackContext: TrackContext, ) => { - const transceiverConfig = this.createTransceiverConfig(trackContext); + const transceiverConfig = createTransceiverConfig( + trackContext, + this.disabledTrackEncodings, + ); const track = trackContext.track!; this.connection!.addTransceiver(track, transceiverConfig); }; - private createTransceiverConfig( - trackContext: TrackContext, - ): RTCRtpTransceiverInit { - let transceiverConfig: RTCRtpTransceiverInit; - - if (trackContext.track!.kind === 'audio') { - transceiverConfig = createAudioTransceiverConfig(trackContext); - } else { - transceiverConfig = createVideoTransceiverConfig( - trackContext, - this.disabledTrackEncodings, - ); - } - - return transceiverConfig; - } - /** * Replaces a track that is being sent to the RTC Engine. * @param trackId - Audio or video track. From a30b60e5915ccb776f369e55afbec77478e84a13 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Wed, 26 Jun 2024 18:41:30 +0200 Subject: [PATCH 05/50] Move `addTrackToConnection` to separate file --- packages/ts-client/src/webrtc/transciever.ts | 15 ++++++++++- .../ts-client/src/webrtc/webRTCEndpoint.ts | 25 ++++++++----------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/packages/ts-client/src/webrtc/transciever.ts b/packages/ts-client/src/webrtc/transciever.ts index 8dad09f3..570d1ef7 100644 --- a/packages/ts-client/src/webrtc/transciever.ts +++ b/packages/ts-client/src/webrtc/transciever.ts @@ -2,7 +2,20 @@ import { TrackContext, TrackEncoding } from './types'; import { simulcastTransceiverConfig } from './const'; import { applyBandwidthLimitation } from './bandwidth'; -export const createTransceiverConfig = ( +export const addTrackToConnection = ( + trackContext: TrackContext, + disabledTrackEncodingsMap: Map, + connection: RTCPeerConnection | undefined, +) => { + const transceiverConfig = createTransceiverConfig( + trackContext, + disabledTrackEncodingsMap, + ); + const track = trackContext.track!; + connection!.addTransceiver(track, transceiverConfig); +}; + +const createTransceiverConfig = ( trackContext: TrackContext, disabledTrackEncodingsMap: Map, ): RTCRtpTransceiverInit => { diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 60fb6cae..a796db53 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -37,7 +37,7 @@ import { getTrackBitrates, getTrackIdToTrackBitrates, } from './bitrate'; -import { createTransceiverConfig } from './transciever'; +import { addTrackToConnection } from './transciever'; /** * Main class that is responsible for connecting to the RTC Engine, sending and receiving media. @@ -736,7 +736,11 @@ export class WebRTCEndpoint< this.localTrackIdToTrack.set(trackId, trackContext); if (this.connection) { - this.addTrackToConnection(trackContext); + addTrackToConnection( + trackContext, + this.disabledTrackEncodings, + this.connection, + ); this.connection .getTransceivers() @@ -758,17 +762,6 @@ export class WebRTCEndpoint< this.sendMediaEvent(mediaEvent); } - private addTrackToConnection = ( - trackContext: TrackContext, - ) => { - const transceiverConfig = createTransceiverConfig( - trackContext, - this.disabledTrackEncodings, - ); - const track = trackContext.track!; - this.connection!.addTransceiver(track, transceiverConfig); - }; - /** * Replaces a track that is being sent to the RTC Engine. * @param trackId - Audio or video track. @@ -1432,7 +1425,11 @@ export class WebRTCEndpoint< this.connection.onsignalingstatechange = this.onSignalingStateChange; Array.from(this.localTrackIdToTrack.values()).forEach((trackContext) => - this.addTrackToConnection(trackContext), + addTrackToConnection( + trackContext, + this.disabledTrackEncodings, + this.connection, + ), ); this.connection From 34d751700a2729176deb9661a639dcdf44168908 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Wed, 26 Jun 2024 18:44:55 +0200 Subject: [PATCH 06/50] Extract `setTransceiverDirection` function --- packages/ts-client/src/webrtc/transciever.ts | 12 ++++++++++++ packages/ts-client/src/webrtc/webRTCEndpoint.ts | 12 ++---------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/ts-client/src/webrtc/transciever.ts b/packages/ts-client/src/webrtc/transciever.ts index 570d1ef7..3a453b56 100644 --- a/packages/ts-client/src/webrtc/transciever.ts +++ b/packages/ts-client/src/webrtc/transciever.ts @@ -2,6 +2,18 @@ import { TrackContext, TrackEncoding } from './types'; import { simulcastTransceiverConfig } from './const'; import { applyBandwidthLimitation } from './bandwidth'; +export const setTransceiverDirection = (connection: RTCPeerConnection) => { + connection + .getTransceivers() + .forEach( + (transceiver) => + (transceiver.direction = + transceiver.direction === 'sendrecv' + ? 'sendonly' + : transceiver.direction), + ); +}; + export const addTrackToConnection = ( trackContext: TrackContext, disabledTrackEncodingsMap: Map, diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index a796db53..07f5145d 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -37,7 +37,7 @@ import { getTrackBitrates, getTrackIdToTrackBitrates, } from './bitrate'; -import { addTrackToConnection } from './transciever'; +import { addTrackToConnection, setTransceiverDirection } from './transciever'; /** * Main class that is responsible for connecting to the RTC Engine, sending and receiving media. @@ -742,15 +742,7 @@ export class WebRTCEndpoint< this.connection, ); - this.connection - .getTransceivers() - .forEach( - (transceiver) => - (transceiver.direction = - transceiver.direction === 'sendrecv' - ? 'sendonly' - : transceiver.direction), - ); + setTransceiverDirection(this.connection); } this.trackIdToSender.set(trackId, { From eb38b61c3c7aadac1f0c18b006b5b1317bc846c3 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Wed, 26 Jun 2024 18:48:48 +0200 Subject: [PATCH 07/50] Move `addTransceiversIfNeeded` to a separate file --- packages/ts-client/src/webrtc/transciever.ts | 29 +++++++++++++++- .../ts-client/src/webrtc/webRTCEndpoint.ts | 33 ++++--------------- 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/packages/ts-client/src/webrtc/transciever.ts b/packages/ts-client/src/webrtc/transciever.ts index 3a453b56..65c3ca1d 100644 --- a/packages/ts-client/src/webrtc/transciever.ts +++ b/packages/ts-client/src/webrtc/transciever.ts @@ -2,6 +2,33 @@ import { TrackContext, TrackEncoding } from './types'; import { simulcastTransceiverConfig } from './const'; import { applyBandwidthLimitation } from './bandwidth'; +export const addTransceiversIfNeeded = ( + connection: RTCPeerConnection | undefined, + serverTracks: Map, +) => { + const recvTransceivers = connection! + .getTransceivers() + .filter((elem) => elem.direction === 'recvonly'); + let toAdd: string[] = []; + + const getNeededTransceiversTypes = (type: string): string[] => { + let typeNumber = serverTracks.get(type); + typeNumber = typeNumber !== undefined ? typeNumber : 0; + const typeTransceiversNumber = recvTransceivers.filter( + (elem) => elem.receiver.track.kind === type, + ).length; + return Array(typeNumber - typeTransceiversNumber).fill(type); + }; + + const audio = getNeededTransceiversTypes('audio'); + const video = getNeededTransceiversTypes('video'); + toAdd = toAdd.concat(audio); + toAdd = toAdd.concat(video); + + for (const kind of toAdd) + connection?.addTransceiver(kind, { direction: 'recvonly' }); +}; + export const setTransceiverDirection = (connection: RTCPeerConnection) => { connection .getTransceivers() @@ -43,7 +70,7 @@ const createTransceiverConfig = ( } return transceiverConfig; -} +}; const createAudioTransceiverConfig = ( trackContext: TrackContext, diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 07f5145d..2d023fb9 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -9,7 +9,6 @@ import { import { v4 as uuidv4 } from 'uuid'; import EventEmitter from 'events'; import TypedEmitter from 'typed-emitter'; -import { simulcastTransceiverConfig } from './const'; import { AddTrackCommand, Command, @@ -37,7 +36,11 @@ import { getTrackBitrates, getTrackIdToTrackBitrates, } from './bitrate'; -import { addTrackToConnection, setTransceiverDirection } from './transciever'; +import { + addTrackToConnection, + addTransceiversIfNeeded, + setTransceiverDirection, +} from './transciever'; /** * Main class that is responsible for connecting to the RTC Engine, sending and receiving media. @@ -1313,30 +1316,6 @@ export class WebRTCEndpoint< } }; - private addTransceiversIfNeeded = (serverTracks: Map) => { - const recvTransceivers = this.connection!.getTransceivers().filter( - (elem) => elem.direction === 'recvonly', - ); - let toAdd: string[] = []; - - const getNeededTransceiversTypes = (type: string): string[] => { - let typeNumber = serverTracks.get(type); - typeNumber = typeNumber !== undefined ? typeNumber : 0; - const typeTransceiversNumber = recvTransceivers.filter( - (elem) => elem.receiver.track.kind === type, - ).length; - return Array(typeNumber - typeTransceiversNumber).fill(type); - }; - - const audio = getNeededTransceiversTypes('audio'); - const video = getNeededTransceiversTypes('video'); - toAdd = toAdd.concat(audio); - toAdd = toAdd.concat(video); - - for (const kind of toAdd) - this.connection?.addTransceiver(kind, { direction: 'recvonly' }); - }; - private async createAndSendOffer() { const connection = this.connection; if (!connection) return; @@ -1441,7 +1420,7 @@ export class WebRTCEndpoint< Object.entries(offerData.data.tracksTypes), ); - this.addTransceiversIfNeeded(tracks); + addTransceiversIfNeeded(this.connection, tracks); await this.createAndSendOffer(); }; From 0121d8f63de27320f4664b75177cebf7fad3cb3e Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Thu, 27 Jun 2024 09:29:25 +0200 Subject: [PATCH 08/50] Extract `setTransceiversToReadOnly` function to a separate file --- packages/ts-client/src/webrtc/transciever.ts | 6 ++++++ packages/ts-client/src/webrtc/webRTCEndpoint.ts | 5 ++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/ts-client/src/webrtc/transciever.ts b/packages/ts-client/src/webrtc/transciever.ts index 65c3ca1d..6b3e9438 100644 --- a/packages/ts-client/src/webrtc/transciever.ts +++ b/packages/ts-client/src/webrtc/transciever.ts @@ -118,3 +118,9 @@ const createVideoTransceiverConfig = ( return transceiverConfig; }; + +export const setTransceiversToReadOnly = (connection: RTCPeerConnection) => { + connection + .getTransceivers() + .forEach((transceiver) => (transceiver.direction = 'sendonly')); +}; diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 2d023fb9..84facad5 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -40,6 +40,7 @@ import { addTrackToConnection, addTransceiversIfNeeded, setTransceiverDirection, + setTransceiversToReadOnly, } from './transciever'; /** @@ -1403,9 +1404,7 @@ export class WebRTCEndpoint< ), ); - this.connection - .getTransceivers() - .forEach((transceiver) => (transceiver.direction = 'sendonly')); + setTransceiversToReadOnly(this.connection); } else { this.connection.restartIce(); } From c323afd03178691aa1f4eb45b7a6f8a145a5206f Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Thu, 27 Jun 2024 09:47:37 +0200 Subject: [PATCH 09/50] Move `getMidToTrackId` function to a separate file --- packages/ts-client/src/webrtc/transciever.ts | 27 ++++++++++++++++++- .../ts-client/src/webrtc/webRTCEndpoint.ts | 23 ++++------------ 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/packages/ts-client/src/webrtc/transciever.ts b/packages/ts-client/src/webrtc/transciever.ts index 6b3e9438..9768f6b1 100644 --- a/packages/ts-client/src/webrtc/transciever.ts +++ b/packages/ts-client/src/webrtc/transciever.ts @@ -1,6 +1,7 @@ -import { TrackContext, TrackEncoding } from './types'; +import { RemoteTrackId, TrackContext, TrackEncoding } from './types'; import { simulcastTransceiverConfig } from './const'; import { applyBandwidthLimitation } from './bandwidth'; +import { TrackContextImpl } from './internal'; export const addTransceiversIfNeeded = ( connection: RTCPeerConnection | undefined, @@ -124,3 +125,27 @@ export const setTransceiversToReadOnly = (connection: RTCPeerConnection) => { .getTransceivers() .forEach((transceiver) => (transceiver.direction = 'sendonly')); }; + +export const getMidToTrackId = ( + connection: RTCPeerConnection | undefined, + localTrackIdToTrack: Map< + RemoteTrackId, + TrackContextImpl + >, +): Record | null => { + const localTrackMidToTrackId: Record = {}; + + if (!connection) return null; + connection.getTransceivers().forEach((transceiver) => { + const localTrackId = transceiver.sender.track?.id; + const mid = transceiver.mid; + if (localTrackId && mid) { + const trackContext = Array.from(localTrackIdToTrack.values()).find( + (trackContext) => trackContext!.track!.id === localTrackId, + )!; + localTrackMidToTrackId[mid] = trackContext.trackId; + } + }); + + return localTrackMidToTrackId; +}; diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 84facad5..dd9a2b8c 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -39,6 +39,7 @@ import { import { addTrackToConnection, addTransceiversIfNeeded, + getMidToTrackId, setTransceiverDirection, setTransceiversToReadOnly, } from './transciever'; @@ -1239,23 +1240,6 @@ export class WebRTCEndpoint< } }; - private getMidToTrackId = (): Record | null => { - const localTrackMidToTrackId: Record = {}; - - if (!this.connection) return null; - this.connection.getTransceivers().forEach((transceiver) => { - const localTrackId = transceiver.sender.track?.id; - const mid = transceiver.mid; - if (localTrackId && mid) { - const trackContext = Array.from(this.localTrackIdToTrack.values()).find( - (trackContext) => trackContext!.track!.id === localTrackId, - )!; - localTrackMidToTrackId[mid] = trackContext.trackId; - } - }); - return localTrackMidToTrackId; - }; - /** * Disconnects from the room. This function should be called when user disconnects from the room * in a clean way e.g. by clicking a dedicated, custom button `disconnect`. @@ -1345,7 +1329,10 @@ export class WebRTCEndpoint< this.localTrackIdToTrack, this.localEndpoint.tracks, ), - midToTrackId: this.getMidToTrackId(), + midToTrackId: getMidToTrackId( + this.connection, + this.localTrackIdToTrack, + ), }, }); this.sendMediaEvent(mediaEvent); From 83faa6531fb30688296e3093644e0077564aff08 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Thu, 27 Jun 2024 10:18:16 +0200 Subject: [PATCH 10/50] Extract `findSenderByTrack` function to a separate file --- packages/ts-client/src/webrtc/RTCPeerConnectionUtils.ts | 6 ++++++ packages/ts-client/src/webrtc/webRTCEndpoint.ts | 7 ++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/ts-client/src/webrtc/RTCPeerConnectionUtils.ts b/packages/ts-client/src/webrtc/RTCPeerConnectionUtils.ts index db46e95d..15c87fcc 100644 --- a/packages/ts-client/src/webrtc/RTCPeerConnectionUtils.ts +++ b/packages/ts-client/src/webrtc/RTCPeerConnectionUtils.ts @@ -5,3 +5,9 @@ export const findSender = ( connection! .getSenders() .find((sender) => sender.track && sender!.track!.id === trackId)!; + +export const findSenderByTrack = ( + connection: RTCPeerConnection | undefined, + track: MediaStreamTrack | null | undefined, +): RTCRtpSender | undefined => + connection?.getSenders().filter((sender) => sender.track === track)[0]; diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index dd9a2b8c..c602732c 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -43,6 +43,7 @@ import { setTransceiverDirection, setTransceiversToReadOnly, } from './transciever'; +import { findSender, findSenderByTrack } from "./RTCPeerConnectionUtils"; /** * Main class that is responsible for connecting to the RTC Engine, sending and receiving media. @@ -1141,9 +1142,9 @@ export class WebRTCEndpoint< public disableTrackEncoding(trackId: string, encoding: TrackEncoding) { const track = this.localTrackIdToTrack.get(trackId)?.track; this.disabledTrackEncodings.get(trackId)!.push(encoding); - const sender = this.connection - ?.getSenders() - .filter((sender) => sender.track === track)[0]; + + const sender = findSenderByTrack(this.connection, track) + const params = sender?.getParameters(); params!.encodings.filter((en) => en.rid == encoding)[0].active = false; sender?.setParameters(params!); From 1d30724673ba8fa89d9eeb854bd4a617bed734d4 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Thu, 27 Jun 2024 11:45:59 +0200 Subject: [PATCH 11/50] Extract `isTrackInUse` function to a separate file --- packages/ts-client/src/webrtc/RTCPeerConnectionUtils.ts | 5 +++++ packages/ts-client/src/webrtc/webRTCEndpoint.ts | 8 +++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/ts-client/src/webrtc/RTCPeerConnectionUtils.ts b/packages/ts-client/src/webrtc/RTCPeerConnectionUtils.ts index 15c87fcc..7f58b262 100644 --- a/packages/ts-client/src/webrtc/RTCPeerConnectionUtils.ts +++ b/packages/ts-client/src/webrtc/RTCPeerConnectionUtils.ts @@ -11,3 +11,8 @@ export const findSenderByTrack = ( track: MediaStreamTrack | null | undefined, ): RTCRtpSender | undefined => connection?.getSenders().filter((sender) => sender.track === track)[0]; + +export const isTrackInUse = ( + connection: RTCPeerConnection | undefined, + track: MediaStreamTrack, +) => connection?.getSenders().some((val) => val.track === track); diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index c602732c..056a00f4 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -43,7 +43,7 @@ import { setTransceiverDirection, setTransceiversToReadOnly, } from './transciever'; -import { findSender, findSenderByTrack } from "./RTCPeerConnectionUtils"; +import { findSenderByTrack, isTrackInUse } from './RTCPeerConnectionUtils'; /** * Main class that is responsible for connecting to the RTC Engine, sending and receiving media. @@ -700,9 +700,7 @@ export class WebRTCEndpoint< trackMetadata, trackId, } = addTrackCommand; - const isUsedTrack = this.connection - ?.getSenders() - .some((val) => val.track === track); + const isUsedTrack = isTrackInUse(this.connection, track); let error; if (isUsedTrack) { @@ -1143,7 +1141,7 @@ export class WebRTCEndpoint< const track = this.localTrackIdToTrack.get(trackId)?.track; this.disabledTrackEncodings.get(trackId)!.push(encoding); - const sender = findSenderByTrack(this.connection, track) + const sender = findSenderByTrack(this.connection, track); const params = sender?.getParameters(); params!.encodings.filter((en) => en.rid == encoding)[0].active = false; From 0f95ae50021baa44d2ccb3b95e72f8fddee6be77 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Thu, 27 Jun 2024 11:47:50 +0200 Subject: [PATCH 12/50] Use already existing `findSenderByTrack` function --- packages/ts-client/src/webrtc/webRTCEndpoint.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 056a00f4..1de1fe9e 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -1109,9 +1109,7 @@ export class WebRTCEndpoint< .get(trackId) ?.filter((en) => en !== encoding)!; this.disabledTrackEncodings.set(trackId, newDisabledTrackEncodings); - const sender = this.connection - ?.getSenders() - .filter((sender) => sender.track === track)[0]; + const sender = findSenderByTrack(this.connection, track) const params = sender?.getParameters(); params!.encodings.filter((en) => en.rid == encoding)[0].active = true; sender?.setParameters(params!); From 7d53c211ae8904efce54e8e408f1bfe51906aa11 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Thu, 27 Jun 2024 11:51:21 +0200 Subject: [PATCH 13/50] Use already existing `findSender` function --- .../ts-client/src/webrtc/webRTCEndpoint.ts | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 1de1fe9e..af62abde 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -43,7 +43,11 @@ import { setTransceiverDirection, setTransceiversToReadOnly, } from './transciever'; -import { findSenderByTrack, isTrackInUse } from './RTCPeerConnectionUtils'; +import { + findSender, + findSenderByTrack, + isTrackInUse, +} from './RTCPeerConnectionUtils'; /** * Main class that is responsible for connecting to the RTC Engine, sending and receiving media. @@ -911,7 +915,7 @@ export class WebRTCEndpoint< return Promise.reject(`Track '${trackId}' doesn't exist`); } - const sender = this.findSender(trackContext.track!.id); + const sender = findSender(this.connection, trackContext.track!.id); const parameters = sender.getParameters(); if (parameters.encodings.length === 0) { @@ -958,7 +962,7 @@ export class WebRTCEndpoint< return Promise.reject(`Track '${trackId}' doesn't exist`); } - const sender = this.findSender(trackContext.track!.id); + const sender = findSender(this.connection, trackContext.track!.id); const parameters = sender.getParameters(); const encoding = parameters.encodings.find( (encoding) => encoding.rid === rid, @@ -1041,7 +1045,7 @@ export class WebRTCEndpoint< private removeTrackHandler(command: RemoveTrackCommand) { const { trackId } = command; const trackContext = this.localTrackIdToTrack.get(trackId)!; - const sender = this.findSender(trackContext.track!.id); + const sender = findSender(this.connection, trackContext.track!.id); this.ongoingRenegotiation = true; @@ -1109,7 +1113,7 @@ export class WebRTCEndpoint< .get(trackId) ?.filter((en) => en !== encoding)!; this.disabledTrackEncodings.set(trackId, newDisabledTrackEncodings); - const sender = findSenderByTrack(this.connection, track) + const sender = findSenderByTrack(this.connection, track); const params = sender?.getParameters(); params!.encodings.filter((en) => en.rid == encoding)[0].active = true; sender?.setParameters(params!); @@ -1156,12 +1160,6 @@ export class WebRTCEndpoint< }); } - private findSender(trackId: string): RTCRtpSender { - return this.connection!.getSenders().find( - (sender) => sender.track && sender!.track!.id === trackId, - )!; - } - /** * Updates the metadata for the current endpoint. * @param metadata - Data about this endpoint that other endpoints will receive upon being added. @@ -1395,7 +1393,7 @@ export class WebRTCEndpoint< this.trackIdToSender.forEach((sth) => { if (sth.localTrackId) { - sth.sender = this.findSender(sth.localTrackId); + sth.sender = findSender(this.connection, sth.localTrackId); } }); From b8873866493a3b75fbac6003889b812b17ab4d9a Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Thu, 27 Jun 2024 14:23:14 +0200 Subject: [PATCH 14/50] Extract `createSdpOfferEvent` to a separte file --- packages/ts-client/src/webrtc/sdpEvents.ts | 38 +++++++++++++++++++ .../ts-client/src/webrtc/webRTCEndpoint.ts | 38 ++++--------------- 2 files changed, 45 insertions(+), 31 deletions(-) create mode 100644 packages/ts-client/src/webrtc/sdpEvents.ts diff --git a/packages/ts-client/src/webrtc/sdpEvents.ts b/packages/ts-client/src/webrtc/sdpEvents.ts new file mode 100644 index 00000000..2ea4e091 --- /dev/null +++ b/packages/ts-client/src/webrtc/sdpEvents.ts @@ -0,0 +1,38 @@ +import { generateCustomEvent } from './mediaEvent'; +import { getTrackIdToTrackBitrates } from './bitrate'; +import { getMidToTrackId } from './transciever'; +import { RemoteTrackId, TrackContext } from './types'; +import { TrackContextImpl } from './internal'; + +export const createSdpOfferEvent = ( + offer: RTCSessionDescriptionInit, + connection: RTCPeerConnection | undefined, + localTrackIdToTrack: Map< + RemoteTrackId, + TrackContextImpl + >, + tracks: Map>, +) => + generateCustomEvent({ + type: 'sdpOffer', + data: { + sdpOffer: offer, + trackIdToTrackMetadata: getTrackIdToMetadata(tracks), + trackIdToTrackBitrates: getTrackIdToTrackBitrates( + connection, + localTrackIdToTrack, + tracks, + ), + midToTrackId: getMidToTrackId(connection, localTrackIdToTrack), + }, + }); + +const getTrackIdToMetadata = ( + tracks: Map>, +): Record => { + const trackIdToMetadata: Record = {}; + Array.from(tracks.entries()).forEach(([trackId, { metadata }]) => { + trackIdToMetadata[trackId] = metadata; + }); + return trackIdToMetadata; +}; diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index af62abde..52efccf2 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -34,12 +34,10 @@ import { applyBandwidthLimitation } from './bandwidth'; import { createTrackVariantBitratesEvent, getTrackBitrates, - getTrackIdToTrackBitrates, } from './bitrate'; import { addTrackToConnection, addTransceiversIfNeeded, - getMidToTrackId, setTransceiverDirection, setTransceiversToReadOnly, } from './transciever'; @@ -48,6 +46,7 @@ import { findSenderByTrack, isTrackInUse, } from './RTCPeerConnectionUtils'; +import { createSdpOfferEvent } from './sdpEvents'; /** * Main class that is responsible for connecting to the RTC Engine, sending and receiving media. @@ -1314,22 +1313,12 @@ export class WebRTCEndpoint< return; } - const mediaEvent = generateCustomEvent({ - type: 'sdpOffer', - data: { - sdpOffer: offer, - trackIdToTrackMetadata: this.getTrackIdToMetadata(), - trackIdToTrackBitrates: getTrackIdToTrackBitrates( - this.connection, - this.localTrackIdToTrack, - this.localEndpoint.tracks, - ), - midToTrackId: getMidToTrackId( - this.connection, - this.localTrackIdToTrack, - ), - }, - }); + const mediaEvent = createSdpOfferEvent( + offer, + this.connection, + this.localTrackIdToTrack, + this.localEndpoint.tracks, + ); this.sendMediaEvent(mediaEvent); for (const track of this.localTrackIdToTrack.values()) { @@ -1340,19 +1329,6 @@ export class WebRTCEndpoint< } } - private getTrackIdToMetadata = (): Record< - string, - TrackMetadata | undefined - > => { - const trackIdToMetadata: Record = {}; - Array.from(this.localEndpoint.tracks.entries()).forEach( - ([trackId, { metadata }]) => { - trackIdToMetadata[trackId] = metadata; - }, - ); - return trackIdToMetadata; - }; - private checkIfTrackBelongToEndpoint = ( trackId: string, endpoint: EndpointWithTrackContext, From 6be7d60aae5d189841bf51576c9113ca0c3855d3 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Fri, 5 Jul 2024 16:07:30 +0200 Subject: [PATCH 15/50] Merge main --- .eslintrc.js | 28 + .github/dependabot.yml | 6 +- .github/workflows/node.yaml | 16 +- .github/workflows/playwright.yaml | 18 +- .gitignore | 3 + CONTRIBUTING.md | 90 -- README.md | 97 ++ e2e-tests/react-client/app/.gitignore | 1 + e2e-tests/react-client/app/package.json | 19 + .../react-client/app}/playwright.config.ts | 49 +- .../react-client/docker-compose-test.yaml | 2 +- .../react-client/scenarios}/fishjam.spec.ts | 0 .../react-client/scenarios}/utils.ts | 0 .../react-client/setup}/globalSetupState.ts | 0 .../react-client}/setup/setupFishjam.ts | 12 +- .../react-client/setup}/teardownFishjam.ts | 0 .../ts-client/app/.gitignore | 0 .../ts-client/app/compile_proto.sh | 0 .../ts-client/app/index.html | 0 .../ts-client/app/package.json | 22 +- e2e-tests/ts-client/app/playwright.config.ts | 73 + .../ts-client/app/src/App.tsx | 165 ++- .../ts-client/app/src/MockComponent.tsx | 49 +- .../ts-client/app/src/MuteTrackTest.tsx | 59 +- .../ts-client/app/src/VideoPlayer.tsx | 4 +- .../app/src/VideoPlayerWithDetector.tsx | 44 +- e2e-tests/ts-client/app/src/main.tsx | 9 + .../ts-client/app/src/mocks.ts | 10 +- .../ts-client/app/src/vite-env.d.ts | 0 e2e-tests/ts-client/app/tsconfig.json | 18 + .../ts-client/app/tsconfig.node.json | 0 .../ts-client/app/vite.config.ts | 4 +- .../ts-client/docker-compose-test.yaml | 2 +- .../ts-client/scenarios/basic.spec.ts | 0 .../scenarios/metadataParsing.spec.ts | 0 .../ts-client/scenarios/raceCondition.spec.ts | 0 .../ts-client/scenarios/replaceTrack.spec.ts | 0 .../ts-client/scenarios/utils.ts | 0 .../ts-client/setup/globalSetupState.ts | 0 e2e-tests/ts-client/setup/setupFishjam.ts | 23 + .../ts-client/setup/teardownFishjam.ts | 0 e2e_tests/react-client/setupFishjam.ts | 19 - e2e_tests/ts-client/app/src/main.tsx | 9 - e2e_tests/ts-client/app/tsconfig.json | 7 - .../minimal-react/.eslintignore copy | 2 + examples/react-client/minimal-react/.eslintrc | 4 + .../react-client/minimal-react/package.json | 12 +- .../minimal-react/src/components/App.tsx | 32 +- .../src/components/VideoPlayer.tsx | 4 +- .../minimal-react/src/components/client.ts | 13 +- examples/react-client/readme.md | 10 +- .../.eslintignore copy | 2 + .../use-camera-and-microphone/.eslintrc | 4 + .../use-camera-and-microphone/README.md | 12 +- .../use-camera-and-microphone/package.json | 14 +- .../src/AdditionalControls.tsx | 28 +- .../src/AudioVisualizer.tsx | 12 +- .../src/DeviceControls.tsx | 22 +- .../src/DeviceSelector.tsx | 15 +- .../src/MainControls.tsx | 161 +- .../use-camera-and-microphone/src/Radio.tsx | 4 +- .../src/fishjamSetup.tsx | 15 +- examples/ts-client/README.md | 10 +- examples/ts-client/minimal/.eslintignore | 2 + examples/ts-client/minimal/.eslintrc | 4 + examples/ts-client/minimal/package.json | 10 +- examples/ts-client/minimal/src/main.ts | 28 +- examples/ts-client/minimal/vite.config.ts | 6 +- examples/ts-client/simple-app/.eslintignore | 2 + examples/ts-client/simple-app/.eslintrc | 4 + examples/ts-client/simple-app/index.html | 48 +- examples/ts-client/simple-app/package.json | 15 +- .../simple-app/src/createMockStream.ts | 8 +- examples/ts-client/simple-app/src/main.ts | 364 ++--- .../ts-client/simple-app/tailwind.config.cjs | 4 +- examples/ts-client/simple-app/vite.config.ts | 6 +- package.json | 20 +- packages/react-client/.eslintrc | 28 +- packages/react-client/package.json | 48 +- packages/react-client/readme.md | 55 +- packages/react-client/src/DeviceManager.ts | 22 +- .../react-client/src/ScreenShareManager.ts | 9 +- packages/react-client/src/types.ts | 2 - packages/react-client/typedoc.json | 5 - packages/ts-client/.eslintrc | 24 +- packages/ts-client/.husky/pre-commit | 2 +- packages/ts-client/README.md | 49 +- packages/ts-client/package.json | 53 +- packages/ts-client/src/FishjamClient.ts | 13 +- packages/ts-client/src/reconnection.ts | 4 +- packages/ts-client/src/webrtc/.eslintrc | 5 + packages/ts-client/src/webrtc/bandwidth.ts | 2 +- packages/ts-client/src/webrtc/bitrate.ts | 28 +- packages/ts-client/src/webrtc/commands.ts | 4 +- packages/ts-client/src/webrtc/const.ts | 3 - packages/ts-client/src/webrtc/index.ts | 1 + packages/ts-client/src/webrtc/internal.ts | 9 +- packages/ts-client/src/webrtc/sdpEvents.ts | 18 +- packages/ts-client/src/webrtc/transceivers.ts | 0 packages/ts-client/src/webrtc/transciever.ts | 93 +- packages/ts-client/src/webrtc/types.ts | 6 +- .../src/webrtc/voiceActivityDetection.ts | 6 +- .../ts-client/src/webrtc/webRTCEndpoint.ts | 40 +- packages/ts-client/tests/.eslintrc | 5 + .../tests/events/connectedEvent.test.ts | 3 +- .../tests/events/trackAddedEvent.test.ts | 2 +- packages/ts-client/tests/fixtures.ts | 30 +- packages/ts-client/tests/utils.ts | 2 +- yarn.lock | 1306 ++++++++++++++--- 109 files changed, 2431 insertions(+), 1201 deletions(-) create mode 100644 .eslintrc.js delete mode 100644 CONTRIBUTING.md create mode 100644 README.md create mode 100644 e2e-tests/react-client/app/.gitignore create mode 100644 e2e-tests/react-client/app/package.json rename {packages/ts-client => e2e-tests/react-client/app}/playwright.config.ts (63%) rename {e2e_tests => e2e-tests}/react-client/docker-compose-test.yaml (91%) rename {e2e_tests/react-client => e2e-tests/react-client/scenarios}/fishjam.spec.ts (100%) rename {e2e_tests/react-client => e2e-tests/react-client/scenarios}/utils.ts (100%) rename {e2e_tests/react-client => e2e-tests/react-client/setup}/globalSetupState.ts (100%) rename {e2e_tests/ts-client => e2e-tests/react-client}/setup/setupFishjam.ts (67%) rename {e2e_tests/react-client => e2e-tests/react-client/setup}/teardownFishjam.ts (100%) rename {e2e_tests => e2e-tests}/ts-client/app/.gitignore (100%) rename {e2e_tests => e2e-tests}/ts-client/app/compile_proto.sh (100%) rename {e2e_tests => e2e-tests}/ts-client/app/index.html (100%) rename {e2e_tests => e2e-tests}/ts-client/app/package.json (50%) create mode 100644 e2e-tests/ts-client/app/playwright.config.ts rename {e2e_tests => e2e-tests}/ts-client/app/src/App.tsx (64%) rename {e2e_tests => e2e-tests}/ts-client/app/src/MockComponent.tsx (73%) rename {e2e_tests => e2e-tests}/ts-client/app/src/MuteTrackTest.tsx (64%) rename {e2e_tests => e2e-tests}/ts-client/app/src/VideoPlayer.tsx (84%) rename {e2e_tests => e2e-tests}/ts-client/app/src/VideoPlayerWithDetector.tsx (62%) create mode 100644 e2e-tests/ts-client/app/src/main.tsx rename {e2e_tests => e2e-tests}/ts-client/app/src/mocks.ts (90%) rename {e2e_tests => e2e-tests}/ts-client/app/src/vite-env.d.ts (100%) create mode 100644 e2e-tests/ts-client/app/tsconfig.json rename {e2e_tests => e2e-tests}/ts-client/app/tsconfig.node.json (100%) rename {e2e_tests => e2e-tests}/ts-client/app/vite.config.ts (52%) rename {e2e_tests => e2e-tests}/ts-client/docker-compose-test.yaml (91%) rename {e2e_tests => e2e-tests}/ts-client/scenarios/basic.spec.ts (100%) rename {e2e_tests => e2e-tests}/ts-client/scenarios/metadataParsing.spec.ts (100%) rename {e2e_tests => e2e-tests}/ts-client/scenarios/raceCondition.spec.ts (100%) rename {e2e_tests => e2e-tests}/ts-client/scenarios/replaceTrack.spec.ts (100%) rename {e2e_tests => e2e-tests}/ts-client/scenarios/utils.ts (100%) rename {e2e_tests => e2e-tests}/ts-client/setup/globalSetupState.ts (100%) create mode 100644 e2e-tests/ts-client/setup/setupFishjam.ts rename {e2e_tests => e2e-tests}/ts-client/setup/teardownFishjam.ts (100%) delete mode 100644 e2e_tests/react-client/setupFishjam.ts delete mode 100644 e2e_tests/ts-client/app/src/main.tsx delete mode 100644 e2e_tests/ts-client/app/tsconfig.json create mode 100644 examples/react-client/minimal-react/.eslintignore copy create mode 100644 examples/react-client/minimal-react/.eslintrc create mode 100644 examples/react-client/use-camera-and-microphone/.eslintignore copy create mode 100644 examples/react-client/use-camera-and-microphone/.eslintrc create mode 100644 examples/ts-client/minimal/.eslintignore create mode 100644 examples/ts-client/minimal/.eslintrc create mode 100644 examples/ts-client/simple-app/.eslintignore create mode 100644 examples/ts-client/simple-app/.eslintrc create mode 100644 packages/ts-client/src/webrtc/.eslintrc create mode 100644 packages/ts-client/src/webrtc/transceivers.ts create mode 100644 packages/ts-client/tests/.eslintrc diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..4942bd08 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,28 @@ +module.exports = { + root: true, + parser: "@typescript-eslint/parser", + plugins: ["@typescript-eslint", "react-hooks", "react-refresh"], + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "prettier", + ], + rules: { + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/consistent-type-imports": "error", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + "no-console": ["error", { allow: ["warn", "error"] }], + "react-hooks/exhaustive-deps": "error", + "react-hooks/rules-of-hooks": "error", + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], + }, +}; diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b4179835..e5884cc1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,4 +8,8 @@ updates: - package-ecosystem: "npm" # See documentation for possible values directory: "/" # Location of package manifests schedule: - interval: "daily" + interval: "weekly" + day: "tuesday" +groups: + development-dependencies: + dependency-type: "development" diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index cc120d49..d6b236e0 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -9,12 +9,12 @@ on: jobs: build_and_test_lib: - name: Build and test + name: Build, check formatting, check linting runs-on: ubuntu-latest strategy: matrix: - node-version: [18.x] + node-version: [20.x] steps: - uses: actions/checkout@v4 @@ -26,16 +26,16 @@ jobs: cache: 'npm' - name: Install dependencies โฌ‡๏ธ - run: yarn - - - name: Build ๐Ÿ“ฆ - run: npm run build + run: yarn --immutable - - name: Run types ๐Ÿ‘ฎ - run: yarn tsc + - name: Build ๐Ÿ“ฆ + run: yarn build - name: Check formatting ๐ŸŽจ run: yarn format:check - name: Run linter ๐Ÿ‘ฎ run: yarn lint:check + + - name: Run typecheck ๐Ÿš“ + run: yarn typecheck diff --git a/.github/workflows/playwright.yaml b/.github/workflows/playwright.yaml index eac6b12a..601fb701 100644 --- a/.github/workflows/playwright.yaml +++ b/.github/workflows/playwright.yaml @@ -17,20 +17,20 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 + cache: npm - - name: Install SDK - run: npm ci + - name: Install dependencies โฌ‡๏ธ + run: yarn --immutable - - name: Install E2E dependencies - run: npm ci - working-directory: e2e/app + - name: Build ๐Ÿ“ฆ + run: yarn build - - name: Install Playwright Browsers + - name: Install Playwright Browsers ๐Ÿงญ run: npx playwright install --with-deps - - name: Run Playwright tests - run: npm run test:e2e + - name: Run Playwright tests ๐Ÿงช + run: yarn test:e2e - uses: actions/upload-artifact@v4 if: always() diff --git a/.gitignore b/.gitignore index accd19ad..ad241985 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ docs # MacOS .DS_Store + +# Playwright +playwright-report diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 9c4f733a..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,90 +0,0 @@ -# Contributing - -Contributions are always welcome, no matter how large or small! - -We aspire to build a community that is friendly and respectful to each other. -Please adhere to this spirit in all your interactions within the project. - -## Development Workflow - -To get started with the project, run `npm install` in the root directory to -install the required dependencies: - -```sh -npm install -``` - -Then, setup [Husky](https://typicode.github.io/husky/) to run pre-commit hooks: - -```sh -npx husky -``` - -Ensure your code passes TypeScript, ESLint and formatter checks by running the -following commands: - -```sh -npm run build:check -npm run lint:check -npm run format:check -``` - -To lint and format your code, use the following commands: - -```sh -npm run lint -npm run format -``` - -For other scripts, refer to [package.json](./package.json). - -### Code Checking - -We utilize [TypeScript](https://www.typescriptlang.org/) for type checking, -[ESLint](https://eslint.org/) for linting, and [Prettier](https://prettier.io/) -for formatting the code. - -### E2E Tests - -We employ [Playwright](https://playwright.dev/) to run End-to-End (E2E) tests. - -You can use the `npm run test:e2e` command to run these tests. However, you may -need to first install the browsers using this command: -`npx playwright install --with-deps`. - -E2E tests initiate a Fishjam instance using Docker and -[Testcontainers](https://node.testcontainers.org/). - -#### Colima - -If you are a [Colima](https://github.com/abiosoft/colima) user, you will need to -run the following commands first: - -```bash -export DOCKER_HOST=unix://${HOME}/.colima/default/docker.sock -export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock -``` - -To learn about known issues, refer to the -[Testcontainers' documentation](https://node.testcontainers.org/supported-container-runtimes/#known-issues_1). - -### Submitting a Pull Request - -> **Working on your first pull request?** Get started with this _free_ series: -> [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). - -When you're sending a pull request: - -- Focus on one change and try to keep pull requests small. -- Make sure that formatter, linter and test checks are passing. -- Review the documentation to make sure it looks good. -- Follow the pull request template when opening a pull request. -- If your pull request changes the API or implementation, first discuss the - changes with the maintainers by opening an issue. - -## Releasing New Versions - -To release a new version of the package, navigate to `Actions` > -`Release package` workflow and trigger it with the chosen release type. The -workflow will update the package version in `package.json`, release the package -to NPM, create a new git tag and a GitHub release. diff --git a/README.md b/README.md new file mode 100644 index 00000000..5289d0d2 --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# Fishjam Cloud Web Client + +React and TypeScript client libraries for [Fishjam Cloud](https://cloud.fishjam.stream). + +## Installation + +To install SDK you can use `npm` or `yarn` commands: + +### React Library: + +React library is useful for projects that uses React. It is wrapper over TypeScript library, that provides React +integration. + +```bash +npm install @fishjam-cloud/react-client +``` + +or + +```bash +yarn add @fishjam-cloud/react-client +``` + +### TypeScript Library: + +TypeScript library is useful for projects that do not use React. Or if you want to have more control on how all +streaming events are handled. + +```bash +npm install @fishjam-cloud/ts-client +``` + +or + +```bash +yarn add @fishjam-cloud/ts-client +``` + +## Documentation + +For more information visit [React Client](./packages/react-client/readme.md) or +[TypeScript Client](./packages/ts-client/README.md) website. + +## Contributing + +Contributions are always welcome, no matter how large or small! + +We aspire to build a community that is friendly and respectful to each other. Please adhere to this spirit in all your +interactions within the project. + +### Development Workflow + +To get started with the project, run npm install in the root directory to install the required dependencies and build +TypeScript: + +```bash +yarn +yarn build +``` + +Ensure your code passes TypeScript, ESLint and formatter checks by running the following commands: + +``` +yarn typecheck +yarn lint:check +yarn format:check +``` + +To lint and format your code, use the following commands: + +```bash +yarn lint +yarn format +``` + +## Submitting a Pull Request + +> **Working on your first pull request?** Get started with this _free_ series: +> [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). + +When you're sending a pull request: + +- Focus on one change and try to keep pull requests small. +- Make sure that formatter, linter and test checks are passing. +- Review the documentation to make sure it looks good. +- Follow the pull request template when opening a pull request. +- If your pull request changes the API or implementation, first discuss the changes with the maintainers by opening an + issue. + +## Copyright and License + +Copyright 2024, +[Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=fishjam-web-client) + +[![Software Mansion](https://logo.swmansion.com/logo?color=white&variant=desktop&width=200&tag=fishjam-github)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=fishjam-web-client) + +Licensed under the [Apache License, Version 2.0](LICENSE) diff --git a/e2e-tests/react-client/app/.gitignore b/e2e-tests/react-client/app/.gitignore new file mode 100644 index 00000000..12b46b84 --- /dev/null +++ b/e2e-tests/react-client/app/.gitignore @@ -0,0 +1 @@ +/test-results/ diff --git a/e2e-tests/react-client/app/package.json b/e2e-tests/react-client/app/package.json new file mode 100644 index 00000000..601a9081 --- /dev/null +++ b/e2e-tests/react-client/app/package.json @@ -0,0 +1,19 @@ +{ + "name": "@fishjam-e2e/react-client-e2e", + "private": true, + "version": "0.0.0", + "type": "module", + "license": "Apache-2.0", + "scripts": { + "e2e": "NODE_OPTIONS=--dns-result-order=ipv4first playwright test", + "e2e:ui": "NODE_OPTIONS=--dns-result-order=ipv4first playwright test --ui" + }, + "devDependencies": { + "@playwright/test": "^1.45.1", + "@types/node": "^20.11.18", + "@vitest/coverage-v8": "^1.6.0", + "testcontainers": "^10.3.2", + "vite": "^5.1.2", + "vitest": "^1.6.0" + } +} diff --git a/packages/ts-client/playwright.config.ts b/e2e-tests/react-client/app/playwright.config.ts similarity index 63% rename from packages/ts-client/playwright.config.ts rename to e2e-tests/react-client/app/playwright.config.ts index f9b7aa34..e722a51e 100644 --- a/packages/ts-client/playwright.config.ts +++ b/e2e-tests/react-client/app/playwright.config.ts @@ -10,7 +10,7 @@ import { defineConfig, devices } from '@playwright/test'; * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: './e2e', + testDir: '../.', /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ @@ -20,11 +20,14 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + reporter: [ + ['list'], + ['html', { outputFolder: '../../../playwright-report/react-client-e2e', open: "never" }] + ], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://localhost:5173', + baseURL: 'http://localhost:3007', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', @@ -50,46 +53,16 @@ export default defineConfig({ }, }, }, - - // { - // name: 'firefox', - // use: { ...devices['Desktop Firefox'] }, - // }, - - // { - // name: 'webkit', - // use: { ...devices['Desktop Safari'] }, - // }, - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, ], /* Run your local dev server before starting the tests */ webServer: { - command: 'npm run dev', - url: 'http://localhost:5173', + command: 'yarn run dev', + url: 'http://localhost:3007', reuseExistingServer: !process.env.CI, - cwd: 'e2e/app', + cwd: "../../../examples/react-client/minimal-react", }, - globalSetup: './e2e/setup/setupFishjam', - globalTeardown: './e2e/setup/teardownFishjam', + globalSetup: '../setup/setupFishjam', + globalTeardown: '../setup/teardownFishjam', }); diff --git a/e2e_tests/react-client/docker-compose-test.yaml b/e2e-tests/react-client/docker-compose-test.yaml similarity index 91% rename from e2e_tests/react-client/docker-compose-test.yaml rename to e2e-tests/react-client/docker-compose-test.yaml index 3328f32d..bcc8eecf 100644 --- a/e2e_tests/react-client/docker-compose-test.yaml +++ b/e2e-tests/react-client/docker-compose-test.yaml @@ -2,7 +2,7 @@ version: "3" services: fishjam: - image: "ghcr.io/fishjam-dev/fishjam:${FISHJAM_VERSION:-0.6.2}" + image: "ghcr.io/fishjam-dev/fishjam:${FISHJAM_VERSION:-0.6.3}" container_name: fishjam restart: on-failure healthcheck: diff --git a/e2e_tests/react-client/fishjam.spec.ts b/e2e-tests/react-client/scenarios/fishjam.spec.ts similarity index 100% rename from e2e_tests/react-client/fishjam.spec.ts rename to e2e-tests/react-client/scenarios/fishjam.spec.ts diff --git a/e2e_tests/react-client/utils.ts b/e2e-tests/react-client/scenarios/utils.ts similarity index 100% rename from e2e_tests/react-client/utils.ts rename to e2e-tests/react-client/scenarios/utils.ts diff --git a/e2e_tests/react-client/globalSetupState.ts b/e2e-tests/react-client/setup/globalSetupState.ts similarity index 100% rename from e2e_tests/react-client/globalSetupState.ts rename to e2e-tests/react-client/setup/globalSetupState.ts diff --git a/e2e_tests/ts-client/setup/setupFishjam.ts b/e2e-tests/react-client/setup/setupFishjam.ts similarity index 67% rename from e2e_tests/ts-client/setup/setupFishjam.ts rename to e2e-tests/react-client/setup/setupFishjam.ts index 1e42c2e6..3ac876cc 100644 --- a/e2e_tests/ts-client/setup/setupFishjam.ts +++ b/e2e-tests/react-client/setup/setupFishjam.ts @@ -1,4 +1,4 @@ -import { Wait, DockerComposeEnvironment } from 'testcontainers'; +import { DockerComposeEnvironment, Wait } from 'testcontainers'; import { setupState } from './globalSetupState'; import { type NetworkInterfaceInfo, networkInterfaces } from 'os'; @@ -6,14 +6,12 @@ export default async function setupFishjam() { const EXTERNAL_IP = Object.values(networkInterfaces()) .flat() .filter((x): x is NetworkInterfaceInfo => x !== undefined) - .filter(({ family }) => - typeof family === 'string' ? family === 'IPv4' : family === 4, - ) + .filter(({ family }) => family === 'IPv4') .filter(({ internal }) => !internal) .map(({ address }) => address)[0]; - const container = await new DockerComposeEnvironment( - 'e2e', + setupState.fishjamContainer = await new DockerComposeEnvironment( + '../.', 'docker-compose-test.yaml', ) .withEnvironment({ EXTERNAL_IP }) @@ -22,6 +20,4 @@ export default async function setupFishjam() { Wait.forLogMessage('Access FishjamWeb.Endpoint at'), ) .up(); - - setupState.fishjamContainer = container; } diff --git a/e2e_tests/react-client/teardownFishjam.ts b/e2e-tests/react-client/setup/teardownFishjam.ts similarity index 100% rename from e2e_tests/react-client/teardownFishjam.ts rename to e2e-tests/react-client/setup/teardownFishjam.ts diff --git a/e2e_tests/ts-client/app/.gitignore b/e2e-tests/ts-client/app/.gitignore similarity index 100% rename from e2e_tests/ts-client/app/.gitignore rename to e2e-tests/ts-client/app/.gitignore diff --git a/e2e_tests/ts-client/app/compile_proto.sh b/e2e-tests/ts-client/app/compile_proto.sh similarity index 100% rename from e2e_tests/ts-client/app/compile_proto.sh rename to e2e-tests/ts-client/app/compile_proto.sh diff --git a/e2e_tests/ts-client/app/index.html b/e2e-tests/ts-client/app/index.html similarity index 100% rename from e2e_tests/ts-client/app/index.html rename to e2e-tests/ts-client/app/index.html diff --git a/e2e_tests/ts-client/app/package.json b/e2e-tests/ts-client/app/package.json similarity index 50% rename from e2e_tests/ts-client/app/package.json rename to e2e-tests/ts-client/app/package.json index 41ee7e3c..3bed14c7 100644 --- a/e2e_tests/ts-client/app/package.json +++ b/e2e-tests/ts-client/app/package.json @@ -1,5 +1,5 @@ { - "name": "example", + "name": "@fishjam-e2e/ts-client-e2e", "private": true, "version": "0.0.0", "type": "module", @@ -7,7 +7,13 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint . --ext .ts,.tsx --fix", + "lint:check": "eslint . --ext .ts,.tsx", + "typecheck": "tsc", + "e2e": "NODE_OPTIONS=--dns-result-order=ipv4first playwright test", + "e2e:ui": "NODE_OPTIONS=--dns-result-order=ipv4first playwright test --ui", "preview": "vite preview" }, "dependencies": { @@ -17,18 +23,20 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@playwright/test": "^1.41.2", + "@playwright/test": "^1.45.1", "@types/node": "^20.11.18", "@types/react": "^18.2.55", "@types/react-dom": "^18.2.19", - "@typescript-eslint/eslint-plugin": "^7.0.1", - "@typescript-eslint/parser": "^7.0.1", + "@typescript-eslint/eslint-plugin": "^7.15.0", + "@typescript-eslint/parser": "^7.15.0", "@vitejs/plugin-react": "^4.2.1", + "@vitest/coverage-v8": "^1.6.0", "eslint": "^8.56.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", + "testcontainers": "^10.3.2", "ts-proto": "^1.176.0", - "typescript": "^5.4.5", - "vite": "^5.1.2" + "vite": "^5.1.2", + "vitest": "^1.6.0" } } diff --git a/e2e-tests/ts-client/app/playwright.config.ts b/e2e-tests/ts-client/app/playwright.config.ts new file mode 100644 index 00000000..70533a27 --- /dev/null +++ b/e2e-tests/ts-client/app/playwright.config.ts @@ -0,0 +1,73 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "../.", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [ + ["list"], + [ + "html", + { + outputFolder: "../../../playwright-report/ts-client-e2e", + open: "never", + }, + ], + ], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://localhost:5173", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + + extraHTTPHeaders: { + Authorization: "Bearer development", + }, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { + ...devices["Desktop Chrome"], + launchOptions: { + args: [ + "--use-fake-ui-for-media-stream", + "--use-fake-device-for-media-stream", + ], + // default Google Chrome path on MacOS + // executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + }, + }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: "yarn run dev", + url: "http://localhost:5173", + reuseExistingServer: !process.env.CI, + }, + + globalSetup: "../setup/setupFishjam", + globalTeardown: "../setup/teardownFishjam", +}); diff --git a/e2e_tests/ts-client/app/src/App.tsx b/e2e-tests/ts-client/app/src/App.tsx similarity index 64% rename from e2e_tests/ts-client/app/src/App.tsx rename to e2e-tests/ts-client/app/src/App.tsx index ec3f48d4..1fec1c7c 100644 --- a/e2e_tests/ts-client/app/src/App.tsx +++ b/e2e-tests/ts-client/app/src/App.tsx @@ -1,18 +1,18 @@ -import { +import type { Endpoint, SerializedMediaEvent, TrackContext, TrackEncoding, - WebRTCEndpoint, WebRTCEndpointEvents, TrackContextEvents, BandwidthLimit, SimulcastConfig, -} from '@fishjam-dev/ts-client'; -import { PeerMessage } from '@fishjam-dev/ts-client/protos'; -import { useEffect, useState, useSyncExternalStore } from 'react'; -import { MockComponent } from './MockComponent'; -import { VideoPlayerWithDetector } from './VideoPlayerWithDetector'; +} from "@fishjam-dev/ts-client"; +import { WebRTCEndpoint } from "@fishjam-dev/ts-client"; +import { PeerMessage } from "@fishjam-dev/ts-client/protos"; +import { useEffect, useState, useSyncExternalStore } from "react"; +import { MockComponent } from "./MockComponent"; +import { VideoPlayerWithDetector } from "./VideoPlayerWithDetector"; /* eslint-disable no-console */ @@ -24,25 +24,25 @@ export type TrackMetadata = { goodTrack: string; }; -function endpointMetadataParser(a: any): EndpointMetadata { +function endpointMetadataParser(a: unknown): EndpointMetadata { if ( - typeof a !== 'object' || + typeof a !== "object" || a === null || - !('goodStuff' in a) || - typeof a.goodStuff !== 'string' + !("goodStuff" in a) || + typeof a.goodStuff !== "string" ) - throw 'Invalid metadata!!!'; + throw "Invalid metadata!!!"; return { goodStuff: a.goodStuff }; } -function trackMetadataParser(a: any): TrackMetadata { +function trackMetadataParser(a: unknown): TrackMetadata { if ( - typeof a !== 'object' || + typeof a !== "object" || a === null || - !('goodTrack' in a) || - typeof a.goodTrack !== 'string' + !("goodTrack" in a) || + typeof a.goodTrack !== "string" ) - throw 'Invalid track metadata!!!'; + throw "Invalid track metadata!!!"; return { goodTrack: a.goodTrack }; } @@ -69,14 +69,14 @@ class RemoteStore { const trackCb: TrackContextEvents< EndpointMetadata, TrackMetadata - >['encodingChanged'] = () => cb(); + >["encodingChanged"] = () => cb(); const trackAddedCb: WebRTCEndpointEvents< EndpointMetadata, TrackMetadata - >['trackAdded'] = (context) => { - context.on('encodingChanged', () => trackCb); - context.on('voiceActivityChanged', () => trackCb); + >["trackAdded"] = (context) => { + context.on("encodingChanged", () => trackCb); + context.on("voiceActivityChanged", () => trackCb); callback(); }; @@ -84,29 +84,29 @@ class RemoteStore { const removeCb: WebRTCEndpointEvents< EndpointMetadata, TrackMetadata - >['trackRemoved'] = (context) => { - context.removeListener('encodingChanged', () => trackCb); - context.removeListener('voiceActivityChanged', () => trackCb); + >["trackRemoved"] = (context) => { + context.removeListener("encodingChanged", () => trackCb); + context.removeListener("voiceActivityChanged", () => trackCb); callback(); }; - this.webrtc.on('trackAdded', trackAddedCb); - this.webrtc.on('trackReady', cb); - this.webrtc.on('trackUpdated', cb); - this.webrtc.on('trackRemoved', removeCb); - this.webrtc.on('endpointAdded', cb); - this.webrtc.on('endpointRemoved', cb); - this.webrtc.on('endpointUpdated', cb); + this.webrtc.on("trackAdded", trackAddedCb); + this.webrtc.on("trackReady", cb); + this.webrtc.on("trackUpdated", cb); + this.webrtc.on("trackRemoved", removeCb); + this.webrtc.on("endpointAdded", cb); + this.webrtc.on("endpointRemoved", cb); + this.webrtc.on("endpointUpdated", cb); return () => { - this.webrtc.removeListener('trackAdded', trackAddedCb); - this.webrtc.removeListener('trackReady', cb); - this.webrtc.removeListener('trackUpdated', cb); - this.webrtc.removeListener('trackRemoved', removeCb); - this.webrtc.removeListener('endpointAdded', cb); - this.webrtc.removeListener('endpointRemoved', cb); - this.webrtc.removeListener('endpointUpdated', cb); + this.webrtc.removeListener("trackAdded", trackAddedCb); + this.webrtc.removeListener("trackReady", cb); + this.webrtc.removeListener("trackUpdated", cb); + this.webrtc.removeListener("trackRemoved", removeCb); + this.webrtc.removeListener("endpointAdded", cb); + this.webrtc.removeListener("endpointRemoved", cb); + this.webrtc.removeListener("endpointUpdated", cb); }; } @@ -114,8 +114,8 @@ class RemoteStore { const newTracks = webrtc.getRemoteTracks(); const newEndpoints = webrtc.getRemoteEndpoints(); const ids = - Object.keys(newTracks).sort().join(':') + - Object.keys(newEndpoints).sort().join(':'); + Object.keys(newTracks).sort().join(":") + + Object.keys(newEndpoints).sort().join(":"); if (!(ids in this.cache) || this.invalidateCache) { this.cache[ids] = [newEndpoints, newTracks]; this.invalidateCache = false; @@ -135,25 +135,26 @@ const webrtc = new WebRTCEndpoint({ const remoteTracksStore = new RemoteStore(webrtc); function connect(token: string, metadata: EndpointMetadata) { - const websocketUrl = 'ws://localhost:5002/socket/peer/websocket'; + const websocketUrl = "ws://localhost:5002/socket/peer/websocket"; const websocket = new WebSocket(websocketUrl); - websocket.binaryType = 'arraybuffer'; + websocket.binaryType = "arraybuffer"; function socketOpenHandler(_event: Event) { const message = PeerMessage.encode({ authRequest: { token } }).finish(); websocket.send(message); } - websocket.addEventListener('open', socketOpenHandler); + websocket.addEventListener("open", socketOpenHandler); - webrtc.on('sendMediaEvent', (mediaEvent: SerializedMediaEvent) => { - console.log(`%c(${clientId}) - Send: ${mediaEvent}`, 'color:blue'); + webrtc.on("sendMediaEvent", (mediaEvent: SerializedMediaEvent) => { + console.log(`%c(${clientId}) - Send: ${mediaEvent}`, "color:blue"); const message = PeerMessage.encode({ mediaEvent: { data: mediaEvent }, }).finish(); websocket.send(message); }); + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const messageHandler = (event: MessageEvent) => { const uint8Array = new Uint8Array(event.data); try { @@ -163,19 +164,19 @@ function connect(token: string, metadata: EndpointMetadata) { const mediaEvent = JSON.parse(data?.mediaEvent?.data); console.log( `%c(${clientId}) - Received: ${JSON.stringify(mediaEvent)}`, - 'color:green', + "color:green", ); } else { console.log( `%c(${clientId}) - Received: ${JSON.stringify(data)}`, - 'color:green', + "color:green", ); } if (data.authenticated !== undefined) { webrtc.connect(metadata); } else if (data.authRequest !== undefined) { - console.warn('Received unexpected control message: authRequest'); + console.warn("Received unexpected control message: authRequest"); } else if (data.mediaEvent !== undefined) { webrtc.receiveMediaEvent(data.mediaEvent.data); } @@ -184,32 +185,32 @@ function connect(token: string, metadata: EndpointMetadata) { } }; - websocket.addEventListener('message', messageHandler); + websocket.addEventListener("message", messageHandler); - const closeHandler = (event: any) => { - console.log({ name: 'Close handler!', event }); + const closeHandler = (event: unknown) => { + console.log({ name: "Close handler!", event }); }; - websocket.addEventListener('close', closeHandler); + websocket.addEventListener("close", closeHandler); - const errorHandler = (event: any) => { - console.log({ name: 'Error handler!', event }); + const errorHandler = (event: unknown) => { + console.log({ name: "Error handler!", event }); }; - websocket.addEventListener('error', errorHandler); + websocket.addEventListener("error", errorHandler); - const trackReady = (event: any) => { - console.log({ name: 'trackReady', event }); + const trackReady = (event: unknown) => { + console.log({ name: "trackReady", event }); }; - websocket.addEventListener('trackReady', trackReady); + websocket.addEventListener("trackReady", trackReady); } async function addScreenshareTrack(): Promise { const stream = await window.navigator.mediaDevices.getDisplayMedia(); const track = stream.getVideoTracks()[0]; - const trackMetadata: TrackMetadata = { goodTrack: 'screenshare' }; + const trackMetadata: TrackMetadata = { goodTrack: "screenshare" }; const simulcastConfig: SimulcastConfig = { enabled: false, activeEncodings: [], @@ -222,21 +223,21 @@ async function addScreenshareTrack(): Promise { export function App() { const [tokenInput, setTokenInput] = useState( - localStorage.getItem('token') ?? '', + localStorage.getItem("token") ?? "", ); const [endpointMetadataInput, setEndpointMetadataInput] = useState( - JSON.stringify({ goodStuff: 'ye' }), + JSON.stringify({ goodStuff: "ye" }), ); const [connected, setConnected] = useState(false); useEffect(() => { - localStorage.setItem('token', tokenInput); + localStorage.setItem("token", tokenInput); }, [tokenInput]); const handleConnect = () => connect( tokenInput, - endpointMetadataInput !== '' + endpointMetadataInput !== "" ? JSON.parse(endpointMetadataInput) : undefined, ); @@ -256,15 +257,15 @@ export function App() { useEffect(() => { const callback = () => setConnected(true); - webrtc.on('connected', callback); + webrtc.on("connected", callback); return () => { - webrtc.removeListener('connected', callback); + webrtc.removeListener("connected", callback); }; }, []); return ( -
+
-
{connected ? 'true' : 'false'}
+
{connected ? "true" : "false"}

-
+
{Object.values(remoteTracks).map( ({ stream, @@ -299,21 +300,22 @@ export function App() {
+ data-stream-id={stream?.id} + >
Endpoint id: {endpoint.id}
- Metadata:{' '} + Metadata:{" "} {JSON.stringify(metadata)}
- Raw:{' '} + Raw:{" "} {JSON.stringify(rawMetadata)}
- Error:{' '} + Error:{" "} {metadataParsingError} -
+
{stream?.id}
- - - + + +
), )}
-
+
Our metadata: setEndpointMetadataInput(e.target.value)}> + onChange={(e) => setEndpointMetadataInput(e.target.value)} + >
Endpoints: @@ -343,15 +346,15 @@ export function App() { ({ id, metadata, rawMetadata, metadataParsingError }) => (
{id} - metadata:{' '} + metadata:{" "} {JSON.stringify(metadata)}
- raw metadata:{' '} + raw metadata:{" "} {JSON.stringify(rawMetadata)}
- metadata parsing error:{' '} + metadata parsing error:{" "} {metadataParsingError?.toString?.() ?? metadataParsingError} diff --git a/e2e_tests/ts-client/app/src/MockComponent.tsx b/e2e-tests/ts-client/app/src/MockComponent.tsx similarity index 73% rename from e2e_tests/ts-client/app/src/MockComponent.tsx rename to e2e-tests/ts-client/app/src/MockComponent.tsx index a1a0c872..d62ce564 100644 --- a/e2e_tests/ts-client/app/src/MockComponent.tsx +++ b/e2e-tests/ts-client/app/src/MockComponent.tsx @@ -1,18 +1,25 @@ -import { createStream } from './mocks'; -import { VideoPlayer } from './VideoPlayer'; -import { useRef, useState } from 'react'; -import { EndpointMetadata, TrackMetadata } from './App'; -import { +import { createStream } from "./mocks"; +import { VideoPlayer } from "./VideoPlayer"; +import { useRef, useState } from "react"; +import type { EndpointMetadata, TrackMetadata } from "./App"; +import type { BandwidthLimit, SimulcastConfig, WebRTCEndpoint, -} from '@fishjam-dev/ts-client'; -import { MuteTrackTest } from './MuteTrackTest'; +} from "@fishjam-dev/ts-client"; +import { MuteTrackTest } from "./MuteTrackTest"; -export const brainMock = createStream('๐Ÿง ', 'white', 'low', 24); -export const brain2Mock = createStream('๐Ÿคฏ', '#00ff00', 'low', 24); -export const heartMock = createStream('๐Ÿซ€', 'white', 'low', 24); -export const heart2Mock = createStream('๐Ÿ’', '#FF0000', 'low', 24); +// eslint-disable-next-line react-refresh/only-export-components +export const brainMock = createStream("๐Ÿง ", "white", "low", 24); + +// eslint-disable-next-line react-refresh/only-export-components +export const brain2Mock = createStream("๐Ÿคฏ", "#00ff00", "low", 24); + +// eslint-disable-next-line react-refresh/only-export-components +export const heartMock = createStream("๐Ÿซ€", "white", "low", 24); + +// eslint-disable-next-line react-refresh/only-export-components +export const heart2Mock = createStream("๐Ÿ’", "#FF0000", "low", 24); type Props = { webrtc: WebRTCEndpoint; @@ -22,10 +29,10 @@ export const MockComponent = ({ webrtc }: Props) => { const heartId = useRef | null>(null); const brainId = useRef | null>(null); const [replaceStatus, setReplaceStatus] = useState< - 'unknown' | 'success' | 'failure' - >('unknown'); + "unknown" | "success" | "failure" + >("unknown"); const [trackMetadataInput, setTrackMetadataInput] = useState( - JSON.stringify({ goodTrack: 'ye' }), + JSON.stringify({ goodTrack: "ye" }), ); const addHeart = async () => { @@ -36,19 +43,19 @@ export const MockComponent = ({ webrtc }: Props) => { }; const removeHeart = async () => { - if (!heartId.current) throw Error('Heart id is undefined'); + if (!heartId.current) throw Error("Heart id is undefined"); webrtc.removeTrack(await heartId.current); }; const removeBrain = async () => { - if (!brainId.current) throw Error('Brain id is undefined'); + if (!brainId.current) throw Error("Brain id is undefined"); webrtc.removeTrack(await brainId.current); }; const replaceHeart = async () => { - if (!heartId.current) throw Error('Track Id is not set'); + if (!heartId.current) throw Error("Track Id is not set"); const stream = heart2Mock.stream; const track = stream.getVideoTracks()[0]; @@ -58,11 +65,11 @@ export const MockComponent = ({ webrtc }: Props) => { track, JSON.parse(trackMetadataInput), ); - setReplaceStatus('success'); + setReplaceStatus("success"); }; const replaceBrain = async () => { - if (!brainId.current) throw Error('Track Id is not set'); + if (!brainId.current) throw Error("Track Id is not set"); const stream = brain2Mock.stream; const track = stream.getVideoTracks()[0]; @@ -79,8 +86,8 @@ export const MockComponent = ({ webrtc }: Props) => { const track = stream.getVideoTracks()[0]; const simulcastConfig: SimulcastConfig = { - enabled: false, - activeEncodings: [], + enabled: true, + activeEncodings: ["h", "m", "l"], disabledEncodings: [], }; const maxBandwidth: BandwidthLimit = 0; diff --git a/e2e_tests/ts-client/app/src/MuteTrackTest.tsx b/e2e-tests/ts-client/app/src/MuteTrackTest.tsx similarity index 64% rename from e2e_tests/ts-client/app/src/MuteTrackTest.tsx rename to e2e-tests/ts-client/app/src/MuteTrackTest.tsx index 02f4dca6..54f5c2d6 100644 --- a/e2e_tests/ts-client/app/src/MuteTrackTest.tsx +++ b/e2e-tests/ts-client/app/src/MuteTrackTest.tsx @@ -1,9 +1,9 @@ -import { WebRTCEndpoint } from '@fishjam-dev/ts-client'; -import { brain2Mock, heart2Mock } from './MockComponent'; -import { useEffect, useState } from 'react'; -import { VideoPlayer } from './VideoPlayer'; -import { WebRTCEndpointEvents } from '../../../src'; -import { EndpointMetadata, TrackMetadata } from './App'; +import type { WebRTCEndpoint } from "@fishjam-dev/ts-client"; +import { brain2Mock, heart2Mock } from "./MockComponent"; +import { useEffect, useState } from "react"; +import { VideoPlayer } from "./VideoPlayer"; +import type { WebRTCEndpointEvents } from "@fishjam-dev/ts-client/webrtc"; +import type { EndpointMetadata, TrackMetadata } from "./App"; type Props = { webrtc: WebRTCEndpoint; @@ -18,7 +18,7 @@ export const MuteTrackTest = ({ webrtc }: Props) => { const localTrackAdded: WebRTCEndpointEvents< EndpointMetadata, TrackMetadata - >['localTrackAdded'] = (event) => { + >["localTrackAdded"] = (event) => { setCurrentStream(event.stream); setCurrentTrack(event.track); setTrackId(event.trackId); @@ -27,16 +27,16 @@ export const MuteTrackTest = ({ webrtc }: Props) => { const localTrackReplaced: WebRTCEndpointEvents< EndpointMetadata, TrackMetadata - >['localTrackReplaced'] = (event) => { + >["localTrackReplaced"] = (event) => { setCurrentTrack(event.track); }; - webrtc.on('localTrackAdded', localTrackAdded); - webrtc.on('localTrackReplaced', localTrackReplaced); + webrtc.on("localTrackAdded", localTrackAdded); + webrtc.on("localTrackReplaced", localTrackReplaced); return () => { - webrtc.removeListener('localTrackAdded', localTrackAdded); - webrtc.removeListener('localTrackReplaced', localTrackReplaced); + webrtc.removeListener("localTrackAdded", localTrackAdded); + webrtc.removeListener("localTrackReplaced", localTrackReplaced); }; }, [webrtc]); @@ -47,10 +47,10 @@ export const MuteTrackTest = ({ webrtc }: Props) => { await webrtc.addTrack( track, - { goodTrack: 'camera' }, + { goodTrack: "camera" }, { enabled: true, - activeEncodings: ['l', 'm', 'h'], + activeEncodings: ["l", "m", "h"], disabledEncodings: [], }, ); @@ -61,7 +61,7 @@ export const MuteTrackTest = ({ webrtc }: Props) => { stream: MediaStream | null, track: MediaStreamTrack | null, ) => { - if (!trackId) throw Error('Track id is null'); + if (!trackId) throw Error("Track id is null"); await webrtc.replaceTrack(trackId, track); }; @@ -69,25 +69,28 @@ export const MuteTrackTest = ({ webrtc }: Props) => { return (
+ display: "flex", + flexDirection: "column", + padding: "8px", + borderStyle: "dotted", + borderWidth: "1px", + borderColor: "black", + }} + >
- track: {currentTrack?.id ?? 'null'} + track: {currentTrack?.id ?? "null"}
@@ -195,7 +209,8 @@

type="text" id="peer-name-input" placeholder="" - class="input-bordered input-success input w-full" /> + class="input-bordered input-success input w-full" + />

@@ -234,7 +249,8 @@

Canvas track

id="local-track-video" class="w-[200px]" playsinline - muted> + muted + >
diff --git a/examples/ts-client/simple-app/package.json b/examples/ts-client/simple-app/package.json index 711cb668..41bca45c 100644 --- a/examples/ts-client/simple-app/package.json +++ b/examples/ts-client/simple-app/package.json @@ -1,5 +1,5 @@ { - "name": "simple-app-example", + "name": "@fishjam-example/simple-app-example", "private": true, "version": "0.0.1", "license": "Apache-2.0", @@ -7,19 +7,22 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "preview": "vite preview" + "preview": "vite preview", + "format": "prettier --write . --ignore-path ./.eslintignore", + "format:check": "prettier --check . --ignore-path ./.eslintignore", + "lint": "eslint . --ext .ts,.tsx --fix", + "lint:check": "eslint . --ext .ts,.tsx", + "typecheck": "tsc" }, "devDependencies": { "autoprefixer": "^10.4.17", - "daisyui": "^4.7.2", - "postcss": "^8.4.35", + "daisyui": "^4.12.10", + "postcss": "^8.4.39", "tailwindcss": "^3.4.1", - "typescript": "^5.4.0", "vite": "^5.1.2", "vite-plugin-checker": "^0.6.4" }, "dependencies": { - "@fishjam-dev/browser-media-utils": "https://github.com/fishjam-dev/browser-media-utils#1.0.0", "@fishjam-dev/ts-client": "*" } } diff --git a/examples/ts-client/simple-app/src/createMockStream.ts b/examples/ts-client/simple-app/src/createMockStream.ts index 78894c7e..c2117789 100644 --- a/examples/ts-client/simple-app/src/createMockStream.ts +++ b/examples/ts-client/simple-app/src/createMockStream.ts @@ -10,11 +10,11 @@ export const createStream: ( backgroundColor: string, framerate: number, ) => { - const canvasElement = document.createElement('canvas'); + const canvasElement = document.createElement("canvas"); canvasElement.width = canvasWidth; canvasElement.height = canvasHeight; - const ctx = canvasElement.getContext('2d'); - if (!ctx) throw 'ctx is null'; + const ctx = canvasElement.getContext("2d"); + if (!ctx) throw "ctx is null"; const fontSize = 150; let degree = 0; @@ -33,7 +33,7 @@ export const createStream: ( ctx.font = `${fontSize}px Calibri`; ctx.translate(translateX, translateY); ctx.rotate(radian); - ctx.fillStyle = '#FFFFFF'; + ctx.fillStyle = "#FFFFFF"; ctx.fillText(emoji, -fontSize / 2, +fontSize / 2); // ctx.fillStyle = "#FF00FF"; // ctx.fillRect(0, 0, 10, 10); diff --git a/examples/ts-client/simple-app/src/main.ts b/examples/ts-client/simple-app/src/main.ts index cddc6f18..131a6d2a 100644 --- a/examples/ts-client/simple-app/src/main.ts +++ b/examples/ts-client/simple-app/src/main.ts @@ -1,56 +1,60 @@ -import './style.css'; +import "./style.css"; -import { createStream } from './createMockStream'; -import { FishjamClient, TrackEncoding, Peer } from '@fishjam-dev/ts-client'; -import { - enumerateDevices, - getUserMedia, - SCREEN_SHARING_MEDIA_CONSTRAINTS, -} from '@fishjam-dev/browser-media-utils'; +import { createStream } from "./createMockStream"; +import type { TrackEncoding, Peer } from "@fishjam-dev/ts-client"; +import { FishjamClient } from "@fishjam-dev/ts-client"; + +const SCREEN_SHARING_MEDIA_CONSTRAINTS = { + video: { + frameRate: { ideal: 20, max: 25 }, + width: { max: 1920, ideal: 1920 }, + height: { max: 1080, ideal: 1080 }, + }, +}; /* eslint-disable no-console */ const peerTokenInput = - document.querySelector('#peer-token-input')!; + document.querySelector("#peer-token-input")!; const peerNameInput = - document.querySelector('#peer-name-input')!; + document.querySelector("#peer-name-input")!; const connectButton = - document.querySelector('#connect-btn')!; + document.querySelector("#connect-btn")!; const disconnectButton = - document.querySelector('#disconnect-btn')!; + document.querySelector("#disconnect-btn")!; const reconnectButton = - document.querySelector('#reconnect-btn')!; + document.querySelector("#reconnect-btn")!; const forceErrorButton = - document.querySelector('#force-error-btn')!; + document.querySelector("#force-error-btn")!; const forceCloseButton = - document.querySelector('#force-close-btn')!; + document.querySelector("#force-close-btn")!; const addTrackButton = - document.querySelector('#add-track-btn')!; + document.querySelector("#add-track-btn")!; const removeTrackButton = - document.querySelector('#remove-track-btn')!; + document.querySelector("#remove-track-btn")!; const localVideo = - document.querySelector('#local-track-video')!; + document.querySelector("#local-track-video")!; const enumerateDevicesButton = document.querySelector( - '#enumerate-devices-btn', + "#enumerate-devices-btn", )!; const screenSharingContainer = document.querySelector( - '#screen-sharing-container', + "#screen-sharing-container", )!; -const templateVideoPlayer = document.querySelector('#video-player-template')!; -const ENCODINGS: TrackEncoding[] = ['l', 'm', 'h']; +const templateVideoPlayer = document.querySelector("#video-player-template")!; +const ENCODINGS: TrackEncoding[] = ["l", "m", "h"]; const elementsToShowIfConnected = - document.querySelectorAll('.show-if-connected'); -elementsToShowIfConnected.forEach((e) => e.classList.add('hidden')); + document.querySelectorAll(".show-if-connected"); +elementsToShowIfConnected.forEach((e) => e.classList.add("hidden")); const borderActiveClasses = [ - 'border-success', - 'border-4', - 'rounded-3', - 'border-solid', + "border-success", + "border-4", + "rounded-3", + "border-solid", ]; -const stream = createStream('๐Ÿงช', 'black', 24).stream; +const stream = createStream("๐Ÿงช", "black", 24).stream; localVideo.srcObject = stream; type Track = { @@ -71,15 +75,15 @@ localVideo.play(); const inputArray = [peerTokenInput, peerNameInput]; inputArray.forEach((input) => { - input.value = localStorage.getItem(input.id) || ''; + input.value = localStorage.getItem(input.id) || ""; // eslint-disable-next-line @typescript-eslint/no-explicit-any - input.addEventListener('input', (event: any) => { + input.addEventListener("input", (event: any) => { localStorage.setItem(input.id, event.target?.value); }); }); -const TrackTypeValues = ['screensharing', 'camera', 'audio'] as const; +const TrackTypeValues = ["screensharing", "camera", "audio"] as const; export type TrackType = (typeof TrackTypeValues)[number]; export type PeerMetadata = { @@ -92,10 +96,10 @@ export type TrackMetadata = { const isPeerMetadata = (input: unknown): input is PeerMetadata => { return ( - typeof input === 'object' && + typeof input === "object" && input !== null && - 'name' in input && - typeof input['name'] === 'string' + "name" in input && + typeof input["name"] === "string" ); }; @@ -103,22 +107,22 @@ const isTrackType = (input: unknown): input is TrackType => TrackTypeValues.includes(input as TrackType); const isTrackMetadata = (input: unknown): input is TrackMetadata => - typeof input === 'object' && + typeof input === "object" && input !== null && - 'type' in input && + "type" in input && isTrackType(input.type) && - 'active' in input && - typeof input.active === 'boolean'; + "active" in input && + typeof input.active === "boolean"; const trackMetadataParser = (input: unknown): TrackMetadata => { if (isTrackMetadata(input)) return input; - throw Error('Invalid track metadata'); + throw Error("Invalid track metadata"); }; const peerMetadataParser = (input: unknown): PeerMetadata => { if (isPeerMetadata(input)) return input; - throw Error('Invalid peer metadata'); + throw Error("Invalid peer metadata"); }; const client: FishjamClient = new FishjamClient({ @@ -129,37 +133,37 @@ const client: FishjamClient = new FishjamClient({ (window as unknown as { client: typeof client }).client = client; -client.on('socketClose', () => { - toastInfo('Socket closed'); +client.on("socketClose", () => { + toastInfo("Socket closed"); }); -client.on('socketError', () => { - toastAlert('Socket error'); +client.on("socketError", () => { + toastAlert("Socket error"); }); -client.on('authSuccess', () => { - toastSuccess('Auth success'); +client.on("authSuccess", () => { + toastSuccess("Auth success"); }); -client.on('authError', () => { - toastAlert('Auth error'); +client.on("authError", () => { + toastAlert("Auth error"); }); -client.on('disconnected', () => { - toastInfo('Disconnected'); +client.on("disconnected", () => { + toastInfo("Disconnected"); }); -client.on('trackAdded', (ctx) => { - console.log({ name: 'trackAdded', ctx }); +client.on("trackAdded", (ctx) => { + console.log({ name: "trackAdded", ctx }); }); client.on( - 'joined', + "joined", (_peerId: string, peersInRoom: Peer[]) => { - console.log('Join success!'); + console.log("Join success!"); toastSuccess(`Joined room`); - const template = document.querySelector('#remote-peer-template-card')!; - const remotePeers = document.querySelector('#remote-peers')!; + const template = document.querySelector("#remote-peer-template-card")!; + const remotePeers = document.querySelector("#remote-peers")!; (peersInRoom || []).forEach((peer: Peer) => { // @ts-ignore @@ -167,7 +171,7 @@ client.on( const card = clone.firstElementChild; card.dataset.peerId = peer.id; - const peerId = clone.querySelector('.remote-peer-template-id'); + const peerId = clone.querySelector(".remote-peer-template-id"); peerId.innerHTML = peer.id; clone.firstElementChild.dataset.peerId = peer.id; @@ -178,23 +182,23 @@ client.on( }, ); -client.on('joinError', (metadata) => { - console.log({ name: 'joinError', metadata }); - toastAlert('Join error'); +client.on("joinError", (metadata) => { + console.log({ name: "joinError", metadata }); + toastAlert("Join error"); }); -client.on('peerJoined', (peer: Peer) => { - console.log('Peer join success!'); +client.on("peerJoined", (peer: Peer) => { + console.log("Peer join success!"); - const template = document.querySelector('#remote-peer-template-card')!; - const remotePeers = document.querySelector('#remote-peers')!; + const template = document.querySelector("#remote-peer-template-card")!; + const remotePeers = document.querySelector("#remote-peers")!; // @ts-ignore const clone = template.content.cloneNode(true); const card = clone.firstElementChild; card.dataset.peerId = peer.id; - const peerId = clone.querySelector('.remote-peer-template-id'); + const peerId = clone.querySelector(".remote-peer-template-id"); peerId.innerHTML = peer.id; clone.firstElementChild.dataset.peerId = peer.id; @@ -204,9 +208,9 @@ client.on('peerJoined', (peer: Peer) => { toastInfo(`New peer joined`); }); -client.on('peerUpdated', (_peer) => {}); +client.on("peerUpdated", (_peer) => {}); -client.on('peerLeft', (peer) => { +client.on("peerLeft", (peer) => { const peerComponent = document.querySelector( `div[data-peer-id="${peer.id}"`, )!; @@ -217,7 +221,7 @@ client.on('peerLeft', (peer) => { const setupSimulcastCheckbox = ( element: DocumentFragment, trackId: string, - encoding: 'l' | 'm' | 'h', + encoding: "l" | "m" | "h", ) => { const simulcastInputL: HTMLInputElement | null = element.querySelector( @@ -225,9 +229,9 @@ const setupSimulcastCheckbox = ( ); if (!simulcastInputL) return; - simulcastInputL.setAttribute('name', `${trackId}-simulcast`); + simulcastInputL.setAttribute("name", `${trackId}-simulcast`); - simulcastInputL.addEventListener('click', () => { + simulcastInputL.addEventListener("click", () => { if (client.getRemoteTracks()[trackId]?.simulcastConfig?.enabled) { client.setTargetTrackEncoding(trackId, encoding); } else { @@ -241,17 +245,17 @@ const setupSimulcastCheckbox = ( }); }; -client.on('trackReady', (ctx) => { - console.log({ name: 'On track ready', ctx }); +client.on("trackReady", (ctx) => { + console.log({ name: "On track ready", ctx }); if (!ctx.trackId) return; const peerId = ctx.endpoint.id; const peerComponent = document.querySelector(`div[data-peer-id="${peerId}"`)!; const videoPlayerTemplate = document.querySelector( - '#remote-peer-template-video', + "#remote-peer-template-video", ); - if (!videoPlayerTemplate) throw new Error('Remote video template not found'); + if (!videoPlayerTemplate) throw new Error("Remote video template not found"); const videoWrapper = ( videoPlayerTemplate.content.cloneNode(true) @@ -259,21 +263,21 @@ client.on('trackReady', (ctx) => { const tracksContainer: HTMLDivElement | null = videoWrapper.querySelector(`.remote-track-container`); - if (!tracksContainer) throw new Error('Remote track container not found'); + if (!tracksContainer) throw new Error("Remote track container not found"); tracksContainer.dataset.trackId = ctx.trackId; const videoPlayer: HTMLVideoElement | null = videoWrapper.querySelector(`video`); - if (!videoPlayer) throw new Error('Video element not found'); + if (!videoPlayer) throw new Error("Video element not found"); - const container = peerComponent.querySelector('.remote-videos'); + const container = peerComponent.querySelector(".remote-videos"); - if (!container) throw new Error('Remote videos container not found!'); + if (!container) throw new Error("Remote videos container not found!"); const simulcastContainer: HTMLDivElement | null = videoWrapper.querySelector(`.simulcast-enabled`); - if (!simulcastContainer) throw new Error('Simulcast container not found'); + if (!simulcastContainer) throw new Error("Simulcast container not found"); simulcastContainer.innerHTML = ( ctx?.simulcastConfig?.enabled ?? false @@ -281,24 +285,24 @@ client.on('trackReady', (ctx) => { const simulcastRadios: HTMLDivElement | null = videoWrapper.querySelector(`.simulcast-radios`); - if (!simulcastRadios) throw new Error('Simulcast radios not found'); + if (!simulcastRadios) throw new Error("Simulcast radios not found"); if (!ctx?.simulcastConfig?.enabled) { - simulcastRadios.classList.add('hidden'); + simulcastRadios.classList.add("hidden"); } ENCODINGS.forEach((encoding) => { setupSimulcastCheckbox(videoWrapper, ctx.trackId, encoding); }); - const rawMetadata = videoWrapper.querySelector('.remote-track-raw-metadata'); - if (!rawMetadata) throw new Error('Raw metadata component not found'); + const rawMetadata = videoWrapper.querySelector(".remote-track-raw-metadata"); + if (!rawMetadata) throw new Error("Raw metadata component not found"); rawMetadata.innerHTML = JSON.stringify(ctx.rawMetadata, undefined, 2); const parsedMetadata = videoWrapper.querySelector( - '.remote-track-parsed-metadata', + ".remote-track-parsed-metadata", ); - if (!parsedMetadata) throw new Error('Parsed metadata component not found'); + if (!parsedMetadata) throw new Error("Parsed metadata component not found"); parsedMetadata.innerHTML = JSON.stringify(ctx.metadata, undefined, 2); container.appendChild(videoWrapper); @@ -309,81 +313,81 @@ client.on('trackReady', (ctx) => { }; }); -client.on('trackUpdated', (ctx) => { - console.log({ name: 'trackUpdated', ctx }); +client.on("trackUpdated", (ctx) => { + console.log({ name: "trackUpdated", ctx }); const videoWrapper: HTMLElement | null = document.querySelector( `div[data-track-id="${ctx.trackId}"`, )!; - const rawMetadata = videoWrapper.querySelector('.remote-track-raw-metadata'); - if (!rawMetadata) throw new Error('Raw metadata component not found'); + const rawMetadata = videoWrapper.querySelector(".remote-track-raw-metadata"); + if (!rawMetadata) throw new Error("Raw metadata component not found"); rawMetadata.innerHTML = JSON.stringify(ctx.rawMetadata, undefined, 2); const parsedMetadata = videoWrapper.querySelector( - '.remote-track-parsed-metadata', + ".remote-track-parsed-metadata", ); - if (!parsedMetadata) throw new Error('Parsed metadata component not found'); + if (!parsedMetadata) throw new Error("Parsed metadata component not found"); parsedMetadata.innerHTML = JSON.stringify(ctx.metadata, undefined, 2); }); -client.on('trackAdded', (ctx) => { - ctx.on('encodingChanged', () => { +client.on("trackAdded", (ctx) => { + ctx.on("encodingChanged", () => { const activeEncodingElement = document.querySelector( `div[data-track-id="${ctx.trackId}"] .simulcast-active-encoding`, )!; - activeEncodingElement.innerHTML = ctx.encoding ?? ''; + activeEncodingElement.innerHTML = ctx.encoding ?? ""; }); - ctx.on('voiceActivityChanged', () => {}); + ctx.on("voiceActivityChanged", () => {}); }); -client.on('trackRemoved', (ctx) => { +client.on("trackRemoved", (ctx) => { const tracksContainer: HTMLElement | null = document.querySelector( `div[data-track-id="${ctx.trackId}"`, ); tracksContainer?.remove(); }); -client.on('trackUpdated', (_ctx) => {}); +client.on("trackUpdated", (_ctx) => {}); -client.on('bandwidthEstimationChanged', (_estimation) => {}); +client.on("bandwidthEstimationChanged", (_estimation) => {}); -client.on('tracksPriorityChanged', (_enabledTracks, _disabledTracks) => {}); +client.on("tracksPriorityChanged", (_enabledTracks, _disabledTracks) => {}); -connectButton.addEventListener('click', () => { - console.log('Connect'); +connectButton.addEventListener("click", () => { + console.log("Connect"); client.connect({ - peerMetadata: { name: peerNameInput.value || '' }, + peerMetadata: { name: peerNameInput.value || "" }, token: peerTokenInput.value, }); - elementsToShowIfConnected.forEach((e) => e.classList.remove('hidden')); + elementsToShowIfConnected.forEach((e) => e.classList.remove("hidden")); }); -disconnectButton.addEventListener('click', () => { - console.log('Disconnect'); +disconnectButton.addEventListener("click", () => { + console.log("Disconnect"); client.disconnect(); - elementsToShowIfConnected.forEach((e) => e.classList.add('hidden')); + elementsToShowIfConnected.forEach((e) => e.classList.add("hidden")); }); -reconnectButton.addEventListener('click', () => { - console.log('Reconnect button'); +reconnectButton.addEventListener("click", () => { + console.log("Reconnect button"); // @ts-expect-error - client['reconnect']?.(); + client["reconnect"]?.(); }); -forceErrorButton.addEventListener('click', () => { - console.log('force error button'); - client['websocket']?.dispatchEvent(new Event('error')); +forceErrorButton.addEventListener("click", () => { + console.log("force error button"); + client["websocket"]?.dispatchEvent(new Event("error")); }); -forceCloseButton.addEventListener('click', () => { - console.log('force close button'); - client['websocket']?.dispatchEvent(new Event('close')); +forceCloseButton.addEventListener("click", () => { + console.log("force close button"); + client["websocket"]?.dispatchEvent(new Event("close")); }); const addTrack = async (stream: MediaStream): Promise => { - console.log('Add track'); + console.log("Add track"); const trackMetadata: TrackMetadata = { - type: 'camera', + type: "camera", active: true, }; const track = stream.getVideoTracks()[0]; @@ -395,53 +399,53 @@ const addTrack = async (stream: MediaStream): Promise => { const removeTrack = (track: Track) => { if (!track) return; - console.log('Remove track'); + console.log("Remove track"); track.id && client.removeTrack(track.id); track.id = null; }; -addTrackButton.addEventListener('click', async () => { +addTrackButton.addEventListener("click", async () => { remoteTracks.canvas = await addTrack(stream); localVideo.classList.add(...borderActiveClasses); }); -removeTrackButton.addEventListener('click', () => { +removeTrackButton.addEventListener("click", () => { removeTrack(remoteTracks.canvas); localVideo.classList.remove(...borderActiveClasses); }); -enumerateDevicesButton.addEventListener('click', () => { - enumerateDevices(true, false).then((result) => { +enumerateDevicesButton.addEventListener("click", async () => { + navigator.mediaDevices.enumerateDevices().then((result) => { console.log(result); - if (result.video.type !== 'OK') return; - - const videoPlayers = document.querySelector('#video-players')!; - videoPlayers.innerHTML = ''; + const videoPlayers = document.querySelector("#video-players")!; + videoPlayers.innerHTML = ""; // Video devices views - result.video.devices.forEach((device) => { + result.forEach((device) => { const clone = // @ts-ignore templateVideoPlayer.content.firstElementChild.cloneNode(true); - const videoPlayer = clone.querySelector('.video-player'); + const videoPlayer = clone.querySelector(".video-player"); - clone.querySelector('.device-label').innerHTML = device.label; - clone.querySelector('.device-id').innerHTML = device.deviceId; + clone.querySelector(".device-label").innerHTML = device.label; + clone.querySelector(".device-id").innerHTML = device.deviceId; clone - .querySelector('.start-template-btn') - .addEventListener('click', () => { - console.log('Start'); - getUserMedia(device.deviceId, 'video').then((stream) => { - console.log('Connecting stream'); - videoPlayer.srcObject = stream; - videoPlayer.play(); - }); + .querySelector(".start-template-btn") + .addEventListener("click", () => { + console.log("Start"); + navigator.mediaDevices + .getUserMedia({ video: true }) + .then((stream) => { + console.log("Connecting stream"); + videoPlayer.srcObject = stream; + videoPlayer.play(); + }); }); clone - .querySelector('.stop-template-btn') - .addEventListener('click', () => { - console.log('Stop'); + .querySelector(".stop-template-btn") + .addEventListener("click", () => { + console.log("Stop"); const stream = videoPlayer.srcObject; stream.getTracks().forEach((track: MediaStreamTrack) => { track.stop(); @@ -450,8 +454,8 @@ enumerateDevicesButton.addEventListener('click', () => { }); clone - .querySelector('.add-track-template-btn') - .addEventListener('click', async () => { + .querySelector(".add-track-template-btn") + .addEventListener("click", async () => { if (!videoPlayer.srcObject) return; remoteTracks.cameras[device.deviceId] = await addTrack( @@ -461,8 +465,8 @@ enumerateDevicesButton.addEventListener('click', () => { }); clone - .querySelector('.remove-track-template-btn') - .addEventListener('click', () => { + .querySelector(".remove-track-template-btn") + .addEventListener("click", () => { removeTrack(remoteTracks.cameras[device.deviceId]); videoPlayer.classList.remove(...borderActiveClasses); }); @@ -480,24 +484,24 @@ const templateClone = ( screenSharingContainer.appendChild(templateClone); const screenSharingVideo = templateClone.querySelector( - '.video-player', + ".video-player", )! as HTMLVideoElement; templateClone - .querySelector('.start-template-btn')! - .addEventListener('click', async () => { + .querySelector(".start-template-btn")! + .addEventListener("click", async () => { const stream = await navigator.mediaDevices.getDisplayMedia( SCREEN_SHARING_MEDIA_CONSTRAINTS, ); - console.log('Screen sharing stream'); + console.log("Screen sharing stream"); screenSharingVideo.srcObject = stream; await screenSharingVideo.play(); }); templateClone - .querySelector('.stop-template-btn')! - .addEventListener('click', () => { - console.log('Stop screen sharing'); + .querySelector(".stop-template-btn")! + .addEventListener("click", () => { + console.log("Stop screen sharing"); const stream = screenSharingVideo.srcObject as MediaStream; stream?.getTracks().forEach((track: MediaStreamTrack) => { track.stop(); @@ -506,8 +510,8 @@ templateClone }); templateClone - .querySelector('.add-track-template-btn')! - .addEventListener('click', async () => { + .querySelector(".add-track-template-btn")! + .addEventListener("click", async () => { if (!screenSharingVideo.srcObject) return; remoteTracks.screen = await addTrack( @@ -517,25 +521,25 @@ templateClone }); templateClone - .querySelector('.remove-track-template-btn')! - .addEventListener('click', () => { + .querySelector(".remove-track-template-btn")! + .addEventListener("click", () => { removeTrack(remoteTracks.screen); screenSharingVideo.classList.remove(...borderActiveClasses); }); (function hideRemotePeersIfEmpty() { const remotePeersContainer = document.getElementById( - 'remote-peers-container', + "remote-peers-container", )!; - const targetNode = document.getElementById('remote-peers')!; + const targetNode = document.getElementById("remote-peers")!; const config = { childList: true }; const callback = () => { if (targetNode.childElementCount === 0) { - remotePeersContainer.style.display = 'none'; + remotePeersContainer.style.display = "none"; } else { - remotePeersContainer.style.display = ''; + remotePeersContainer.style.display = ""; } }; @@ -547,7 +551,7 @@ templateClone // Toasts const templateAlert = document.getElementById( - 'toast-alert-template', + "toast-alert-template", )! as HTMLTemplateElement; function toastAlert(message: string) { @@ -555,7 +559,7 @@ function toastAlert(message: string) { } const templateInfo = document.getElementById( - 'toast-info-template', + "toast-info-template", )! as HTMLTemplateElement; function toastInfo(message: string) { @@ -563,7 +567,7 @@ function toastInfo(message: string) { } const templateSuccess = document.getElementById( - 'toast-success-template', + "toast-success-template", )! as HTMLTemplateElement; function toastSuccess(message: string) { @@ -572,29 +576,29 @@ function toastSuccess(message: string) { function toast(message: string, template: HTMLTemplateElement) { const hiddenClasses = [ - 'opacity-0', - '-translate-y-4', - 'scale-x-95', - 'h-0', - 'py-0', - 'mt-0', + "opacity-0", + "-translate-y-4", + "scale-x-95", + "h-0", + "py-0", + "mt-0", ]; const visibleClasses = [ - 'opacity-100', - 'translate-y-0', - 'scale-100', - 'h-[60px]', - 'mt-2', + "opacity-100", + "translate-y-0", + "scale-100", + "h-[60px]", + "mt-2", ]; - const toastContainer = document.getElementById('toast-container')!; + const toastContainer = document.getElementById("toast-container")!; const clone = template.content.firstElementChild!.cloneNode( true, )! as HTMLElement; let animationDuration: number = 0; clone.classList.forEach((c) => { - if (c.startsWith('duration-')) { - const [_, d] = c.split('-'); + if (c.startsWith("duration-")) { + const [_, d] = c.split("-"); const parsed = parseInt(d); if (isNaN(parsed)) return; @@ -608,7 +612,7 @@ function toast(message: string, template: HTMLTemplateElement) { clone.classList.add(...visibleClasses); }, 1); - clone.querySelector('.toast-message')!.innerHTML = message; + clone.querySelector(".toast-message")!.innerHTML = message; toastContainer.appendChild(clone); const hideAlert = () => { @@ -620,7 +624,7 @@ function toast(message: string, template: HTMLTemplateElement) { }, animationDuration); }; - clone.addEventListener('click', () => { + clone.addEventListener("click", () => { hideAlert(); }); diff --git a/examples/ts-client/simple-app/tailwind.config.cjs b/examples/ts-client/simple-app/tailwind.config.cjs index 0b328ed4..829c1bdb 100644 --- a/examples/ts-client/simple-app/tailwind.config.cjs +++ b/examples/ts-client/simple-app/tailwind.config.cjs @@ -1,10 +1,10 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: ['./index.html', './src/**/*.{js,ts}'], + content: ["./index.html", "./src/**/*.{js,ts}"], theme: { extend: {}, }, - plugins: [require('daisyui')], + plugins: [require("daisyui")], daisyui: { logs: false, }, diff --git a/examples/ts-client/simple-app/vite.config.ts b/examples/ts-client/simple-app/vite.config.ts index 95dc4c3d..8f875455 100644 --- a/examples/ts-client/simple-app/vite.config.ts +++ b/examples/ts-client/simple-app/vite.config.ts @@ -1,5 +1,5 @@ -import { defineConfig } from 'vite'; -import checker from 'vite-plugin-checker'; +import { defineConfig } from "vite"; +import checker from "vite-plugin-checker"; // import mkcert from 'vite-plugin-mkcert' // https://vitejs.dev/config/ @@ -15,7 +15,7 @@ export default defineConfig({ checker({ typescript: true, eslint: { - lintCommand: 'eslint --ext .ts', + lintCommand: "eslint --ext .ts", }, }), // mkcert(), diff --git a/package.json b/package.json index 44c3da13..1832d0ac 100644 --- a/package.json +++ b/package.json @@ -7,15 +7,29 @@ "packages/ts-client", "packages/react-client", "examples/ts-client/*", - "examples/react-client/*" + "examples/react-client/*", + "e2e-tests/ts-client/app", + "e2e-tests/react-client/app" ], "packageManager": "yarn@4.3.0", "scripts": { - "build": "yarn workspace @fishjam-dev/ts-client build && yarn workspace @fishjam-dev/react-client build", - "tsc": "yarn workspaces foreach -A -p run tsc", + "build": "yarn workspaces foreach -Apt run build", + "test:e2e": "yarn workspace @fishjam-e2e/ts-client-e2e e2e && yarn workspace @fishjam-e2e/react-client-e2e e2e", + "typecheck": "yarn workspaces foreach -A -p run typecheck", "format": "yarn workspaces foreach -A -p run format", "format:check": "yarn workspaces foreach -A -p run format:check", "lint": "yarn workspaces foreach -A -p run lint", "lint:check": "yarn workspaces foreach -A -p run lint:check" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^7.15.0", + "@typescript-eslint/parser": "^7.15.0", + "eslint": "^8.55.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "prettier": "^3.3.0", + "prettier-plugin-tailwindcss": "^0.6.5", + "typescript": "^5.5.0" } } diff --git a/packages/react-client/.eslintrc b/packages/react-client/.eslintrc index 5add10d3..9f9c7553 100644 --- a/packages/react-client/.eslintrc +++ b/packages/react-client/.eslintrc @@ -1,28 +1,4 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint", "react-hooks", "react-refresh"], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "prettier", - ], - "rules": { - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/consistent-type-imports": "error", - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/no-unused-vars": [ - "warn", - { - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_", - "caughtErrorsIgnorePattern": "^_", - }, - ], - "no-console": ["error", { "allow": ["warn", "error"] }], - "react-hooks/exhaustive-deps": "error", - "react-hooks/rules-of-hooks": "error", - "react-refresh/only-export-components": ["warn", { "allowConstantExport": true }], - }, + "extends": ["../../.eslintrc.js"], + "ignorePatterns": ["lib"], } diff --git a/packages/react-client/package.json b/packages/react-client/package.json index 54838c8d..feee7cc8 100644 --- a/packages/react-client/package.json +++ b/packages/react-client/package.json @@ -3,28 +3,25 @@ "version": "0.4.0", "description": "React client library for Fishjam.", "license": "Apache-2.0", - "author": "Membrane Team", + "author": "Fishjam Cloud Team", "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", "files": [ - "dist/src" + "dist/**" ], - "bugs": { - "url": "https://github.com/fishjam-dev/react-client-sdk/issues" - }, - "homepage": "https://github.com/fishjam-dev/react-client-sdk#readme", + "repository": "github:fishjam-cloud/web-client-sdk", + "homepage": "https://github.com/fishjam-cloud/web-client-sdk#readme", + "bugs": "https://github.com/fishjam-cloud/web-client-sdk/issues", "keywords": [ "webrtc", - "membrane", "fishjam" ], - "repository": { - "type": "git", - "url": "git://github.com/fishjam-dev/react-client-sdk.git" - }, "exports": { - ".": "./dist/index.js" + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } }, "typesVersions": { "*": { @@ -37,38 +34,31 @@ "build": "tsc", "e2e": "NODE_OPTIONS=--dns-result-order=ipv4first playwright test", "docs": "typedoc src src/experimental", - "format:fix": "prettier --write . --ignore-path ./.eslintignore", + "format": "prettier --write . --ignore-path ./.eslintignore", "format:check": "prettier --check . --ignore-path ./.eslintignore", - "lint:fix": "eslint . --ext .ts,.tsx --fix", + "lint": "eslint . --ext .ts,.tsx --fix", "lint:check": "eslint . --ext .ts,.tsx", - "prepare": "tsc" + "prepack": "yarn workspace @fishjam-dev/ts-client build && yarn build", + "typecheck": "tsc" }, "devDependencies": { - "@playwright/test": "^1.42.1", + "@playwright/test": "^1.45.1", "@types/events": "^3.0.3", "@types/lodash.isequal": "^4.5.8", "@types/node": "^20.11.27", "@types/react": "^18.2.55", - "@typescript-eslint/eslint-plugin": "^7.2.0", - "@typescript-eslint/parser": "^7.8.0", - "eslint": "^8.57.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.6", - "prettier": "^3.3.0", - "prettier-plugin-tailwindcss": "^0.5.12", "react": "^18.2.0", "typed-emitter": "^2.1.0", - "typedoc": "^0.25.12", - "typedoc-plugin-mdn-links": "^3.1.18", - "typescript": "^5.4.0" + "typedoc": "^0.26.3", + "typedoc-plugin-mdn-links": "^3.2.2" }, "dependencies": { - "@fishjam-dev/ts-client": "*", + "@fishjam-dev/ts-client": "workspace:*", "events": "3.3.0", "lodash.isequal": "4.5.0" }, "directories": { "example": "examples" - } + }, + "packageManager": "yarn@4.3.0" } diff --git a/packages/react-client/readme.md b/packages/react-client/readme.md index f4c487fa..171e3fbc 100644 --- a/packages/react-client/readme.md +++ b/packages/react-client/readme.md @@ -1,40 +1,44 @@ -# Fishjam React client +# Fishjam React Client -React client library for [Fishjam](https://github.com/fishjam-dev/fishjam). +React client library for [Fishjam Cloud](https://cloud.fishjam.stream). It is a wrapper around -the [Fishjam TS client](https://github.com/fishjam-dev/ts-client-sdk). +the [TS client](../ts-client/README.md). ## Documentation Documentation is available [here](https://fishjam-dev.github.io/react-client-sdk/) or you can generate it locally: ```bash -npm run docs +yarn run docs ``` ## Installation -You can install the library using `npm`: +You can install the library using `npm` or `yarn: ```bash -npm install @fishjam-dev/react-client +npm install @fishjam-cloud/react-client ``` -It was tested with `nodejs` version mentioned in `.tool-versions` file. +```bash +yarn add @fishjam-cloud/react-client +``` + +It was tested with `nodejs` version mentioned in [`.tool-versions`](./.tool-versions) file. ## Usage For pure TypeScript usage, -see [Fishjam TS client](https://github.com/fishjam-dev/ts-client-sdk). +see [TS client](../ts-client/README.md). Prerequisites: -- Running [Fishjam](https://github.com/fishjam-dev/fishjam) server. +- Account on Fishjam Cloud with App configured. - Created room and token of peer in that room. - You can use [dashboard](https://github.com/fishjam-dev/fishjam-dashboard) to create room and peer token. + You can use Room Manager to create room and peer token. This snippet is based -on [minimal-react](https://github.com/fishjam-dev/react-client-sdk/tree/main/examples/minimal-react) example. +on [minimal-react](../../examples/react-client/minimal-react/) example. ```tsx // main.tsx @@ -66,7 +70,7 @@ export type TrackMetadata = { type: "camera" | "screen"; }; -// Create a Membrane client instance +// Create a Fishjam client instance // remember to use FishjamContextProvider export const { useApi, useTracks, useStatus, useConnect, useDisconnect, FishjamContextProvider } = create< PeerMetadata, @@ -129,12 +133,6 @@ export const App = () => { }; ``` -## Contributing - -We welcome contributions to this SDK. Please report any bugs or issues you find or feel free to make a pull request with your own bug fixes and/or features. - -Detailed information about contributing to Fishjam Dashboard can be found in [contributing](./CONTRIBUTING.md) document. - ### Releasing new versions To release a new version of the package, go to `Actions` > `Release package` workflow and trigger it with the chosen release type. @@ -142,27 +140,12 @@ The workflow will bump the package version in `package.json`, release the packag ## Examples -For examples, see [examples](https://github.com/fishjam-dev/react-client-sdk/tree/main/examples) folder. - -More information about usage of webrtc can be found -in [MembraneWebRTC documentation](https://fishjam-dev.github.io/membrane-webrtc-js/). - -## Fishjam ecosystem - -| | | -| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Client SDKs | [React](https://github.com/fishjam-dev/react-client-sdk), [React Native](https://github.com/fishjam-dev/react-native-client-sdk), [iOs](https://github.com/fishjam-dev/ios-client-sdk), [Android](https://github.com/fishjam-dev/android-client-sdk) | -| Server SDKs | [Elixir](https://github.com/fishjam-dev/elixir_server_sdk), [Python](https://github.com/fishjam-dev/python-server-sdk), [OpenAPI](https://fishjam-dev.github.io/fishjam-docs/api_reference/rest_api) | -| Services | [Videoroom](https://github.com/fishjam-dev/fishjam-videoroom) - an example videoconferencing app written in elixir
[Dashboard](https://github.com/fishjam-dev/fishjam-dashboard) - an internal tool used to showcase Fishjam's capabilities | -| Resources | [Fishjam Book](https://fishjam-dev.github.io/book/) - theory of the framework, [Docs](https://fishjam-dev.github.io/fishjam-docs/), [Tutorials](https://github.com/fishjam-dev/fishjam-clients-tutorials) | -| Membrane | Fishjam is based on [Membrane](https://membrane.stream/), [Discord](https://discord.gg/nwnfVSY) | -| Compositor | [Compositor](https://github.com/membraneframework/membrane_video_compositor_plugin) - Membrane plugin to transform video | -| Protobufs | If you want to use Fishjam on your own, you can use our [protobufs](https://github.com/fishjam-dev/protos) | +For examples, see [examples](../../examples/react-client/) folder. ## Copyright and License -Copyright 2023, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=fishjam) +Copyright 2024, [Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=react-client) -[![Software Mansion](https://logo.swmansion.com/logo?color=white&variant=desktop&width=200&tag=membrane-github)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=fishjam) +[![Software Mansion](https://logo.swmansion.com/logo?color=white&variant=desktop&width=200&tag=react-client)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=react-client) Licensed under the [Apache License, Version 2.0](LICENSE) diff --git a/packages/react-client/src/DeviceManager.ts b/packages/react-client/src/DeviceManager.ts index 478c5b4a..bd6785ba 100644 --- a/packages/react-client/src/DeviceManager.ts +++ b/packages/react-client/src/DeviceManager.ts @@ -1,5 +1,4 @@ import type { - AudioOrVideoType, CurrentDevices, DeviceError, DeviceManagerConfig, @@ -25,6 +24,7 @@ import { import EventEmitter from "events"; import type TypedEmitter from "typed-emitter"; import type { TrackType } from "./ScreenShareManager"; +import type { TrackKind } from "@fishjam-dev/ts-client"; const removeExact = ( trackConstraints: boolean | MediaTrackConstraints | undefined, @@ -164,9 +164,9 @@ const handleNotAllowedError = async (constraints: MediaStreamConstraints): Promi return await getMedia({ video: false, audio: false }, { video: PERMISSION_DENIED, audio: PERMISSION_DENIED }); }; -const getError = (result: GetMedia, type: AudioOrVideoType): DeviceError | null => { +const getError = (result: GetMedia, kind: TrackKind): DeviceError | null => { if (result.type === "OK") { - return result.previousErrors[type] || null; + return result.previousErrors[kind] || null; } console.warn({ name: "Unhandled DeviceManager error", result }); @@ -477,9 +477,9 @@ export class DeviceManager extends (EventEmitter as new () => TypedEmitter { - if (trackId === this?.[type].media?.track?.id) { - await this.stop(type); + private onTrackEnded = async (kind: TrackKind, trackId: string) => { + if (trackId === this?.[kind].media?.track?.id) { + await this.stop(kind); } }; @@ -637,14 +637,14 @@ export class DeviceManager extends (EventEmitter as new () => TypedEmitter TypedEmitter< } } - private onTrackEnded = async (type: AudioOrVideoType, trackId: string) => { - const mediaType = type === "video" ? "videoMedia" : "audioMedia"; + private onTrackEnded = async (kind: TrackKind, trackId: string) => { + const mediaType = kind === "video" ? "videoMedia" : "audioMedia"; if (trackId === this?.data[mediaType]?.track?.id) { await this.stop("audiovideo"); } diff --git a/packages/react-client/src/types.ts b/packages/react-client/src/types.ts index 13c357c5..08b213e7 100644 --- a/packages/react-client/src/types.ts +++ b/packages/react-client/src/types.ts @@ -4,8 +4,6 @@ import type { PeerStatus, Selector, State, Track, TrackId, TrackWithOrigin, UseR import type { JSX, ReactNode } from "react"; import type { Client } from "./Client"; -export type AudioOrVideoType = "audio" | "video"; - export type DevicesStatus = "OK" | "Error" | "Not requested" | "Requesting"; export type MediaStatus = "OK" | "Error" | "Not requested" | "Requesting"; diff --git a/packages/react-client/typedoc.json b/packages/react-client/typedoc.json index 43698d10..5631c776 100644 --- a/packages/react-client/typedoc.json +++ b/packages/react-client/typedoc.json @@ -1,8 +1,3 @@ { - "externalSymbolLinkMappings": { - "@fishjam-dev/membrane-webrtc-js": { - "*": "https://fishjam-dev.github.io/membrane-webrtc-js/" - } - }, "plugin": ["typedoc-plugin-mdn-links"] } diff --git a/packages/ts-client/.eslintrc b/packages/ts-client/.eslintrc index 86c64dfd..9f9c7553 100644 --- a/packages/ts-client/.eslintrc +++ b/packages/ts-client/.eslintrc @@ -1,24 +1,4 @@ { - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint"], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "prettier" - ], - "rules": { - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/no-unused-vars": [ - "warn", - { - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_", - "caughtErrorsIgnorePattern": "^_" - } - ], - "no-console": ["error", { "allow": ["warn", "error"] }] - } + "extends": ["../../.eslintrc.js"], + "ignorePatterns": ["lib"], } diff --git a/packages/ts-client/.husky/pre-commit b/packages/ts-client/.husky/pre-commit index 8aff43c0..5dcc1882 100644 --- a/packages/ts-client/.husky/pre-commit +++ b/packages/ts-client/.husky/pre-commit @@ -1,2 +1,2 @@ npx lint-staged -npm run build:check +yarn build \ No newline at end of file diff --git a/packages/ts-client/README.md b/packages/ts-client/README.md index 8eb66628..4e7cb9a1 100644 --- a/packages/ts-client/README.md +++ b/packages/ts-client/README.md @@ -1,17 +1,23 @@ -[![NPM Version](https://img.shields.io/npm/v/@fishjam-dev/ts-client)](https://www.npmjs.com/package/@fishjam-dev/ts-client) +[![NPM Version](https://img.shields.io/npm/v/@fishjam-dev/ts-client)](https://www.npmjs.com/package/@fishjam-cloud/ts-client) [![TypeScript Strict](https://badgen.net/badge/TS/Strict)](https://www.typescriptlang.org) -[![TypeDoc](https://img.shields.io/badge/TypeDoc-8A2BE2)](https://fishjam-dev.github.io/ts-client-sdk/) +[![TypeDoc](https://img.shields.io/badge/TypeDoc-8A2BE2)](https://fishjam-cloud.github.io/web-client-sdk/) # Fishjam TS Client -TypeScript client library for [Fishjam](https://github.com/fishjam-dev/fishjam). +TypeScript client library for [Fishjam Cloud](https://cloud.fishjam.stream). ## Installation You can install this package using `npm`: ```bash -npm install @fishjam-dev/ts-client +npm install @fishjam-cloud/ts-client +``` + +or `yarn`: + +```bash +yarn @fishjam-cloud/react-client ``` ## Documentation @@ -26,13 +32,12 @@ production, refer to the Prerequisites: -- A running [Fishjam](https://github.com/fishjam-dev/fishjam) server. -- A created room and a peer's token in that room. You can use the - [dashboard](https://github.com/fishjam-dev/fishjam-dashboard) example to - create a room and a peer token. +- Account on Fishjam Cloud with App configured. +- Created room and token of peer in that room. You can use Room Manager to + create room and peer token. -The following code snippet is based on the [minimal](./examples/minimal) -example. +The following code snippet is based on the +[minimal](../../examples/ts-client//minimal/) example. ```ts import { FishjamClient, WebRTCEndpoint } from '@fishjam-dev/ts-client'; @@ -117,31 +122,13 @@ async function startScreenSharing(webrtc: WebRTCEndpoint) { ## Examples -For more examples, see the [examples](./examples) folder. - -## Contributing - -We welcome contributions to the Fishjam TS Client. Please report any bugs or -issues that you find, or feel free to make a pull request with your own bug -fixes and/or features. - -More detailed information about contributing can be found in -[CONTRIBUTING.md](./CONTRIBUTING.md). - -## Fishjam Ecosystem - -| | | -| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| ๐Ÿ“ฑ Client SDKs | [TypeScript](https://github.com/fishjam-dev/ts-client-sdk/)
[React](https://github.com/fishjam-dev/react-client-sdk)
[iOS](https://github.com/fishjam-dev/ios-client-sdk)
[Android](https://github.com/fishjam-dev/android-client-sdk)
[React Native](https://github.com/fishjam-dev/react-native-client-sdk) | -| โš™๏ธ Server SDKs | [JavaScript](https://github.com/fishjam-dev/js-server-sdk)
[Python](https://github.com/fishjam-dev/python-server-sdk)
[Elixir](https://github.com/fishjam-dev/elixir_server_sdk) | -| ๐Ÿ“š Resources | [Fishjam Docs](https://fishjam-dev.github.io/fishjam-docs/)
[Membrane Framework](https://membrane.stream/)
[Join Membrane Discord!](https://discord.gg/nwnfVSY) | -| ๐Ÿซ™ Services | [Videoroom](https://github.com/fishjam-dev/fishjam-videoroom)
A videoconferencing app built on top of Fishjam

[Dashboard](https://github.com/fishjam-dev/fishjam-dashboard)
An all-around development tool for Fishjam | +For more examples, see the [examples](../../examples/ts-client/) folder. ## Copyright and License Copyright 2024, -[Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=fishjam) +[Software Mansion](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=fishjam-ts) -[![Software Mansion](https://logo.swmansion.com/logo?color=white&variant=desktop&width=200&tag=membrane-github)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=fishjam) +[![Software Mansion](https://logo.swmansion.com/logo?color=white&variant=desktop&width=200&tag=fishjam-github)](https://swmansion.com/?utm_source=git&utm_medium=readme&utm_campaign=fishjam-ts) Licensed under the [Apache License, Version 2.0](LICENSE) diff --git a/packages/ts-client/package.json b/packages/ts-client/package.json index 079c0439..db074104 100644 --- a/packages/ts-client/package.json +++ b/packages/ts-client/package.json @@ -3,22 +3,32 @@ "version": "0.5.0", "description": "Typescript client library for Fishjam", "license": "Apache-2.0", - "author": "Software Mansion (https://swmansion.com)", - "repository": "github:fishjam-dev/ts-client-sdk", - "homepage": "https://github.com/fishjam-dev/ts-client-sdk#readme", - "bugs": "https://github.com/fishjam-dev/ts-client-sdk/issues", + "author": "Fishjam Cloud Team", + "repository": "github:fishjam-cloud/web-client-sdk", + "homepage": "https://github.com/fishjam-cloud/web-client-sdk#readme", + "bugs": "https://github.com/fishjam-cloud/web-client-sdk/issues", "keywords": [ "webrtc", - "membrane", "fishjam" ], "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "files": [ - "dist/src" + "dist/src/**" ], "exports": { - ".": "./dist/src/index.js" + ".": { + "import": "./dist/src/index.js", + "types": "./dist/src/index.d.ts" + }, + "./protos": { + "import": "./dist/src/protos/index.js", + "types": "./dist/src/protos/index.d.ts" + }, + "./webrtc": { + "import": "./dist/src/webrtc/index.js", + "types": "./dist/src/webrtc/index.d.ts" + } }, "scripts": { "build": "tsc", @@ -32,46 +42,41 @@ "test": "vitest run tests/**", "test:e2e": "NODE_OPTIONS=--dns-result-order=ipv4first playwright test", "test:coverage": "vitest run tests/** --coverage", - "prepare": "tsc" + "typecheck": "tsc", + "prepare": "tsc", + "prepack": "yarn build" }, "dependencies": { "events": "^3.3.0", "protobufjs": "^7.3.0", "typed-emitter": "^2.1.0", - "uuid": "^9.0.1" + "uuid": "^10.0.0" }, "devDependencies": { - "@playwright/test": "^1.40.1", + "@playwright/test": "^1.45.1", "@types/events": "^3.0.3", "@types/node": "^20.10.3", - "@types/uuid": "^9.0.8", - "@typescript-eslint/eslint-plugin": "^7.8.0", - "@typescript-eslint/parser": "^7.8.0", + "@types/uuid": "^10.0.0", "@vitest/coverage-v8": "^1.6.0", - "eslint": "^8.55.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-react-hooks": "^4.6.0", "fake-mediastreamtrack": "^1.2.0", "husky": "^9.0.11", "lint-staged": "^15.2.5", - "prettier": "^3.3.0", - "prettier-plugin-tailwindcss": "^0.5.12", "react": "^18.2.0", "ts-proto": "^1.176.0", "typed-emitter": "^2.1.0", - "typedoc": "^0.25.13", + "typedoc": "^0.26.3", "typedoc-plugin-external-resolver": "^1.0.3", - "typedoc-plugin-mdn-links": "^3.1.6", - "typescript": "^5.4.0", + "typedoc-plugin-mdn-links": "^3.2.2", "vitest": "^1.6.0", "zod": "^3.23.6" }, "lint-staged": { "*": [ - "npm run format:check" + "yarn format:check" ], "*.(js|ts|tsx)": [ - "npm run lint:check" + "yarn lint:check" ] - } + }, + "packageManager": "yarn@4.3.0" } diff --git a/packages/ts-client/src/FishjamClient.ts b/packages/ts-client/src/FishjamClient.ts index 74be7632..380fdd0a 100644 --- a/packages/ts-client/src/FishjamClient.ts +++ b/packages/ts-client/src/FishjamClient.ts @@ -1,6 +1,5 @@ -import { +import type { BandwidthLimit, - WebRTCEndpoint, Endpoint, SerializedMediaEvent, SimulcastConfig, @@ -10,12 +9,14 @@ import { MetadataParser, WebRTCEndpointEvents, } from './webrtc'; -import TypedEmitter from 'typed-emitter'; +import { WebRTCEndpoint } from './webrtc'; +import type TypedEmitter from 'typed-emitter'; import { EventEmitter } from 'events'; import { PeerMessage } from './protos'; -import { ReconnectConfig, ReconnectManager } from './reconnection'; -import { AuthErrorReason, isAuthError } from './auth'; -import { Deferred } from './webrtc/deferred'; +import type { ReconnectConfig } from './reconnection'; +import { ReconnectManager } from './reconnection'; +import type { AuthErrorReason } from './auth'; +import { isAuthError } from './auth'; export type Peer = Endpoint< PeerMetadata, diff --git a/packages/ts-client/src/reconnection.ts b/packages/ts-client/src/reconnection.ts index ec8f3fe7..64423263 100644 --- a/packages/ts-client/src/reconnection.ts +++ b/packages/ts-client/src/reconnection.ts @@ -1,5 +1,5 @@ -import { Endpoint } from './webrtc'; -import { FishjamClient, MessageEvents } from './FishjamClient'; +import type { Endpoint } from './webrtc'; +import type { FishjamClient, MessageEvents } from './FishjamClient'; import { isAuthError } from './auth'; export type ReconnectionStatus = 'reconnecting' | 'idle' | 'error'; diff --git a/packages/ts-client/src/webrtc/.eslintrc b/packages/ts-client/src/webrtc/.eslintrc new file mode 100644 index 00000000..944d8e3f --- /dev/null +++ b/packages/ts-client/src/webrtc/.eslintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "@typescript-eslint/no-explicit-any": "off", + }, +} diff --git a/packages/ts-client/src/webrtc/bandwidth.ts b/packages/ts-client/src/webrtc/bandwidth.ts index 43793904..f2b93a3b 100644 --- a/packages/ts-client/src/webrtc/bandwidth.ts +++ b/packages/ts-client/src/webrtc/bandwidth.ts @@ -1,4 +1,4 @@ -import { +import type { SimulcastBandwidthLimit, TrackBandwidthLimit, TrackEncoding, diff --git a/packages/ts-client/src/webrtc/bitrate.ts b/packages/ts-client/src/webrtc/bitrate.ts index 30e59b98..448eea61 100644 --- a/packages/ts-client/src/webrtc/bitrate.ts +++ b/packages/ts-client/src/webrtc/bitrate.ts @@ -1,10 +1,11 @@ -import { - type BandwidthLimit, +import type { RemoteTrackId, TrackContext, TrackEncoding, + TrackKind, } from './types'; -import { TrackContextImpl } from './internal'; +import { type BandwidthLimit } from './types'; +import type { TrackContextImpl } from './internal'; import { findSender } from './RTCPeerConnectionUtils'; import { generateCustomEvent } from './mediaEvent'; @@ -73,6 +74,9 @@ export const getTrackIdToTrackBitrates = ( return trackIdToTrackBitrates; }; +const isNotSimulcastTrack = (encodings: RTCRtpEncodingParameters[]) => + encodings.length === 1 && !encodings[0].rid; + export const getTrackBitrates = ( connection: RTCPeerConnection | undefined, localTrackIdToTrack: Map< @@ -84,16 +88,28 @@ export const getTrackBitrates = ( const trackContext = localTrackIdToTrack.get(trackId); if (!trackContext) throw "Track with id ${trackId} not present in 'localTrackIdToTrack'"; - const kind = trackContext.track?.kind as 'audio' | 'video' | undefined; + + const kind = trackContext.track?.kind as TrackKind | undefined; + + if (!trackContext.track) { + if (!trackContext.trackKind) { + throw new Error('trackContext.trackKind is empty'); + } + + return defaultBitrates[trackContext.trackKind]; + } + const sender = findSender(connection, trackContext.track!.id); const encodings = sender.getParameters().encodings; - if (encodings.length == 1 && !encodings[0].rid) + if (isNotSimulcastTrack(encodings)) { return ( encodings[0].maxBitrate || (kind ? defaultBitrates[kind] : UNLIMITED_BANDWIDTH) ); - else if (kind == 'audio') throw 'Audio track cannot have multiple encodings'; + } else if (kind === 'audio') { + throw 'Audio track cannot have multiple encodings'; + } const bitrates: Record = {}; diff --git a/packages/ts-client/src/webrtc/commands.ts b/packages/ts-client/src/webrtc/commands.ts index 8f1d357a..8c4bb850 100644 --- a/packages/ts-client/src/webrtc/commands.ts +++ b/packages/ts-client/src/webrtc/commands.ts @@ -1,5 +1,5 @@ -import { SimulcastConfig, TrackBandwidthLimit } from './types'; -import { Deferred } from './deferred'; +import type { SimulcastConfig, TrackBandwidthLimit } from './types'; +import type { Deferred } from './deferred'; export type AddTrackCommand = { commandType: 'ADD-TRACK'; diff --git a/packages/ts-client/src/webrtc/const.ts b/packages/ts-client/src/webrtc/const.ts index 372c778f..b9483080 100644 --- a/packages/ts-client/src/webrtc/const.ts +++ b/packages/ts-client/src/webrtc/const.ts @@ -1,6 +1,3 @@ -import type { BandwidthLimit, TrackEncoding } from './types'; -// const TEMPORAL_LAYERS_COUNT = 2; - export const simulcastTransceiverConfig: RTCRtpTransceiverInit = { direction: 'sendonly', // keep this array from low resolution to high resolution diff --git a/packages/ts-client/src/webrtc/index.ts b/packages/ts-client/src/webrtc/index.ts index dd54cd7c..3c0f5638 100644 --- a/packages/ts-client/src/webrtc/index.ts +++ b/packages/ts-client/src/webrtc/index.ts @@ -12,6 +12,7 @@ export type { EncodingReason, Config, MetadataParser, + TrackKind, } from './types'; export { WebRTCEndpoint } from './webRTCEndpoint'; diff --git a/packages/ts-client/src/webrtc/internal.ts b/packages/ts-client/src/webrtc/internal.ts index cf00586f..57bae377 100644 --- a/packages/ts-client/src/webrtc/internal.ts +++ b/packages/ts-client/src/webrtc/internal.ts @@ -1,6 +1,6 @@ import EventEmitter from 'events'; -import TypedEmitter from 'typed-emitter'; -import { +import type TypedEmitter from 'typed-emitter'; +import type { EncodingReason, Endpoint, MetadataParser, @@ -9,10 +9,14 @@ import { TrackContext, TrackContextEvents, TrackEncoding, + TrackKind, TrackNegotiationStatus, VadStatus, } from './types'; +export const isTrackKind = (kind: string): kind is TrackKind => + kind === 'audio' || kind === 'video'; + export class TrackContextImpl extends (EventEmitter as { new (): TypedEmitter< @@ -24,6 +28,7 @@ export class TrackContextImpl endpoint: Endpoint; trackId: string; track: MediaStreamTrack | null = null; + trackKind: TrackKind | null = null; stream: MediaStream | null = null; metadata?: ParsedMetadata; rawMetadata: any; diff --git a/packages/ts-client/src/webrtc/sdpEvents.ts b/packages/ts-client/src/webrtc/sdpEvents.ts index 2ea4e091..91b384bd 100644 --- a/packages/ts-client/src/webrtc/sdpEvents.ts +++ b/packages/ts-client/src/webrtc/sdpEvents.ts @@ -1,8 +1,8 @@ import { generateCustomEvent } from './mediaEvent'; import { getTrackIdToTrackBitrates } from './bitrate'; import { getMidToTrackId } from './transciever'; -import { RemoteTrackId, TrackContext } from './types'; -import { TrackContextImpl } from './internal'; +import type { RemoteTrackId, TrackContext } from './types'; +import type { EndpointWithTrackContext, TrackContextImpl } from './internal'; export const createSdpOfferEvent = ( offer: RTCSessionDescriptionInit, @@ -11,19 +11,25 @@ export const createSdpOfferEvent = ( RemoteTrackId, TrackContextImpl >, - tracks: Map>, + localEndpoint: EndpointWithTrackContext, + midToTrackId: Map, ) => generateCustomEvent({ type: 'sdpOffer', data: { sdpOffer: offer, - trackIdToTrackMetadata: getTrackIdToMetadata(tracks), + trackIdToTrackMetadata: getTrackIdToMetadata(localEndpoint.tracks), trackIdToTrackBitrates: getTrackIdToTrackBitrates( connection, localTrackIdToTrack, - tracks, + localEndpoint.tracks, + ), + midToTrackId: getMidToTrackId( + connection, + localTrackIdToTrack, + midToTrackId, + localEndpoint, ), - midToTrackId: getMidToTrackId(connection, localTrackIdToTrack), }, }); diff --git a/packages/ts-client/src/webrtc/transceivers.ts b/packages/ts-client/src/webrtc/transceivers.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/ts-client/src/webrtc/transciever.ts b/packages/ts-client/src/webrtc/transciever.ts index 9768f6b1..8c9a75f8 100644 --- a/packages/ts-client/src/webrtc/transciever.ts +++ b/packages/ts-client/src/webrtc/transciever.ts @@ -1,7 +1,7 @@ -import { RemoteTrackId, TrackContext, TrackEncoding } from './types'; +import type { RemoteTrackId, TrackContext, TrackEncoding } from './types'; import { simulcastTransceiverConfig } from './const'; import { applyBandwidthLimitation } from './bandwidth'; -import { TrackContextImpl } from './internal'; +import type { EndpointWithTrackContext, TrackContextImpl } from './internal'; export const addTransceiversIfNeeded = ( connection: RTCPeerConnection | undefined, @@ -126,26 +126,85 @@ export const setTransceiversToReadOnly = (connection: RTCPeerConnection) => { .forEach((transceiver) => (transceiver.direction = 'sendonly')); }; +type Mid = string; +type TrackId = string; +type MidToTrackId = Record; + export const getMidToTrackId = ( connection: RTCPeerConnection | undefined, localTrackIdToTrack: Map< RemoteTrackId, TrackContextImpl >, -): Record | null => { - const localTrackMidToTrackId: Record = {}; - + midToTrackId: Map, + localEndpoint: EndpointWithTrackContext, +): MidToTrackId | null => { if (!connection) return null; - connection.getTransceivers().forEach((transceiver) => { - const localTrackId = transceiver.sender.track?.id; - const mid = transceiver.mid; - if (localTrackId && mid) { - const trackContext = Array.from(localTrackIdToTrack.values()).find( - (trackContext) => trackContext!.track!.id === localTrackId, - )!; - localTrackMidToTrackId[mid] = trackContext.trackId; - } - }); - - return localTrackMidToTrackId; + + // - negotiated unmuted tracks: tracks added in previous negotiation, data is being transmitted + // - not yet negotiated tracks: tracks added in this negotiation, data will be transmitted after successful negotiation + const mappingFromTransceivers = getTransceiverMapping( + connection, + localTrackIdToTrack, + ); + + // - negotiated unmuted tracks: tracks added in previous negotiation, data is being transmitted + // - negotiated muted tracks: tracks added in previous negotiation, data is not being transmitted but can be transmitted in the future + const mappingFromLocalNegotiatedTracks = getAllNegotiatedLocalTracksMapping( + midToTrackId, + localEndpoint, + ); + + return { ...mappingFromTransceivers, ...mappingFromLocalNegotiatedTracks }; +}; + +const getTrackContext = ( + localTrackIdToTrack: Map< + RemoteTrackId, + TrackContextImpl + >, + localTrackId: string, +) => + Array.from(localTrackIdToTrack.values()).find( + (trackContext) => trackContext?.track?.id === localTrackId, + )!; + +const getTransceiverMapping = ( + connection: RTCPeerConnection, + localTrackIdToTrack: Map< + RemoteTrackId, + TrackContextImpl + >, +): MidToTrackId => + connection + .getTransceivers() + .filter((transceiver) => transceiver.sender.track?.id && transceiver.mid) + .reduce( + (acc, transceiver) => { + const localTrackId = transceiver.sender.track!.id; + const mid = transceiver!.mid!; + + const trackContext = getTrackContext(localTrackIdToTrack, localTrackId); + + acc[mid] = trackContext.trackId; + + return acc; + }, + {} as Record, + ); + +const getAllNegotiatedLocalTracksMapping = ( + midToTrackId: Map = new Map(), + localEndpoint: EndpointWithTrackContext, +): MidToTrackId => { + return [...midToTrackId.entries()] + .filter(([_mid, trackId]) => localEndpoint.tracks.get(trackId)) + .reduce( + (acc, [mid, trackId]) => { + acc[mid] = trackId; + + return acc; + }, + {} as Record, + ); }; diff --git a/packages/ts-client/src/webrtc/types.ts b/packages/ts-client/src/webrtc/types.ts index 0c7af5ef..907148e3 100644 --- a/packages/ts-client/src/webrtc/types.ts +++ b/packages/ts-client/src/webrtc/types.ts @@ -1,5 +1,5 @@ -import TypedEmitter from 'typed-emitter'; -import { SerializedMediaEvent } from './mediaEvent'; +import type TypedEmitter from 'typed-emitter'; +import type { SerializedMediaEvent } from './mediaEvent'; export type MetadataParser = ( rawMetadata: unknown, @@ -7,6 +7,8 @@ export type MetadataParser = ( export type LocalTrackId = string; export type RemoteTrackId = string; +export type TrackKind = 'audio' | 'video'; + /** * Type describing Voice Activity Detection statuses. * diff --git a/packages/ts-client/src/webrtc/voiceActivityDetection.ts b/packages/ts-client/src/webrtc/voiceActivityDetection.ts index cdc07f4d..e656dc8f 100644 --- a/packages/ts-client/src/webrtc/voiceActivityDetection.ts +++ b/packages/ts-client/src/webrtc/voiceActivityDetection.ts @@ -1,6 +1,6 @@ -import { MediaEvent } from './mediaEvent'; -import { TrackContextImpl } from './internal'; -import { VadStatus } from './types'; +import type { MediaEvent } from './mediaEvent'; +import type { TrackContextImpl } from './internal'; +import type { VadStatus } from './types'; const vadStatuses = ['speech', 'silence'] as const; diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 52efccf2..d4d0ec23 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -1,22 +1,21 @@ +import type { MediaEvent, SerializedMediaEvent } from './mediaEvent'; import { deserializeMediaEvent, generateCustomEvent, generateMediaEvent, - MediaEvent, - SerializedMediaEvent, serializeMediaEvent, } from './mediaEvent'; import { v4 as uuidv4 } from 'uuid'; import EventEmitter from 'events'; -import TypedEmitter from 'typed-emitter'; -import { +import type TypedEmitter from 'typed-emitter'; +import type { AddTrackCommand, Command, RemoveTrackCommand, ReplaceTackCommand, } from './commands'; import { Deferred } from './deferred'; -import { +import type { BandwidthLimit, Config, LocalTrackId, @@ -28,24 +27,22 @@ import { TrackEncoding, WebRTCEndpointEvents, } from './types'; -import { EndpointWithTrackContext, TrackContextImpl } from './internal'; +import type { EndpointWithTrackContext } from './internal'; +import { TrackContextImpl, isTrackKind } from './internal'; import { handleVoiceActivationDetectionNotification } from './voiceActivityDetection'; import { applyBandwidthLimitation } from './bandwidth'; +import { createTrackVariantBitratesEvent, getTrackBitrates } from './bitrate'; import { - createTrackVariantBitratesEvent, - getTrackBitrates, -} from './bitrate'; + findSender, + findSenderByTrack, + isTrackInUse, +} from './RTCPeerConnectionUtils'; import { addTrackToConnection, addTransceiversIfNeeded, setTransceiverDirection, setTransceiversToReadOnly, } from './transciever'; -import { - findSender, - findSenderByTrack, - isTrackInUse, -} from './RTCPeerConnectionUtils'; import { createSdpOfferEvent } from './sdpEvents'; /** @@ -734,9 +731,12 @@ export class WebRTCEndpoint< this.trackMetadataParser, ); + if (!isTrackKind(track.kind)) throw new Error('Track has no kind'); + trackContext.track = track; trackContext.stream = stream; trackContext.maxBandwidth = maxBandwidth; + trackContext.trackKind = track.kind; this.localEndpoint.tracks.set(trackId, trackContext); @@ -844,6 +844,8 @@ export class WebRTCEndpoint< ) { const { trackId, newTrack, newTrackMetadata } = command; + // todo add validation to track.kind, you cannot replace video with audio + const trackContext = this.localTrackIdToTrack.get(trackId)!; const track = this.trackIdToSender.get(trackId); @@ -874,8 +876,6 @@ export class WebRTCEndpoint< this.emit('localTrackUnmuted', { trackId: trackId }); } - trackContext.track = newTrack; - track.localTrackId = newTrack?.id ?? null; try { @@ -1131,7 +1131,7 @@ export class WebRTCEndpoint< /** * Disables track encoding so that it will be no longer sent to the server. * @param {string} trackId - id of track - * @param {rackEncoding} encoding - encoding that will be disabled + * @param {TrackEncoding} encoding - encoding that will be disabled * @example * ```ts * const trackId = webrtc.addTrack(track, stream, {}, {enabled: true, activeEncodings: ["l", "m", "h"]}); @@ -1317,7 +1317,8 @@ export class WebRTCEndpoint< offer, this.connection, this.localTrackIdToTrack, - this.localEndpoint.tracks, + this.localEndpoint, + this.midToTrackId, ); this.sendMediaEvent(mediaEvent); @@ -1471,13 +1472,16 @@ export class WebRTCEndpoint< const mid = event.transceiver.mid!; const trackId = this.midToTrackId.get(mid)!; + if (this.checkIfTrackBelongToEndpoint(trackId, this.localEndpoint)) return; + if (!isTrackKind(event.track.kind)) throw new Error('Track has no kind'); const trackContext = this.trackIdToTrack.get(trackId)!; trackContext.stream = stream; trackContext.track = event.track; + trackContext.trackKind = event.track.kind; this.idToEndpoint .get(trackContext.endpoint.id) diff --git a/packages/ts-client/tests/.eslintrc b/packages/ts-client/tests/.eslintrc new file mode 100644 index 00000000..944d8e3f --- /dev/null +++ b/packages/ts-client/tests/.eslintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "@typescript-eslint/no-explicit-any": "off", + }, +} diff --git a/packages/ts-client/tests/events/connectedEvent.test.ts b/packages/ts-client/tests/events/connectedEvent.test.ts index 4b4fff2f..f5161b5b 100644 --- a/packages/ts-client/tests/events/connectedEvent.test.ts +++ b/packages/ts-client/tests/events/connectedEvent.test.ts @@ -4,7 +4,8 @@ import { createSimulcastTrack, trackId, } from '../fixtures'; -import { Endpoint, WebRTCEndpoint } from '../../src'; +import type { Endpoint } from '../../src'; +import { WebRTCEndpoint } from '../../src'; import { expect, vi, it } from 'vitest'; it('Connecting to empty room produce event', () => diff --git a/packages/ts-client/tests/events/trackAddedEvent.test.ts b/packages/ts-client/tests/events/trackAddedEvent.test.ts index 650bf019..b237e929 100644 --- a/packages/ts-client/tests/events/trackAddedEvent.test.ts +++ b/packages/ts-client/tests/events/trackAddedEvent.test.ts @@ -6,7 +6,7 @@ import { createCustomOfferDataEventWithOneVideoTrack, trackId, } from '../fixtures'; -import { CustomOfferDataEvent, TracksAddedMediaEvent } from '../schema'; +import type { CustomOfferDataEvent, TracksAddedMediaEvent } from '../schema'; import { mockRTCPeerConnection } from '../mocks'; import { deserializeMediaEvent } from '../../src/webrtc/mediaEvent'; import { expect, it } from 'vitest'; diff --git a/packages/ts-client/tests/fixtures.ts b/packages/ts-client/tests/fixtures.ts index cc03e8b9..a847b5c1 100644 --- a/packages/ts-client/tests/fixtures.ts +++ b/packages/ts-client/tests/fixtures.ts @@ -1,34 +1,36 @@ -import { +import type { ConnectedMediaEvent, - ConnectedMediaEventSchema, CustomEncodingUpdatedEvent, - CustomEncodingSwitchedEventSchema, CustomOfferDataEvent, - CustomOfferDataEventSchema, CustomSdpAnswerDataEvent, - CustomSdpAnswerDataEventSchema, Endpoint, - EndpointSchema, EndpointUpdatedWebrtcEvent, - EndpointUpdatedWebrtcEventSchema, Track, TracksAddedMediaEvent, - TracksAddedMediaEventSchema, - CustomBandwidthEstimationEventSchema, CustomBandwidthEstimationEvent, CustomVadNotificationEvent, - CustomVadNotificationEventSchema, TrackUpdatedEvent, - TrackUpdatedEventSchema, EndpointAddedWebrtcEvent, - EndpointAddedWebrtcEventSchema, - EndpointRemovedEventSchema, EndpointRemovedEvent, TracksRemovedEvent, +} from './schema'; +import { + ConnectedMediaEventSchema, + CustomEncodingSwitchedEventSchema, + CustomOfferDataEventSchema, + CustomSdpAnswerDataEventSchema, + EndpointSchema, + EndpointUpdatedWebrtcEventSchema, + TracksAddedMediaEventSchema, + CustomBandwidthEstimationEventSchema, + CustomVadNotificationEventSchema, + TrackUpdatedEventSchema, + EndpointAddedWebrtcEventSchema, + EndpointRemovedEventSchema, TracksRemovedEventSchema, } from './schema'; import { FakeMediaStreamTrack } from 'fake-mediastreamtrack'; -import { TrackEncoding, VadStatus } from '../src'; +import type { TrackEncoding, VadStatus } from '../src'; import { vi } from 'vitest'; export const endpointId = 'exampleEndpointId'; diff --git a/packages/ts-client/tests/utils.ts b/packages/ts-client/tests/utils.ts index 1c73e715..c86f2d10 100644 --- a/packages/ts-client/tests/utils.ts +++ b/packages/ts-client/tests/utils.ts @@ -5,7 +5,7 @@ import { createConnectedEventWithOneEndpointWithOneTrack, stream, } from './fixtures'; -import { WebRTCEndpoint } from '../src'; +import type { WebRTCEndpoint } from '../src'; import { mockRTCPeerConnection } from './mocks'; export const setupRoom = ( diff --git a/yarn.lock b/yarn.lock index 381902f1..83fda72a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -280,6 +280,13 @@ __metadata: languageName: node linkType: hard +"@balena/dockerignore@npm:^1.0.2": + version: 1.0.2 + resolution: "@balena/dockerignore@npm:1.0.2" + checksum: 10c0/0bcb067e86f6734ab943ce4ce9a7c8611f2e983a70bccebf9d2309db57695c09dded7faf5be49c929c4c9e9a9174ae55fc625626de0fb9958823c37423d12f4e + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -490,75 +497,132 @@ __metadata: languageName: node linkType: hard -"@fishjam-dev/browser-media-utils@https://github.com/fishjam-dev/browser-media-utils#1.0.0": - version: 0.0.1 - resolution: "@fishjam-dev/browser-media-utils@https://github.com/fishjam-dev/browser-media-utils.git#commit=86841c9962ddfa8263d846005906aa19bde16572" - checksum: 10c0/6d5bce6165f47d1fac58ae4dc564a0c1c6343eae935ff0299d4c95ecbf0026d2d4679ab988cf486e162a4fc91c6de35a669d78b0c30b35817d3bb280d5007a6f - languageName: node - linkType: hard - "@fishjam-dev/react-client@npm:*, @fishjam-dev/react-client@workspace:packages/react-client": version: 0.0.0-use.local resolution: "@fishjam-dev/react-client@workspace:packages/react-client" dependencies: - "@fishjam-dev/ts-client": "npm:*" - "@playwright/test": "npm:^1.42.1" + "@fishjam-dev/ts-client": "workspace:*" + "@playwright/test": "npm:^1.45.1" "@types/events": "npm:^3.0.3" "@types/lodash.isequal": "npm:^4.5.8" "@types/node": "npm:^20.11.27" "@types/react": "npm:^18.2.55" - "@typescript-eslint/eslint-plugin": "npm:^7.2.0" - "@typescript-eslint/parser": "npm:^7.8.0" - eslint: "npm:^8.57.0" - eslint-config-prettier: "npm:^9.1.0" - eslint-plugin-react-hooks: "npm:^4.6.0" - eslint-plugin-react-refresh: "npm:^0.4.6" events: "npm:3.3.0" lodash.isequal: "npm:4.5.0" - prettier: "npm:^3.3.0" - prettier-plugin-tailwindcss: "npm:^0.5.12" react: "npm:^18.2.0" typed-emitter: "npm:^2.1.0" - typedoc: "npm:^0.25.12" - typedoc-plugin-mdn-links: "npm:^3.1.18" - typescript: "npm:^5.4.0" + typedoc: "npm:^0.26.3" + typedoc-plugin-mdn-links: "npm:^3.2.2" languageName: unknown linkType: soft -"@fishjam-dev/ts-client@npm:*, @fishjam-dev/ts-client@workspace:packages/ts-client": +"@fishjam-dev/ts-client@npm:*, @fishjam-dev/ts-client@workspace:*, @fishjam-dev/ts-client@workspace:packages/ts-client": version: 0.0.0-use.local resolution: "@fishjam-dev/ts-client@workspace:packages/ts-client" dependencies: - "@playwright/test": "npm:^1.40.1" + "@playwright/test": "npm:^1.45.1" "@types/events": "npm:^3.0.3" "@types/node": "npm:^20.10.3" - "@types/uuid": "npm:^9.0.8" - "@typescript-eslint/eslint-plugin": "npm:^7.8.0" - "@typescript-eslint/parser": "npm:^7.8.0" + "@types/uuid": "npm:^10.0.0" "@vitest/coverage-v8": "npm:^1.6.0" - eslint: "npm:^8.55.0" - eslint-config-prettier: "npm:^9.1.0" - eslint-plugin-react-hooks: "npm:^4.6.0" events: "npm:^3.3.0" fake-mediastreamtrack: "npm:^1.2.0" husky: "npm:^9.0.11" lint-staged: "npm:^15.2.5" - prettier: "npm:^3.3.0" - prettier-plugin-tailwindcss: "npm:^0.5.12" protobufjs: "npm:^7.3.0" react: "npm:^18.2.0" ts-proto: "npm:^1.176.0" typed-emitter: "npm:^2.1.0" - typedoc: "npm:^0.25.13" + typedoc: "npm:^0.26.3" typedoc-plugin-external-resolver: "npm:^1.0.3" - typedoc-plugin-mdn-links: "npm:^3.1.6" - typescript: "npm:^5.4.0" - uuid: "npm:^9.0.1" + typedoc-plugin-mdn-links: "npm:^3.2.2" + uuid: "npm:^10.0.0" vitest: "npm:^1.6.0" zod: "npm:^3.23.6" languageName: unknown linkType: soft +"@fishjam-e2e/react-client-e2e@workspace:e2e-tests/react-client/app": + version: 0.0.0-use.local + resolution: "@fishjam-e2e/react-client-e2e@workspace:e2e-tests/react-client/app" + dependencies: + "@playwright/test": "npm:^1.45.1" + "@types/node": "npm:^20.11.18" + "@vitest/coverage-v8": "npm:^1.6.0" + testcontainers: "npm:^10.3.2" + vite: "npm:^5.1.2" + vitest: "npm:^1.6.0" + languageName: unknown + linkType: soft + +"@fishjam-e2e/ts-client-e2e@workspace:e2e-tests/ts-client/app": + version: 0.0.0-use.local + resolution: "@fishjam-e2e/ts-client-e2e@workspace:e2e-tests/ts-client/app" + dependencies: + "@fishjam-dev/ts-client": "npm:*" + "@playwright/test": "npm:^1.45.1" + "@types/node": "npm:^20.11.18" + "@types/react": "npm:^18.2.55" + "@types/react-dom": "npm:^18.2.19" + "@typescript-eslint/eslint-plugin": "npm:^7.15.0" + "@typescript-eslint/parser": "npm:^7.15.0" + "@vitejs/plugin-react": "npm:^4.2.1" + "@vitest/coverage-v8": "npm:^1.6.0" + eslint: "npm:^8.56.0" + eslint-plugin-react-hooks: "npm:^4.6.0" + eslint-plugin-react-refresh: "npm:^0.4.5" + protobufjs: "npm:^7.2.6" + react: "npm:^18.2.0" + react-dom: "npm:^18.2.0" + testcontainers: "npm:^10.3.2" + ts-proto: "npm:^1.176.0" + vite: "npm:^5.1.2" + vitest: "npm:^1.6.0" + languageName: unknown + linkType: soft + +"@fishjam-example/minimal-example@workspace:examples/ts-client/minimal": + version: 0.0.0-use.local + resolution: "@fishjam-example/minimal-example@workspace:examples/ts-client/minimal" + dependencies: + "@fishjam-dev/ts-client": "npm:*" + autoprefixer: "npm:^10.4.17" + vite: "npm:^5.1.2" + vite-plugin-checker: "npm:^0.6.4" + languageName: unknown + linkType: soft + +"@fishjam-example/minimal-react-example@workspace:examples/react-client/minimal-react": + version: 0.0.0-use.local + resolution: "@fishjam-example/minimal-react-example@workspace:examples/react-client/minimal-react" + dependencies: + "@fishjam-dev/react-client": "npm:*" + "@types/react": "npm:^18.2.55" + "@types/react-dom": "npm:^18.2.19" + "@vitejs/plugin-react-swc": "npm:^3.6.0" + autoprefixer: "npm:^10.4.17" + postcss: "npm:^8.4.39" + react: "npm:^18.2.0" + react-dom: "npm:^18.2.0" + vite: "npm:^5.1.2" + vite-plugin-checker: "npm:^0.6.4" + languageName: unknown + linkType: soft + +"@fishjam-example/simple-app-example@workspace:examples/ts-client/simple-app": + version: 0.0.0-use.local + resolution: "@fishjam-example/simple-app-example@workspace:examples/ts-client/simple-app" + dependencies: + "@fishjam-dev/ts-client": "npm:*" + autoprefixer: "npm:^10.4.17" + daisyui: "npm:^4.12.10" + postcss: "npm:^8.4.39" + tailwindcss: "npm:^3.4.1" + vite: "npm:^5.1.2" + vite-plugin-checker: "npm:^0.6.4" + languageName: unknown + linkType: soft + "@humanwhocodes/config-array@npm:^0.11.14": version: 0.11.14 resolution: "@humanwhocodes/config-array@npm:0.11.14" @@ -712,14 +776,14 @@ __metadata: languageName: node linkType: hard -"@playwright/test@npm:^1.40.1, @playwright/test@npm:^1.42.1": - version: 1.44.1 - resolution: "@playwright/test@npm:1.44.1" +"@playwright/test@npm:^1.45.1": + version: 1.45.1 + resolution: "@playwright/test@npm:1.45.1" dependencies: - playwright: "npm:1.44.1" + playwright: "npm:1.45.1" bin: playwright: cli.js - checksum: 10c0/f72669db3dfa83dc12d43ddbce8fbb27a69a80347b515fa00d8467ca640f8c7b7f5f5ff6d6cdfc5a1bce2c7d4b6ee62b988d682ef265f567302998de4f2a64ab + checksum: 10c0/ba214addee06e846041b819b8bcc2b04dae1beb36d05cd0942bb0fc7f9742002c881e2058b75aba37a8baef9a3aaff66e818b20b8013e9020d2cc28ff0c655d7 languageName: node linkType: hard @@ -908,6 +972,13 @@ __metadata: languageName: node linkType: hard +"@shikijs/core@npm:1.9.1": + version: 1.9.1 + resolution: "@shikijs/core@npm:1.9.1" + checksum: 10c0/70523a0fc2e8d32da6f35e640c854cb3ae3cb7202bcab2f08c92b8925ea4b2c19ec6df55c5cfd50d586bc3a3a5156945db720962ed66a3e624c2ef241aebe6ce + languageName: node + linkType: hard + "@sinclair/typebox@npm:^0.27.8": version: 0.27.8 resolution: "@sinclair/typebox@npm:0.27.8" @@ -1102,6 +1173,27 @@ __metadata: languageName: node linkType: hard +"@types/docker-modem@npm:*": + version: 3.0.6 + resolution: "@types/docker-modem@npm:3.0.6" + dependencies: + "@types/node": "npm:*" + "@types/ssh2": "npm:*" + checksum: 10c0/d3ffd273148bc883ff9b1a972b1f84c1add6d9a197d2f4fc9774db4c814f39c2e51cc649385b55d781c790c16fb0bf9c1f4c62499bd0f372a4b920190919445d + languageName: node + linkType: hard + +"@types/dockerode@npm:^3.3.29": + version: 3.3.29 + resolution: "@types/dockerode@npm:3.3.29" + dependencies: + "@types/docker-modem": "npm:*" + "@types/node": "npm:*" + "@types/ssh2": "npm:*" + checksum: 10c0/1c51a9f7d9a2ab81ecdf9e56b9a8e25400a97ed5f0701c583ed030b4518dee4b3d2ecc7d927fe1ff618c06747f3b285168adac5293aa3cdafafa1914db595cc2 + languageName: node + linkType: hard + "@types/estree@npm:1.0.5, @types/estree@npm:^1.0.0": version: 1.0.5 resolution: "@types/estree@npm:1.0.5" @@ -1132,6 +1224,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:*, @types/node@npm:^20.11.18": + version: 20.14.9 + resolution: "@types/node@npm:20.14.9" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 10c0/911ffa444dc032897f4a23ed580c67903bd38ea1c5ec99b1d00fa10b83537a3adddef8e1f29710cbdd8e556a61407ed008e06537d834e48caf449ce59f87d387 + languageName: node + linkType: hard + "@types/node@npm:>=13.7.0, @types/node@npm:^20.10.3, @types/node@npm:^20.11.27": version: 20.14.5 resolution: "@types/node@npm:20.14.5" @@ -1141,6 +1242,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^18.11.18": + version: 18.19.39 + resolution: "@types/node@npm:18.19.39" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 10c0/a9eb33bc093beba6bd5d4e839de7d1d1f496cd7e741c2f6c7161318dba0f37227bb25d8306907194992488d6c59a7363a419d72298549483d33402227a2d435b + languageName: node + linkType: hard + "@types/prop-types@npm:*": version: 15.7.12 resolution: "@types/prop-types@npm:15.7.12" @@ -1167,22 +1277,50 @@ __metadata: languageName: node linkType: hard -"@types/uuid@npm:^9.0.8": - version: 9.0.8 - resolution: "@types/uuid@npm:9.0.8" - checksum: 10c0/b411b93054cb1d4361919579ef3508a1f12bf15b5fdd97337d3d351bece6c921b52b6daeef89b62340fd73fd60da407878432a1af777f40648cbe53a01723489 +"@types/ssh2-streams@npm:*": + version: 0.1.12 + resolution: "@types/ssh2-streams@npm:0.1.12" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/6c860066e76391c937723b9f8c3953208737be5adf33b5584d7817ec90913094f2ca578e1d47717182f1d62cb5ca8e83fdec0241d73bf064221e3a2b2d132f0e + languageName: node + linkType: hard + +"@types/ssh2@npm:*": + version: 1.15.0 + resolution: "@types/ssh2@npm:1.15.0" + dependencies: + "@types/node": "npm:^18.11.18" + checksum: 10c0/055c271845847867c365b0c002e59536608e400864aea4f54ebc72e8588b92dbc4b6572e3095092dba0d86d49898e2180a389810269d119f897972ebbddb4a7f languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^7.2.0, @typescript-eslint/eslint-plugin@npm:^7.8.0": - version: 7.13.1 - resolution: "@typescript-eslint/eslint-plugin@npm:7.13.1" +"@types/ssh2@npm:^0.5.48": + version: 0.5.52 + resolution: "@types/ssh2@npm:0.5.52" + dependencies: + "@types/node": "npm:*" + "@types/ssh2-streams": "npm:*" + checksum: 10c0/95c52fd3438dedae6a59ca87b6558cb36568db6b9144c6c8a28c168739e04c51e27c02908aae14950b7b5020e1c40fea039b1203ae2734c356a40a050fd51c84 + languageName: node + linkType: hard + +"@types/uuid@npm:^10.0.0": + version: 10.0.0 + resolution: "@types/uuid@npm:10.0.0" + checksum: 10c0/9a1404bf287164481cb9b97f6bb638f78f955be57c40c6513b7655160beb29df6f84c915aaf4089a1559c216557dc4d2f79b48d978742d3ae10b937420ddac60 + languageName: node + linkType: hard + +"@typescript-eslint/eslint-plugin@npm:^7.15.0": + version: 7.15.0 + resolution: "@typescript-eslint/eslint-plugin@npm:7.15.0" dependencies: "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:7.13.1" - "@typescript-eslint/type-utils": "npm:7.13.1" - "@typescript-eslint/utils": "npm:7.13.1" - "@typescript-eslint/visitor-keys": "npm:7.13.1" + "@typescript-eslint/scope-manager": "npm:7.15.0" + "@typescript-eslint/type-utils": "npm:7.15.0" + "@typescript-eslint/utils": "npm:7.15.0" + "@typescript-eslint/visitor-keys": "npm:7.15.0" graphemer: "npm:^1.4.0" ignore: "npm:^5.3.1" natural-compare: "npm:^1.4.0" @@ -1193,44 +1331,44 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/6677f9c090a25978e4e20c24d67365ad89ca1208ebd2bb103d3f1e15a7deea22dea538e9f61f3a3d4f03a741179acf58c02ad7d03f805aceabb78929a8dc1908 + checksum: 10c0/7ed4ef8355cb60f02ed603673ef749928a001931c534960d1f3f9f9b8092f4abd7ec1e80a33b4c38efb6e8e66c902583bd56a4c4d6ccbd870677a40680a7d1f5 languageName: node linkType: hard -"@typescript-eslint/parser@npm:^7.8.0": - version: 7.13.1 - resolution: "@typescript-eslint/parser@npm:7.13.1" +"@typescript-eslint/parser@npm:^7.15.0": + version: 7.15.0 + resolution: "@typescript-eslint/parser@npm:7.15.0" dependencies: - "@typescript-eslint/scope-manager": "npm:7.13.1" - "@typescript-eslint/types": "npm:7.13.1" - "@typescript-eslint/typescript-estree": "npm:7.13.1" - "@typescript-eslint/visitor-keys": "npm:7.13.1" + "@typescript-eslint/scope-manager": "npm:7.15.0" + "@typescript-eslint/types": "npm:7.15.0" + "@typescript-eslint/typescript-estree": "npm:7.15.0" + "@typescript-eslint/visitor-keys": "npm:7.15.0" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.56.0 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/455d067bfb81fa3d133c75ebc4d8d7f2de5001441585f5b58dc8b0d4380d7397dc3745e11a9299d596dfa581265fdcdea6c28b2ddd2d3b542869c851ecd52fcd + checksum: 10c0/8dcad9b84e2cbf89afea97ee7f690f91b487eed21d01997126f98cb7dd56d3b6c98c7ecbdbeda35904af521c4ed746c47887e908f8a1e2148d47c05b491d7b9d languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:7.13.1": - version: 7.13.1 - resolution: "@typescript-eslint/scope-manager@npm:7.13.1" +"@typescript-eslint/scope-manager@npm:7.15.0": + version: 7.15.0 + resolution: "@typescript-eslint/scope-manager@npm:7.15.0" dependencies: - "@typescript-eslint/types": "npm:7.13.1" - "@typescript-eslint/visitor-keys": "npm:7.13.1" - checksum: 10c0/3d8770bf9c89e7a07e54efbc3dac6df02c0ce49d49575076111ac663566c90cbb852f06c94a311db7c0aec1fab0417f3ef6e601b3852aa30bed75c65f4f076f4 + "@typescript-eslint/types": "npm:7.15.0" + "@typescript-eslint/visitor-keys": "npm:7.15.0" + checksum: 10c0/781ec31a07ab7f0bdfb07dd271ef6553aa98f8492f1b3a67c65d178c94d590f4fd2e0916450f2446f1da2fbe007f3454c360ccb25f4d69612f782eb499f400ab languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:7.13.1": - version: 7.13.1 - resolution: "@typescript-eslint/type-utils@npm:7.13.1" +"@typescript-eslint/type-utils@npm:7.15.0": + version: 7.15.0 + resolution: "@typescript-eslint/type-utils@npm:7.15.0" dependencies: - "@typescript-eslint/typescript-estree": "npm:7.13.1" - "@typescript-eslint/utils": "npm:7.13.1" + "@typescript-eslint/typescript-estree": "npm:7.15.0" + "@typescript-eslint/utils": "npm:7.15.0" debug: "npm:^4.3.4" ts-api-utils: "npm:^1.3.0" peerDependencies: @@ -1238,23 +1376,23 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/c02305dccb0b2c7dcc9249230078c83e851ee589f93e08eb6cdc0b4c38d78d85ef4996631ac427836ee9d0a868ac031417feb74a6e4d0600096f41ca3c0e99a0 + checksum: 10c0/06189eb05d741f05977bbc029c6ac46edd566e0136f2f2c22429fd5f2be1224e2d9135b7021bc686871bfaec9c05a5c9990a321762d3abd06e457486956326ba languageName: node linkType: hard -"@typescript-eslint/types@npm:7.13.1": - version: 7.13.1 - resolution: "@typescript-eslint/types@npm:7.13.1" - checksum: 10c0/38a01004e11259e457ae2fd02300ef362a3268a8fc70addfbf1508e2edcaca72da2f0f8771e42c1cb9f191c1f754af583cdcaebd830c8e3c3f796dcf30d3c3a8 +"@typescript-eslint/types@npm:7.15.0": + version: 7.15.0 + resolution: "@typescript-eslint/types@npm:7.15.0" + checksum: 10c0/935387b21d9fdff65de86f6350cdda1f0614e269324f3a4f0a2ca1b0d72ef4b1d40c7de2f3a20a6f8c83edca6507bfbac3168c860625859e59fc455c80392bed languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:7.13.1": - version: 7.13.1 - resolution: "@typescript-eslint/typescript-estree@npm:7.13.1" +"@typescript-eslint/typescript-estree@npm:7.15.0": + version: 7.15.0 + resolution: "@typescript-eslint/typescript-estree@npm:7.15.0" dependencies: - "@typescript-eslint/types": "npm:7.13.1" - "@typescript-eslint/visitor-keys": "npm:7.13.1" + "@typescript-eslint/types": "npm:7.15.0" + "@typescript-eslint/visitor-keys": "npm:7.15.0" debug: "npm:^4.3.4" globby: "npm:^11.1.0" is-glob: "npm:^4.0.3" @@ -1264,31 +1402,31 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/bd5c8951ae79e8eacd05ff100def02926c633045a1a54426f98f20b4ca31c485968af3226dd7939934dfaf36a6b5fcb3386948e2a7d763ddee2db905ac187ebc + checksum: 10c0/0d6e61cb36c4612147ceea796c2bdbb65fca59170d9d768cff314146c5564253a058cbcb9e251722cd76c92a90c257e1210a69f8d4377c8002f211c574d18d24 languageName: node linkType: hard -"@typescript-eslint/utils@npm:7.13.1": - version: 7.13.1 - resolution: "@typescript-eslint/utils@npm:7.13.1" +"@typescript-eslint/utils@npm:7.15.0": + version: 7.15.0 + resolution: "@typescript-eslint/utils@npm:7.15.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.4.0" - "@typescript-eslint/scope-manager": "npm:7.13.1" - "@typescript-eslint/types": "npm:7.13.1" - "@typescript-eslint/typescript-estree": "npm:7.13.1" + "@typescript-eslint/scope-manager": "npm:7.15.0" + "@typescript-eslint/types": "npm:7.15.0" + "@typescript-eslint/typescript-estree": "npm:7.15.0" peerDependencies: eslint: ^8.56.0 - checksum: 10c0/d2f6be42a80608ed265b34a5f6a0c97dc0b627d53b91e83d87c7d67541cb5b3c038e7320026b4ad8dfafe1ac07a0554efa8fe7673f54d74b68c253d6f9519bb6 + checksum: 10c0/26aced17976cee0aa39a79201f68b384bbce1dc96e1c70d0e5f790e1e5655b1b1ddb2afd9eaf3fce9a48c0fb69daecd37a99fdbcdbf1cb58c65ae89ecac88a2c languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:7.13.1": - version: 7.13.1 - resolution: "@typescript-eslint/visitor-keys@npm:7.13.1" +"@typescript-eslint/visitor-keys@npm:7.15.0": + version: 7.15.0 + resolution: "@typescript-eslint/visitor-keys@npm:7.15.0" dependencies: - "@typescript-eslint/types": "npm:7.13.1" + "@typescript-eslint/types": "npm:7.15.0" eslint-visitor-keys: "npm:^3.4.3" - checksum: 10c0/23c1bb896173cadfb33e3801420a70aa2f0481384caa3b534b04f7920acdb9d8f7d635fcaf1f8c7fc78ebce71b8f2435391608d120091761ad2e2c00eb870832 + checksum: 10c0/7509f01c8cd2126a38213bc735a77aa7e976340af0d664be5b2ccd01b8211724b2ea129e33bfd32fe5feac848b7b68ca55bb533f6ccfeec1d2f26a91240489b9 languageName: node linkType: hard @@ -1497,13 +1635,6 @@ __metadata: languageName: node linkType: hard -"ansi-sequence-parser@npm:^1.1.0": - version: 1.1.1 - resolution: "ansi-sequence-parser@npm:1.1.1" - checksum: 10c0/ab2259ccf69f145ecf1418d4e71524158828f44afdf37c7536677871f4cebaa8b176fcb95de8f94a68129357dddc59586597da25f9d4ebf9968f6ef022bf0b31 - languageName: node - linkType: hard - "ansi-styles@npm:^3.2.1": version: 3.2.1 resolution: "ansi-styles@npm:3.2.1" @@ -1553,6 +1684,57 @@ __metadata: languageName: node linkType: hard +"archiver-utils@npm:^2.1.0": + version: 2.1.0 + resolution: "archiver-utils@npm:2.1.0" + dependencies: + glob: "npm:^7.1.4" + graceful-fs: "npm:^4.2.0" + lazystream: "npm:^1.0.0" + lodash.defaults: "npm:^4.2.0" + lodash.difference: "npm:^4.5.0" + lodash.flatten: "npm:^4.4.0" + lodash.isplainobject: "npm:^4.0.6" + lodash.union: "npm:^4.6.0" + normalize-path: "npm:^3.0.0" + readable-stream: "npm:^2.0.0" + checksum: 10c0/6ea5b02e440f3099aff58b18dd384f84ecfe18632e81d26c1011fe7dfdb80ade43d7a06cbf048ef0e9ee0f2c87a80cb24c0f0ac5e3a2c4d67641d6f0d6e36ece + languageName: node + linkType: hard + +"archiver-utils@npm:^3.0.4": + version: 3.0.4 + resolution: "archiver-utils@npm:3.0.4" + dependencies: + glob: "npm:^7.2.3" + graceful-fs: "npm:^4.2.0" + lazystream: "npm:^1.0.0" + lodash.defaults: "npm:^4.2.0" + lodash.difference: "npm:^4.5.0" + lodash.flatten: "npm:^4.4.0" + lodash.isplainobject: "npm:^4.0.6" + lodash.union: "npm:^4.6.0" + normalize-path: "npm:^3.0.0" + readable-stream: "npm:^3.6.0" + checksum: 10c0/9bb7e271e95ff33bdbdcd6f69f8860e0aeed3fcba352a74f51a626d1c32b404f20e3185d5214f171b24a692471d01702f43874d1a4f0d2e5f57bd0834bc54c14 + languageName: node + linkType: hard + +"archiver@npm:^5.3.2": + version: 5.3.2 + resolution: "archiver@npm:5.3.2" + dependencies: + archiver-utils: "npm:^2.1.0" + async: "npm:^3.2.4" + buffer-crc32: "npm:^0.2.1" + readable-stream: "npm:^3.6.0" + readdir-glob: "npm:^1.1.2" + tar-stream: "npm:^2.2.0" + zip-stream: "npm:^4.1.0" + checksum: 10c0/973384d749b3fa96f44ceda1603a65aaa3f24a267230d69a4df9d7b607d38d3ebc6c18c358af76eb06345b6b331ccb9eca07bd079430226b5afce95de22dfade + languageName: node + linkType: hard + "arg@npm:^5.0.2": version: 5.0.2 resolution: "arg@npm:5.0.2" @@ -1574,6 +1756,15 @@ __metadata: languageName: node linkType: hard +"asn1@npm:^0.2.6": + version: 0.2.6 + resolution: "asn1@npm:0.2.6" + dependencies: + safer-buffer: "npm:~2.1.0" + checksum: 10c0/00c8a06c37e548762306bcb1488388d2f76c74c36f70c803f0c081a01d3bdf26090fc088cd812afc5e56a6d49e33765d451a5f8a68ab9c2b087eba65d2e980e0 + languageName: node + linkType: hard + "assertion-error@npm:^1.1.0": version: 1.1.0 resolution: "assertion-error@npm:1.1.0" @@ -1581,6 +1772,20 @@ __metadata: languageName: node linkType: hard +"async-lock@npm:^1.4.1": + version: 1.4.1 + resolution: "async-lock@npm:1.4.1" + checksum: 10c0/f696991c7d894af1dc91abc81cc4f14b3785190a35afb1646d8ab91138238d55cabd83bfdd56c42663a008d72b3dc39493ff83797e550effc577d1ccbde254af + languageName: node + linkType: hard + +"async@npm:^3.2.4": + version: 3.2.5 + resolution: "async@npm:3.2.5" + checksum: 10c0/1408287b26c6db67d45cb346e34892cee555b8b59e6c68e6f8c3e495cad5ca13b4f218180e871f3c2ca30df4ab52693b66f2f6ff43644760cab0b2198bda79c1 + languageName: node + linkType: hard + "autoprefixer@npm:^10.4.17": version: 10.4.19 resolution: "autoprefixer@npm:10.4.19" @@ -1599,6 +1804,13 @@ __metadata: languageName: node linkType: hard +"b4a@npm:^1.6.4": + version: 1.6.6 + resolution: "b4a@npm:1.6.6" + checksum: 10c0/56f30277666cb511a15829e38d369b114df7dc8cec4cedc09cc5d685bc0f27cb63c7bcfb58e09a19a1b3c4f2541069ab078b5328542e85d74a39620327709a38 + languageName: node + linkType: hard + "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" @@ -1606,6 +1818,65 @@ __metadata: languageName: node linkType: hard +"bare-events@npm:^2.0.0, bare-events@npm:^2.2.0": + version: 2.4.2 + resolution: "bare-events@npm:2.4.2" + checksum: 10c0/09fa923061f31f815e83504e2ed4a8ba87732a01db40a7fae703dbb7eef7f05d99264b5e186074cbe9698213990d1af564c62cca07a5ff88baea8099ad9a6303 + languageName: node + linkType: hard + +"bare-fs@npm:^2.1.1": + version: 2.3.1 + resolution: "bare-fs@npm:2.3.1" + dependencies: + bare-events: "npm:^2.0.0" + bare-path: "npm:^2.0.0" + bare-stream: "npm:^2.0.0" + checksum: 10c0/820979ad3dd8693076ba08af842e41b5119fcca63f4324b8f28d96b96050cd260085dffd1169dc644f20746fadb4cf4368b317f2fa2db4e40890921ceb557581 + languageName: node + linkType: hard + +"bare-os@npm:^2.1.0": + version: 2.4.0 + resolution: "bare-os@npm:2.4.0" + checksum: 10c0/85615522fd8309d3815d3bef227623f008fac34e037459294a7e24bb2b51ea125597274b8aa7e7038f82de89c15e2148fef299eece40ec3ea33797a357c4f2bb + languageName: node + linkType: hard + +"bare-path@npm:^2.0.0, bare-path@npm:^2.1.0": + version: 2.1.3 + resolution: "bare-path@npm:2.1.3" + dependencies: + bare-os: "npm:^2.1.0" + checksum: 10c0/35587e177fc8fa5b13fb90bac8779b5ce49c99016d221ddaefe2232d02bd4295d79b941e14ae19fda75ec42a6fe5fb66c07d83ae7ec11462178e66b7be65ca74 + languageName: node + linkType: hard + +"bare-stream@npm:^2.0.0": + version: 2.1.3 + resolution: "bare-stream@npm:2.1.3" + dependencies: + streamx: "npm:^2.18.0" + checksum: 10c0/8703b1d80318496ea560483943d5f425a160ded8d3d75659571842caf5f374f52668809bc1e39b032af14df7210973995efaf273f8c35986bef697380ef4674a + languageName: node + linkType: hard + +"base64-js@npm:^1.3.1": + version: 1.5.1 + resolution: "base64-js@npm:1.5.1" + checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf + languageName: node + linkType: hard + +"bcrypt-pbkdf@npm:^1.0.2": + version: 1.0.2 + resolution: "bcrypt-pbkdf@npm:1.0.2" + dependencies: + tweetnacl: "npm:^0.14.3" + checksum: 10c0/ddfe85230b32df25aeebfdccfbc61d3bc493ace49c884c9c68575de1f5dcf733a5d7de9def3b0f318b786616b8d85bad50a28b1da1750c43e0012c93badcc148 + languageName: node + linkType: hard + "binary-extensions@npm:^2.0.0": version: 2.3.0 resolution: "binary-extensions@npm:2.3.0" @@ -1613,6 +1884,17 @@ __metadata: languageName: node linkType: hard +"bl@npm:^4.0.3": + version: 4.1.0 + resolution: "bl@npm:4.1.0" + dependencies: + buffer: "npm:^5.5.0" + inherits: "npm:^2.0.4" + readable-stream: "npm:^3.4.0" + checksum: 10c0/02847e1d2cb089c9dc6958add42e3cdeaf07d13f575973963335ac0fdece563a50ac770ac4c8fa06492d2dd276f6cc3b7f08c7cd9c7a7ad0f8d388b2a28def5f + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.11 resolution: "brace-expansion@npm:1.1.11" @@ -1655,6 +1937,37 @@ __metadata: languageName: node linkType: hard +"buffer-crc32@npm:^0.2.1, buffer-crc32@npm:^0.2.13": + version: 0.2.13 + resolution: "buffer-crc32@npm:0.2.13" + checksum: 10c0/cb0a8ddf5cf4f766466db63279e47761eb825693eeba6a5a95ee4ec8cb8f81ede70aa7f9d8aeec083e781d47154290eb5d4d26b3f7a465ec57fb9e7d59c47150 + languageName: node + linkType: hard + +"buffer@npm:^5.5.0": + version: 5.7.1 + resolution: "buffer@npm:5.7.1" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.1.13" + checksum: 10c0/27cac81cff434ed2876058d72e7c4789d11ff1120ef32c9de48f59eab58179b66710c488987d295ae89a228f835fc66d088652dffeb8e3ba8659f80eb091d55e + languageName: node + linkType: hard + +"buildcheck@npm:~0.0.6": + version: 0.0.6 + resolution: "buildcheck@npm:0.0.6" + checksum: 10c0/8cbdb89f41bc484b8325f4828db4135b206a0dffb641eb6cdb2b7022483c45dd0e5aac6d820c9a67bdd2caab3a02c76d7ceec7bd9ec494b5a2270d2806b01a76 + languageName: node + linkType: hard + +"byline@npm:^5.0.0": + version: 5.0.0 + resolution: "byline@npm:5.0.0" + checksum: 10c0/33fb64cd84440b3652a99a68d732c56ef18a748ded495ba38e7756a242fab0d4654b9b8ce269fd0ac14c5f97aa4e3c369613672b280a1f60b559b34223105c85 + languageName: node + linkType: hard + "cac@npm:^6.7.14": version: 6.7.14 resolution: "cac@npm:6.7.14" @@ -1781,6 +2094,13 @@ __metadata: languageName: node linkType: hard +"chownr@npm:^1.1.1": + version: 1.1.4 + resolution: "chownr@npm:1.1.4" + checksum: 10c0/ed57952a84cc0c802af900cf7136de643d3aba2eecb59d29344bc2f3f9bf703a301b9d84cdc71f82c3ffc9ccde831b0d92f5b45f91727d6c9da62f23aef9d9db + languageName: node + linkType: hard + "chownr@npm:^2.0.0": version: 2.0.0 resolution: "chownr@npm:2.0.0" @@ -1874,6 +2194,18 @@ __metadata: languageName: node linkType: hard +"compress-commons@npm:^4.1.2": + version: 4.1.2 + resolution: "compress-commons@npm:4.1.2" + dependencies: + buffer-crc32: "npm:^0.2.13" + crc32-stream: "npm:^4.0.2" + normalize-path: "npm:^3.0.0" + readable-stream: "npm:^3.6.0" + checksum: 10c0/e5fa03cb374ed89028e20226c70481e87286240392d5c6856f4e7fef40605c1892748648e20ed56597d390d76513b1b9bb4dbd658a1bbff41c9fa60107c74d3f + languageName: node + linkType: hard + "concat-map@npm:0.0.1": version: 0.0.1 resolution: "concat-map@npm:0.0.1" @@ -1895,6 +2227,43 @@ __metadata: languageName: node linkType: hard +"core-util-is@npm:~1.0.0": + version: 1.0.3 + resolution: "core-util-is@npm:1.0.3" + checksum: 10c0/90a0e40abbddfd7618f8ccd63a74d88deea94e77d0e8dbbea059fa7ebebb8fbb4e2909667fe26f3a467073de1a542ebe6ae4c73a73745ac5833786759cd906c9 + languageName: node + linkType: hard + +"cpu-features@npm:~0.0.9": + version: 0.0.10 + resolution: "cpu-features@npm:0.0.10" + dependencies: + buildcheck: "npm:~0.0.6" + nan: "npm:^2.19.0" + node-gyp: "npm:latest" + checksum: 10c0/0c4a12904657b22477ffbcfd2b4b2bdd45b174f283616b18d9e1ade495083f9f6098493feb09f4ae2d0b36b240f9ecd32cfb4afe210cf0d0f8f0cc257bd58e54 + languageName: node + linkType: hard + +"crc-32@npm:^1.2.0": + version: 1.2.2 + resolution: "crc-32@npm:1.2.2" + bin: + crc32: bin/crc32.njs + checksum: 10c0/11dcf4a2e77ee793835d49f2c028838eae58b44f50d1ff08394a610bfd817523f105d6ae4d9b5bef0aad45510f633eb23c903e9902e4409bed1ce70cb82b9bf0 + languageName: node + linkType: hard + +"crc32-stream@npm:^4.0.2": + version: 4.0.3 + resolution: "crc32-stream@npm:4.0.3" + dependencies: + crc-32: "npm:^1.2.0" + readable-stream: "npm:^3.4.0" + checksum: 10c0/127b0c66a947c54db37054fca86085722140644d3a75ebc61d4477bad19304d2936386b0461e8ee9e1c24b00e804cd7c2e205180e5bcb4632d20eccd60533bc4 + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" @@ -1939,19 +2308,19 @@ __metadata: languageName: node linkType: hard -"daisyui@npm:^4.7.2": - version: 4.12.2 - resolution: "daisyui@npm:4.12.2" +"daisyui@npm:^4.12.10": + version: 4.12.10 + resolution: "daisyui@npm:4.12.10" dependencies: css-selector-tokenizer: "npm:^0.8" culori: "npm:^3" picocolors: "npm:^1" postcss-js: "npm:^4" - checksum: 10c0/28ddd2ed44ddbcee9ba0c7f4c4b3a1db88a07772caadfa47e23a0bc9c01982a72504256be98aa38204be995dcaca1574de68db5237f6eb9a457705a0c81d8d3f + checksum: 10c0/f677d9717d6241d6c829141645ff15a5d6aaa9e279a90e1acb248613c9cc25d7d0b9fcac55ec572537f6bc472e1ab5f0bed7797a1815b6fde9a644be62f357d0 languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:~4.3.4": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:~4.3.4": version: 4.3.5 resolution: "debug@npm:4.3.5" dependencies: @@ -2018,6 +2387,38 @@ __metadata: languageName: node linkType: hard +"docker-compose@npm:^0.24.8": + version: 0.24.8 + resolution: "docker-compose@npm:0.24.8" + dependencies: + yaml: "npm:^2.2.2" + checksum: 10c0/1494389e554fed8aabf9fef24210a641cd2442028b1462d7f68186919f5e75045f7bfb4ccaf47c94ed879dcb63e4d82885c389399f531550c4b244920740b2b3 + languageName: node + linkType: hard + +"docker-modem@npm:^3.0.0": + version: 3.0.8 + resolution: "docker-modem@npm:3.0.8" + dependencies: + debug: "npm:^4.1.1" + readable-stream: "npm:^3.5.0" + split-ca: "npm:^1.0.1" + ssh2: "npm:^1.11.0" + checksum: 10c0/5c00592297fabd78454621fe765a5ef0daea4bbb6692e239ad65b111f4da9d750178f448f8efcaf84f9f999598eb735bc14ad6bf5f0a2dcf9c2d453d5b683540 + languageName: node + linkType: hard + +"dockerode@npm:^3.3.5": + version: 3.3.5 + resolution: "dockerode@npm:3.3.5" + dependencies: + "@balena/dockerignore": "npm:^1.0.2" + docker-modem: "npm:^3.0.0" + tar-fs: "npm:~2.0.1" + checksum: 10c0/c45fa8ed3ad76f13fe7799d539a60fe466f8e34bea06b30d75be9e08bc00536cc9ff2d54e38fbb3b2a8a382bf9d4459a27741e6454ce7d0cda5cd35c51224c73 + languageName: node + linkType: hard + "doctrine@npm:^3.0.0": version: 3.0.0 resolution: "doctrine@npm:3.0.0" @@ -2080,6 +2481,22 @@ __metadata: languageName: node linkType: hard +"end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": + version: 1.4.4 + resolution: "end-of-stream@npm:1.4.4" + dependencies: + once: "npm:^1.4.0" + checksum: 10c0/870b423afb2d54bb8d243c63e07c170409d41e20b47eeef0727547aea5740bd6717aca45597a9f2745525667a6b804c1e7bede41f856818faee5806dd9ff3975 + languageName: node + linkType: hard + +"entities@npm:^4.4.0": + version: 4.5.0 + resolution: "entities@npm:4.5.0" + checksum: 10c0/5b039739f7621f5d1ad996715e53d964035f75ad3b9a4d38c6b3804bb226e282ffeae2443624d8fdd9c47d8e926ae9ac009c54671243f0c3294c26af7cc85250 + languageName: node + linkType: hard + "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -2215,7 +2632,7 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-react-refresh@npm:^0.4.6": +"eslint-plugin-react-refresh@npm:^0.4.5, eslint-plugin-react-refresh@npm:^0.4.6": version: 0.4.7 resolution: "eslint-plugin-react-refresh@npm:0.4.7" peerDependencies: @@ -2241,7 +2658,7 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^8.55.0, eslint@npm:^8.57.0": +"eslint@npm:^8.55.0, eslint@npm:^8.56.0": version: 8.57.0 resolution: "eslint@npm:8.57.0" dependencies: @@ -2403,6 +2820,13 @@ __metadata: languageName: node linkType: hard +"fast-fifo@npm:^1.2.0, fast-fifo@npm:^1.3.2": + version: 1.3.2 + resolution: "fast-fifo@npm:1.3.2" + checksum: 10c0/d53f6f786875e8b0529f784b59b4b05d4b5c31c651710496440006a398389a579c8dbcd2081311478b5bf77f4b0b21de69109c5a4eabea9d8e8783d1eb864e4c + languageName: node + linkType: hard + "fast-glob@npm:^3.2.7, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0": version: 3.3.2 resolution: "fast-glob@npm:3.3.2" @@ -2477,6 +2901,16 @@ __metadata: "fishjam-web-sdk@workspace:.": version: 0.0.0-use.local resolution: "fishjam-web-sdk@workspace:." + dependencies: + "@typescript-eslint/eslint-plugin": "npm:^7.15.0" + "@typescript-eslint/parser": "npm:^7.15.0" + eslint: "npm:^8.55.0" + eslint-config-prettier: "npm:^9.1.0" + eslint-plugin-react-hooks: "npm:^4.6.0" + eslint-plugin-react-refresh: "npm:^0.4.6" + prettier: "npm:^3.3.0" + prettier-plugin-tailwindcss: "npm:^0.6.5" + typescript: "npm:^5.5.0" languageName: unknown linkType: soft @@ -2515,6 +2949,13 @@ __metadata: languageName: node linkType: hard +"fs-constants@npm:^1.0.0": + version: 1.0.0 + resolution: "fs-constants@npm:1.0.0" + checksum: 10c0/a0cde99085f0872f4d244e83e03a46aa387b74f5a5af750896c6b05e9077fac00e9932fdf5aef84f2f16634cd473c63037d7a512576da7d5c2b9163d1909f3a8 + languageName: node + linkType: hard + "fs-extra@npm:^11.1.0": version: 11.2.0 resolution: "fs-extra@npm:11.2.0" @@ -2617,6 +3058,13 @@ __metadata: languageName: node linkType: hard +"get-port@npm:^5.1.1": + version: 5.1.1 + resolution: "get-port@npm:5.1.1" + checksum: 10c0/2873877a469b24e6d5e0be490724a17edb39fafc795d1d662e7bea951ca649713b4a50117a473f9d162312cb0e946597bd0e049ed2f866e79e576e8e213d3d1c + languageName: node + linkType: hard + "get-stream@npm:^8.0.1": version: 8.0.1 resolution: "get-stream@npm:8.0.1" @@ -2658,7 +3106,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.1.3, glob@npm:^7.1.4": +"glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.2.3": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -2702,7 +3150,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6": +"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 @@ -2798,6 +3246,13 @@ __metadata: languageName: node linkType: hard +"ieee754@npm:^1.1.13": + version: 1.2.1 + resolution: "ieee754@npm:1.2.1" + checksum: 10c0/b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb + languageName: node + linkType: hard + "ignore@npm:^5.2.0, ignore@npm:^5.3.1": version: 5.3.1 resolution: "ignore@npm:5.3.1" @@ -2839,7 +3294,7 @@ __metadata: languageName: node linkType: hard -"inherits@npm:2": +"inherits@npm:2, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3": version: 2.0.4 resolution: "inherits@npm:2.0.4" checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 @@ -2941,6 +3396,13 @@ __metadata: languageName: node linkType: hard +"isarray@npm:~1.0.0": + version: 1.0.0 + resolution: "isarray@npm:1.0.0" + checksum: 10c0/18b5be6669be53425f0b84098732670ed4e727e3af33bc7f948aac01782110eb9a18b3b329c5323bcdd3acdaae547ee077d3951317e7f133bff7105264b3003d + languageName: node + linkType: hard + "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -3016,9 +3478,9 @@ __metadata: languageName: node linkType: hard -"jotai@npm:^2.6.4": - version: 2.8.3 - resolution: "jotai@npm:2.8.3" +"jotai@npm:^2.8.4": + version: 2.8.4 + resolution: "jotai@npm:2.8.4" peerDependencies: "@types/react": ">=17.0.0" react: ">=17.0.0" @@ -3027,7 +3489,7 @@ __metadata: optional: true react: optional: true - checksum: 10c0/86a2fee4b1ed1fd6f1a1b6ee54e3596371c28122ecd9b70c3801690156010b3ecfb71821c14d21fc64fee11ff70b57205cc61a82cd33a8c6a1992033d0ddd59b + checksum: 10c0/32bd3a41ea9de2f1a17dc179707a37d4c470a44973d8e48dbc8d5284fbf6333b678a203b10216049ab7e62ca34465cf3c765f64ed54cb0ef470246309415ff0f languageName: node linkType: hard @@ -3102,13 +3564,6 @@ __metadata: languageName: node linkType: hard -"jsonc-parser@npm:^3.2.0": - version: 3.2.1 - resolution: "jsonc-parser@npm:3.2.1" - checksum: 10c0/ada66dec143d7f9cb0e2d0d29c69e9ce40d20f3a4cb96b0c6efb745025ac7f9ba647d7ac0990d0adfc37a2d2ae084a12009a9c833dbdbeadf648879a99b9df89 - languageName: node - linkType: hard - "jsonfile@npm:^6.0.1": version: 6.1.0 resolution: "jsonfile@npm:6.1.0" @@ -3131,6 +3586,15 @@ __metadata: languageName: node linkType: hard +"lazystream@npm:^1.0.0": + version: 1.0.1 + resolution: "lazystream@npm:1.0.1" + dependencies: + readable-stream: "npm:^2.0.5" + checksum: 10c0/ea4e509a5226ecfcc303ba6782cc269be8867d372b9bcbd625c88955df1987ea1a20da4643bf9270336415a398d33531ebf0d5f0d393b9283dc7c98bfcbd7b69 + languageName: node + linkType: hard + "levn@npm:^0.4.1": version: 0.4.1 resolution: "levn@npm:0.4.1" @@ -3162,6 +3626,15 @@ __metadata: languageName: node linkType: hard +"linkify-it@npm:^5.0.0": + version: 5.0.0 + resolution: "linkify-it@npm:5.0.0" + dependencies: + uc.micro: "npm:^2.0.0" + checksum: 10c0/ff4abbcdfa2003472fc3eb4b8e60905ec97718e11e33cca52059919a4c80cc0e0c2a14d23e23d8c00e5402bc5a885cdba8ca053a11483ab3cc8b3c7a52f88e2d + languageName: node + linkType: hard + "lint-staged@npm:^15.2.5": version: 15.2.7 resolution: "lint-staged@npm:15.2.7" @@ -3222,6 +3695,27 @@ __metadata: languageName: node linkType: hard +"lodash.defaults@npm:^4.2.0": + version: 4.2.0 + resolution: "lodash.defaults@npm:4.2.0" + checksum: 10c0/d5b77aeb702caa69b17be1358faece33a84497bcca814897383c58b28a2f8dfc381b1d9edbec239f8b425126a3bbe4916223da2a576bb0411c2cefd67df80707 + languageName: node + linkType: hard + +"lodash.difference@npm:^4.5.0": + version: 4.5.0 + resolution: "lodash.difference@npm:4.5.0" + checksum: 10c0/5d52859218a7df427547ff1fadbc397879709fe6c788b037df7d6d92b676122c92bd35ec85d364edb596b65dfc6573132f420c9b4ee22bb6b9600cd454c90637 + languageName: node + linkType: hard + +"lodash.flatten@npm:^4.4.0": + version: 4.4.0 + resolution: "lodash.flatten@npm:4.4.0" + checksum: 10c0/97e8f0d6b61fe4723c02ad0c6e67e51784c4a2c48f56ef283483e556ad01594cf9cec9c773e177bbbdbdb5d19e99b09d2487cb6b6e5dc405c2693e93b125bd3a + languageName: node + linkType: hard + "lodash.isequal@npm:4.5.0": version: 4.5.0 resolution: "lodash.isequal@npm:4.5.0" @@ -3243,6 +3737,13 @@ __metadata: languageName: node linkType: hard +"lodash.union@npm:^4.6.0": + version: 4.6.0 + resolution: "lodash.union@npm:4.6.0" + checksum: 10c0/6da7f72d1facd472f6090b49eefff984c9f9179e13172039c0debca6851d21d37d83c7ad5c43af23bd220f184cd80e6897e8e3206509fae491f9068b02ae6319 + languageName: node + linkType: hard + "log-update@npm:^6.0.0": version: 6.0.0 resolution: "log-update@npm:6.0.0" @@ -3355,12 +3856,26 @@ __metadata: languageName: node linkType: hard -"marked@npm:^4.3.0": - version: 4.3.0 - resolution: "marked@npm:4.3.0" +"markdown-it@npm:^14.1.0": + version: 14.1.0 + resolution: "markdown-it@npm:14.1.0" + dependencies: + argparse: "npm:^2.0.1" + entities: "npm:^4.4.0" + linkify-it: "npm:^5.0.0" + mdurl: "npm:^2.0.0" + punycode.js: "npm:^2.3.1" + uc.micro: "npm:^2.1.0" bin: - marked: bin/marked.js - checksum: 10c0/0013463855e31b9c88d8bb2891a611d10ef1dc79f2e3cbff1bf71ba389e04c5971298c886af0be799d7fa9aa4593b086a136062d59f1210b0480b026a8c5dc47 + markdown-it: bin/markdown-it.mjs + checksum: 10c0/9a6bb444181d2db7016a4173ae56a95a62c84d4cbfb6916a399b11d3e6581bf1cc2e4e1d07a2f022ae72c25f56db90fbe1e529fca16fbf9541659dc53480d4b4 + languageName: node + linkType: hard + +"mdurl@npm:^2.0.0": + version: 2.0.0 + resolution: "mdurl@npm:2.0.0" + checksum: 10c0/633db522272f75ce4788440669137c77540d74a83e9015666a9557a152c02e245b192edc20bc90ae953bbab727503994a53b236b4d9c99bdaee594d0e7dd2ce0 languageName: node linkType: hard @@ -3402,36 +3917,6 @@ __metadata: languageName: node linkType: hard -"minimal-example@workspace:examples/ts-client/minimal": - version: 0.0.0-use.local - resolution: "minimal-example@workspace:examples/ts-client/minimal" - dependencies: - "@fishjam-dev/ts-client": "npm:*" - autoprefixer: "npm:^10.4.17" - typescript: "npm:^5.4.0" - vite: "npm:^5.1.2" - vite-plugin-checker: "npm:^0.6.4" - languageName: unknown - linkType: soft - -"minimal-react-example@workspace:examples/react-client/minimal-react": - version: 0.0.0-use.local - resolution: "minimal-react-example@workspace:examples/react-client/minimal-react" - dependencies: - "@fishjam-dev/react-client": "npm:*" - "@types/react": "npm:^18.2.55" - "@types/react-dom": "npm:^18.2.19" - "@vitejs/plugin-react-swc": "npm:^3.6.0" - autoprefixer: "npm:^10.4.17" - postcss: "npm:^8.4.35" - react: "npm:^18.2.0" - react-dom: "npm:^18.2.0" - typescript: "npm:^5.4.0" - vite: "npm:^5.1.2" - vite-plugin-checker: "npm:^0.6.4" - languageName: unknown - linkType: soft - "minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -3441,7 +3926,16 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.3, minimatch@npm:^9.0.4": +"minimatch@npm:^5.1.0": + version: 5.1.6 + resolution: "minimatch@npm:5.1.6" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 10c0/3defdfd230914f22a8da203747c42ee3c405c39d4d37ffda284dac5e45b7e1f6c49aa8be606509002898e73091ff2a3bbfc59c2c6c71d4660609f63aa92f98e3 + languageName: node + linkType: hard + +"minimatch@npm:^9.0.4": version: 9.0.4 resolution: "minimatch@npm:9.0.4" dependencies: @@ -3450,6 +3944,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^9.0.5": + version: 9.0.5 + resolution: "minimatch@npm:9.0.5" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 10c0/de96cf5e35bdf0eab3e2c853522f98ffbe9a36c37797778d2665231ec1f20a9447a7e567cb640901f89e4daaa95ae5d70c65a9e8aa2bb0019b6facbc3c0575ed + languageName: node + linkType: hard + "minipass-collect@npm:^2.0.1": version: 2.0.1 resolution: "minipass-collect@npm:2.0.1" @@ -3534,7 +4037,14 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^1.0.3": +"mkdirp-classic@npm:^0.5.2": + version: 0.5.3 + resolution: "mkdirp-classic@npm:0.5.3" + checksum: 10c0/95371d831d196960ddc3833cc6907e6b8f67ac5501a6582f47dfae5eb0f092e9f8ce88e0d83afcae95d6e2b61a01741ba03714eeafb6f7a6e9dcc158ac85b168 + languageName: node + linkType: hard + +"mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4": version: 1.0.4 resolution: "mkdirp@npm:1.0.4" bin: @@ -3573,6 +4083,15 @@ __metadata: languageName: node linkType: hard +"nan@npm:^2.18.0, nan@npm:^2.19.0": + version: 2.20.0 + resolution: "nan@npm:2.20.0" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/75775309a21ad179a55250d62ce47322c33ca03d8ddb5ad4c555bd820dd72484b3c59253dd9f41cc68dd63453ef04017407fbd081a549bc030d977079bb798b7 + languageName: node + linkType: hard + "nanoid@npm:^3.3.7": version: 3.3.7 resolution: "nanoid@npm:3.3.7" @@ -3596,6 +4115,20 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:^2.7.0": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" + dependencies: + whatwg-url: "npm:^5.0.0" + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8 + languageName: node + linkType: hard + "node-gyp@npm:latest": version: 10.1.0 resolution: "node-gyp@npm:10.1.0" @@ -3680,7 +4213,7 @@ __metadata: languageName: node linkType: hard -"once@npm:^1.3.0": +"once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0": version: 1.4.0 resolution: "once@npm:1.4.0" dependencies: @@ -3887,27 +4420,27 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.44.1": - version: 1.44.1 - resolution: "playwright-core@npm:1.44.1" +"playwright-core@npm:1.45.1": + version: 1.45.1 + resolution: "playwright-core@npm:1.45.1" bin: playwright-core: cli.js - checksum: 10c0/6ffa3a04822b3df86d7f47a97e4f20318c0c50868ba4311820e6626ecadaab1424fbd0a3d01f0b4228adc0c781115e44b801742a4970b88739f804d82f142d68 + checksum: 10c0/607ad31ce1e85e2042107954eeed2cb7de5f387b42d9c8c19baa5c1ea4c2ea621bf233094ed86be45de625eeece33b280847ff641ff1bb9acaddee040e17bea1 languageName: node linkType: hard -"playwright@npm:1.44.1": - version: 1.44.1 - resolution: "playwright@npm:1.44.1" +"playwright@npm:1.45.1": + version: 1.45.1 + resolution: "playwright@npm:1.45.1" dependencies: fsevents: "npm:2.3.2" - playwright-core: "npm:1.44.1" + playwright-core: "npm:1.45.1" dependenciesMeta: fsevents: optional: true bin: playwright: cli.js - checksum: 10c0/de827d17746b18ae2ec67d510a640d8ceebf8ee8e3d8399bccffa83b76a967498ca377777e4e6a1daaef4b3c86cb2c44c7468de53d2d915acc61b3b89c032738 + checksum: 10c0/549e8621b120258ff53e93fcf3b2994a835aa084097ea533a9f4b53ff993308f3617cf00943c6975f88b66068890a6bf9d61b4ffdd73b7d8f45a5d284b6f284b languageName: node linkType: hard @@ -3991,7 +4524,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.23, postcss@npm:^8.4.35, postcss@npm:^8.4.38": +"postcss@npm:^8.4.23, postcss@npm:^8.4.38": version: 8.4.38 resolution: "postcss@npm:8.4.38" dependencies: @@ -4002,6 +4535,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.4.39": + version: 8.4.39 + resolution: "postcss@npm:8.4.39" + dependencies: + nanoid: "npm:^3.3.7" + picocolors: "npm:^1.0.1" + source-map-js: "npm:^1.2.0" + checksum: 10c0/16f5ac3c4e32ee76d1582b3c0dcf1a1fdb91334a45ad755eeb881ccc50318fb8d64047de4f1601ac96e30061df203f0f2e2edbdc0bfc49b9c57bc9fb9bedaea3 + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -4009,9 +4553,9 @@ __metadata: languageName: node linkType: hard -"prettier-plugin-tailwindcss@npm:^0.5.12": - version: 0.5.14 - resolution: "prettier-plugin-tailwindcss@npm:0.5.14" +"prettier-plugin-tailwindcss@npm:^0.6.5": + version: 0.6.5 + resolution: "prettier-plugin-tailwindcss@npm:0.6.5" peerDependencies: "@ianvs/prettier-plugin-sort-imports": "*" "@prettier/plugin-pug": "*" @@ -4060,7 +4604,7 @@ __metadata: optional: true prettier-plugin-svelte: optional: true - checksum: 10c0/9857873cb8cb0d9b7b895806e7f6265617a08805691125d282767dffb1cb3d2c4c662f2b9168ef391edc40dff1b81beb99eee488f96544e01b8924db694f2299 + checksum: 10c0/30d62928592b48cab03c46ff63edd35d4a33c4e7c40e583f12bff7223eba8b6f780fd394965b0250160bcf39688f6fb602420374b2055bcbb6a69560b818ca4e languageName: node linkType: hard @@ -4098,6 +4642,13 @@ __metadata: languageName: node linkType: hard +"process-nextick-args@npm:~2.0.0": + version: 2.0.1 + resolution: "process-nextick-args@npm:2.0.1" + checksum: 10c0/bec089239487833d46b59d80327a1605e1c5287eaad770a291add7f45fda1bb5e28b38e0e061add0a1d0ee0984788ce74fa394d345eed1c420cacf392c554367 + languageName: node + linkType: hard + "promise-retry@npm:^2.0.1": version: 2.0.1 resolution: "promise-retry@npm:2.0.1" @@ -4108,7 +4659,27 @@ __metadata: languageName: node linkType: hard -"protobufjs@npm:^7.2.4, protobufjs@npm:^7.3.0": +"proper-lockfile@npm:^4.1.2": + version: 4.1.2 + resolution: "proper-lockfile@npm:4.1.2" + dependencies: + graceful-fs: "npm:^4.2.4" + retry: "npm:^0.12.0" + signal-exit: "npm:^3.0.2" + checksum: 10c0/2f265dbad15897a43110a02dae55105c04d356ec4ed560723dcb9f0d34bc4fb2f13f79bb930e7561be10278e2314db5aca2527d5d3dcbbdee5e6b331d1571f6d + languageName: node + linkType: hard + +"properties-reader@npm:^2.3.0": + version: 2.3.0 + resolution: "properties-reader@npm:2.3.0" + dependencies: + mkdirp: "npm:^1.0.4" + checksum: 10c0/f665057e3a9076c643ba1198afcc71703eda227a59913252f7ff9467ece8d29c0cf8bf14bf1abcaef71570840c32a4e257e6c39b7550451bbff1a777efcf5667 + languageName: node + linkType: hard + +"protobufjs@npm:^7.2.4, protobufjs@npm:^7.2.6, protobufjs@npm:^7.3.0": version: 7.3.2 resolution: "protobufjs@npm:7.3.2" dependencies: @@ -4128,6 +4699,23 @@ __metadata: languageName: node linkType: hard +"pump@npm:^3.0.0": + version: 3.0.0 + resolution: "pump@npm:3.0.0" + dependencies: + end-of-stream: "npm:^1.1.0" + once: "npm:^1.3.1" + checksum: 10c0/bbdeda4f747cdf47db97428f3a135728669e56a0ae5f354a9ac5b74556556f5446a46f720a8f14ca2ece5be9b4d5d23c346db02b555f46739934cc6c093a5478 + languageName: node + linkType: hard + +"punycode.js@npm:^2.3.1": + version: 2.3.1 + resolution: "punycode.js@npm:2.3.1" + checksum: 10c0/1d12c1c0e06127fa5db56bd7fdf698daf9a78104456a6b67326877afc21feaa821257b171539caedd2f0524027fa38e67b13dd094159c8d70b6d26d2bea4dfdb + languageName: node + linkType: hard + "punycode@npm:^2.1.0": version: 2.3.1 resolution: "punycode@npm:2.3.1" @@ -4142,6 +4730,13 @@ __metadata: languageName: node linkType: hard +"queue-tick@npm:^1.0.1": + version: 1.0.1 + resolution: "queue-tick@npm:1.0.1" + checksum: 10c0/0db998e2c9b15215317dbcf801e9b23e6bcde4044e115155dae34f8e7454b9a783f737c9a725528d677b7a66c775eb7a955cf144fe0b87f62b575ce5bfd515a9 + languageName: node + linkType: hard + "react-dom@npm:^18.2.0": version: 18.3.1 resolution: "react-dom@npm:18.3.1" @@ -4186,6 +4781,41 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.5": + version: 2.3.8 + resolution: "readable-stream@npm:2.3.8" + dependencies: + core-util-is: "npm:~1.0.0" + inherits: "npm:~2.0.3" + isarray: "npm:~1.0.0" + process-nextick-args: "npm:~2.0.0" + safe-buffer: "npm:~5.1.1" + string_decoder: "npm:~1.1.1" + util-deprecate: "npm:~1.0.1" + checksum: 10c0/7efdb01f3853bc35ac62ea25493567bf588773213f5f4a79f9c365e1ad13bab845ac0dae7bc946270dc40c3929483228415e92a3fc600cc7e4548992f41ee3fa + languageName: node + linkType: hard + +"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0": + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" + dependencies: + inherits: "npm:^2.0.3" + string_decoder: "npm:^1.1.1" + util-deprecate: "npm:^1.0.1" + checksum: 10c0/e37be5c79c376fdd088a45fa31ea2e423e5d48854be7a22a58869b4e84d25047b193f6acb54f1012331e1bcd667ffb569c01b99d36b0bd59658fb33f513511b7 + languageName: node + linkType: hard + +"readdir-glob@npm:^1.1.2": + version: 1.1.3 + resolution: "readdir-glob@npm:1.1.3" + dependencies: + minimatch: "npm:^5.1.0" + checksum: 10c0/a37e0716726650845d761f1041387acd93aa91b28dd5381950733f994b6c349ddc1e21e266ec7cc1f9b92e205a7a972232f9b89d5424d07361c2c3753d5dbace + languageName: node + linkType: hard + "readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -4351,7 +4981,21 @@ __metadata: languageName: node linkType: hard -"safer-buffer@npm:>= 2.1.2 < 3.0.0": +"safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1": + version: 5.1.2 + resolution: "safe-buffer@npm:5.1.2" + checksum: 10c0/780ba6b5d99cc9a40f7b951d47152297d0e260f0df01472a1b99d4889679a4b94a13d644f7dbc4f022572f09ae9005fa2fbb93bbbd83643316f365a3e9a45b21 + languageName: node + linkType: hard + +"safe-buffer@npm:~5.2.0": + version: 5.2.1 + resolution: "safe-buffer@npm:5.2.1" + checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 + languageName: node + linkType: hard + +"safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:~2.1.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 @@ -4401,15 +5045,12 @@ __metadata: languageName: node linkType: hard -"shiki@npm:^0.14.7": - version: 0.14.7 - resolution: "shiki@npm:0.14.7" +"shiki@npm:^1.9.1": + version: 1.9.1 + resolution: "shiki@npm:1.9.1" dependencies: - ansi-sequence-parser: "npm:^1.1.0" - jsonc-parser: "npm:^3.2.0" - vscode-oniguruma: "npm:^1.7.0" - vscode-textmate: "npm:^8.0.0" - checksum: 10c0/5c7fcbb870d0facccc7ae2f3410a28121f8e0b3f298e4e956de817ad6ab60a4c7e20a9184edfe50a93447addbb88b95b69e6ef88ac16ac6ca3e94c50771a6459 + "@shikijs/core": "npm:1.9.1" + checksum: 10c0/a9d58279a015f6ab9c2a849d90c23545062f15d6e18d4402f463c41fc7d51992d1d478f9d43b33bf1c1310d56cc0caee90d25a42e4453d4e522166b41409956e languageName: node linkType: hard @@ -4434,22 +5075,6 @@ __metadata: languageName: node linkType: hard -"simple-app-example@workspace:examples/ts-client/simple-app": - version: 0.0.0-use.local - resolution: "simple-app-example@workspace:examples/ts-client/simple-app" - dependencies: - "@fishjam-dev/browser-media-utils": "https://github.com/fishjam-dev/browser-media-utils#1.0.0" - "@fishjam-dev/ts-client": "npm:*" - autoprefixer: "npm:^10.4.17" - daisyui: "npm:^4.7.2" - postcss: "npm:^8.4.35" - tailwindcss: "npm:^3.4.1" - typescript: "npm:^5.4.0" - vite: "npm:^5.1.2" - vite-plugin-checker: "npm:^0.6.4" - languageName: unknown - linkType: soft - "slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -4512,6 +5137,13 @@ __metadata: languageName: node linkType: hard +"split-ca@npm:^1.0.1": + version: 1.0.1 + resolution: "split-ca@npm:1.0.1" + checksum: 10c0/f339170b84c6b4706fcf4c60cc84acb36574c0447566bd713301a8d9b4feff7f4627efc8c334bec24944a3e2f35bc596bd58c673c9980d6bfe3137aae1116ba7 + languageName: node + linkType: hard + "sprintf-js@npm:^1.1.3": version: 1.1.3 resolution: "sprintf-js@npm:1.1.3" @@ -4519,6 +5151,33 @@ __metadata: languageName: node linkType: hard +"ssh-remote-port-forward@npm:^1.0.4": + version: 1.0.4 + resolution: "ssh-remote-port-forward@npm:1.0.4" + dependencies: + "@types/ssh2": "npm:^0.5.48" + ssh2: "npm:^1.4.0" + checksum: 10c0/33a441af12817577ea30d089b03c19f980d2fb2370933123a35026dc6be40f2dfce067e4dfc173e23d745464537ff647aa1bb7469be5571cc21f7cdb25181c09 + languageName: node + linkType: hard + +"ssh2@npm:^1.11.0, ssh2@npm:^1.4.0": + version: 1.15.0 + resolution: "ssh2@npm:1.15.0" + dependencies: + asn1: "npm:^0.2.6" + bcrypt-pbkdf: "npm:^1.0.2" + cpu-features: "npm:~0.0.9" + nan: "npm:^2.18.0" + dependenciesMeta: + cpu-features: + optional: true + nan: + optional: true + checksum: 10c0/7c76888fbfa1c15660cf51086a6e5699b3c1caad516e29adb1d2a00fc1ef6b48946ca7ec811b4bb50456984967c4346115c7ddd3dbf981a1193bd1f40fa4529a + languageName: node + linkType: hard + "ssri@npm:^10.0.0": version: 10.0.6 resolution: "ssri@npm:10.0.6" @@ -4542,6 +5201,21 @@ __metadata: languageName: node linkType: hard +"streamx@npm:^2.15.0, streamx@npm:^2.18.0": + version: 2.18.0 + resolution: "streamx@npm:2.18.0" + dependencies: + bare-events: "npm:^2.2.0" + fast-fifo: "npm:^1.3.2" + queue-tick: "npm:^1.0.1" + text-decoder: "npm:^1.1.0" + dependenciesMeta: + bare-events: + optional: true + checksum: 10c0/ef50f419252a73dd35abcde72329eafbf5ad9cd2e27f0cc3abebeff6e0dbea124ac6d3e16acbdf081cce41b4125393ac22f9848fcfa19e640830734883e622ba + languageName: node + linkType: hard + "string-argv@npm:~0.3.2": version: 0.3.2 resolution: "string-argv@npm:0.3.2" @@ -4582,6 +5256,24 @@ __metadata: languageName: node linkType: hard +"string_decoder@npm:^1.1.1": + version: 1.3.0 + resolution: "string_decoder@npm:1.3.0" + dependencies: + safe-buffer: "npm:~5.2.0" + checksum: 10c0/810614ddb030e271cd591935dcd5956b2410dd079d64ff92a1844d6b7588bf992b3e1b69b0f4d34a3e06e0bd73046ac646b5264c1987b20d0601f81ef35d731d + languageName: node + linkType: hard + +"string_decoder@npm:~1.1.1": + version: 1.1.1 + resolution: "string_decoder@npm:1.1.1" + dependencies: + safe-buffer: "npm:~5.1.0" + checksum: 10c0/b4f89f3a92fd101b5653ca3c99550e07bdf9e13b35037e9e2a1c7b47cec4e55e06ff3fc468e314a0b5e80bfbaf65c1ca5a84978764884ae9413bec1fc6ca924e + languageName: node + linkType: hard + "strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -4699,6 +5391,59 @@ __metadata: languageName: node linkType: hard +"tar-fs@npm:^3.0.6": + version: 3.0.6 + resolution: "tar-fs@npm:3.0.6" + dependencies: + bare-fs: "npm:^2.1.1" + bare-path: "npm:^2.1.0" + pump: "npm:^3.0.0" + tar-stream: "npm:^3.1.5" + dependenciesMeta: + bare-fs: + optional: true + bare-path: + optional: true + checksum: 10c0/207b7c0f193495668bd9dbad09a0108ce4ffcfec5bce2133f90988cdda5c81fad83c99f963d01e47b565196594f7a17dbd063ae55b97b36268fcc843975278ee + languageName: node + linkType: hard + +"tar-fs@npm:~2.0.1": + version: 2.0.1 + resolution: "tar-fs@npm:2.0.1" + dependencies: + chownr: "npm:^1.1.1" + mkdirp-classic: "npm:^0.5.2" + pump: "npm:^3.0.0" + tar-stream: "npm:^2.0.0" + checksum: 10c0/0128e888b61c7c4e8e7997d66ceccc3c79d73c01e87cfcc3d9f6b8555b0c88b8d67d91ff167f00b067f726dde497b2d1fb2bba0cfcb3ccb95ae413cb86c715bc + languageName: node + linkType: hard + +"tar-stream@npm:^2.0.0, tar-stream@npm:^2.2.0": + version: 2.2.0 + resolution: "tar-stream@npm:2.2.0" + dependencies: + bl: "npm:^4.0.3" + end-of-stream: "npm:^1.4.1" + fs-constants: "npm:^1.0.0" + inherits: "npm:^2.0.3" + readable-stream: "npm:^3.1.1" + checksum: 10c0/2f4c910b3ee7196502e1ff015a7ba321ec6ea837667220d7bcb8d0852d51cb04b87f7ae471008a6fb8f5b1a1b5078f62f3a82d30c706f20ada1238ac797e7692 + languageName: node + linkType: hard + +"tar-stream@npm:^3.1.5": + version: 3.1.7 + resolution: "tar-stream@npm:3.1.7" + dependencies: + b4a: "npm:^1.6.4" + fast-fifo: "npm:^1.2.0" + streamx: "npm:^2.15.0" + checksum: 10c0/a09199d21f8714bd729993ac49b6c8efcb808b544b89f23378ad6ffff6d1cb540878614ba9d4cfec11a64ef39e1a6f009a5398371491eb1fda606ffc7f70f718 + languageName: node + linkType: hard + "tar@npm:^6.1.11, tar@npm:^6.1.2": version: 6.2.1 resolution: "tar@npm:6.2.1" @@ -4724,6 +5469,38 @@ __metadata: languageName: node linkType: hard +"testcontainers@npm:^10.3.2": + version: 10.10.0 + resolution: "testcontainers@npm:10.10.0" + dependencies: + "@balena/dockerignore": "npm:^1.0.2" + "@types/dockerode": "npm:^3.3.29" + archiver: "npm:^5.3.2" + async-lock: "npm:^1.4.1" + byline: "npm:^5.0.0" + debug: "npm:^4.3.5" + docker-compose: "npm:^0.24.8" + dockerode: "npm:^3.3.5" + get-port: "npm:^5.1.1" + node-fetch: "npm:^2.7.0" + proper-lockfile: "npm:^4.1.2" + properties-reader: "npm:^2.3.0" + ssh-remote-port-forward: "npm:^1.0.4" + tar-fs: "npm:^3.0.6" + tmp: "npm:^0.2.3" + checksum: 10c0/5cf4ae3001430cd5e8ad0a81bf00956274efe3be987e970ebe47403c31dec8daea2b292f889a5d0778297d56a70d807a8cd50504e9f06778ffe0ab2ed9c6eea1 + languageName: node + linkType: hard + +"text-decoder@npm:^1.1.0": + version: 1.1.0 + resolution: "text-decoder@npm:1.1.0" + dependencies: + b4a: "npm:^1.6.4" + checksum: 10c0/623a6cfb5ee86c250fea31f369a0d40e4ef5c2c32ce8db43492648b51193858213e61bf47a6078f285053715dcc6342806ce6ea9a49d7847ffca282ca88ad7e8 + languageName: node + linkType: hard + "text-table@npm:^0.2.0": version: 0.2.0 resolution: "text-table@npm:0.2.0" @@ -4777,6 +5554,13 @@ __metadata: languageName: node linkType: hard +"tmp@npm:^0.2.3": + version: 0.2.3 + resolution: "tmp@npm:0.2.3" + checksum: 10c0/3e809d9c2f46817475b452725c2aaa5d11985cf18d32a7a970ff25b568438e2c076c2e8609224feef3b7923fa9749b74428e3e634f6b8e520c534eef2fd24125 + languageName: node + linkType: hard + "to-fast-properties@npm:^2.0.0": version: 2.0.0 resolution: "to-fast-properties@npm:2.0.0" @@ -4793,6 +5577,13 @@ __metadata: languageName: node linkType: hard +"tr46@npm:~0.0.3": + version: 0.0.3 + resolution: "tr46@npm:0.0.3" + checksum: 10c0/047cb209a6b60c742f05c9d3ace8fa510bff609995c129a37ace03476a9b12db4dbf975e74600830ef0796e18882b2381fb5fb1f6b4f96b832c374de3ab91a11 + languageName: node + linkType: hard + "ts-api-utils@npm:^1.3.0": version: 1.3.0 resolution: "ts-api-utils@npm:1.3.0" @@ -4849,6 +5640,13 @@ __metadata: languageName: node linkType: hard +"tweetnacl@npm:^0.14.3": + version: 0.14.5 + resolution: "tweetnacl@npm:0.14.5" + checksum: 10c0/4612772653512c7bc19e61923fbf42903f5e0389ec76a4a1f17195859d114671ea4aa3b734c2029ce7e1fa7e5cc8b80580f67b071ecf0b46b5636d030a0102a2 + languageName: node + linkType: hard + "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0" @@ -4901,48 +5699,56 @@ __metadata: languageName: node linkType: hard -"typedoc-plugin-mdn-links@npm:^3.1.18, typedoc-plugin-mdn-links@npm:^3.1.6": - version: 3.1.30 - resolution: "typedoc-plugin-mdn-links@npm:3.1.30" +"typedoc-plugin-mdn-links@npm:^3.2.2": + version: 3.2.2 + resolution: "typedoc-plugin-mdn-links@npm:3.2.2" peerDependencies: - typedoc: ">= 0.23.14 || 0.24.x || 0.25.x" - checksum: 10c0/85de49ed0303132ea40c0acec5689184ff0ada4921c6c3f9ccc7a4d7bdad7555d337b3593a8f2afd387ff41a8b6649c0cf423259a6ebf98745e6187c7d4ea12a + typedoc: ">= 0.23.14 || 0.24.x || 0.25.x || 0.26.x" + checksum: 10c0/67b478ec05e90f087d272310672e80237bceec587af1a250d36489c405a02eec93d14b71f468364f69c7b73e3e18c2dbc4220d021649ac6b1a816a6d0d6ae525 languageName: node linkType: hard -"typedoc@npm:^0.25.12, typedoc@npm:^0.25.13": - version: 0.25.13 - resolution: "typedoc@npm:0.25.13" +"typedoc@npm:^0.26.3": + version: 0.26.3 + resolution: "typedoc@npm:0.26.3" dependencies: lunr: "npm:^2.3.9" - marked: "npm:^4.3.0" - minimatch: "npm:^9.0.3" - shiki: "npm:^0.14.7" + markdown-it: "npm:^14.1.0" + minimatch: "npm:^9.0.5" + shiki: "npm:^1.9.1" + yaml: "npm:^2.4.5" peerDependencies: - typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x + typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x bin: typedoc: bin/typedoc - checksum: 10c0/13878e6a9fc2b65d65e3b514efa11b43bdfd57149861cefc4a969ec213f4bc4b36ee9239d0b654ae18bcbbd5174206d409383f9000b7bdea22da1945f7ac91de + checksum: 10c0/d6d0a19d4b90cb547dcefb282c35ea4bef0b76dcb3d0c7afdae2bb9b0af092a7389b40fb02aae8fba7bf8ee7da371ab200690d9c9d4a38b156275a3674c3f2ce languageName: node linkType: hard -"typescript@npm:^5.4.0": - version: 5.4.5 - resolution: "typescript@npm:5.4.5" +"typescript@npm:^5.5.0": + version: 5.5.3 + resolution: "typescript@npm:5.5.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/2954022ada340fd3d6a9e2b8e534f65d57c92d5f3989a263754a78aba549f7e6529acc1921913560a4b816c46dce7df4a4d29f9f11a3dc0d4213bb76d043251e + checksum: 10c0/f52c71ccbc7080b034b9d3b72051d563601a4815bf3e39ded188e6ce60813f75dbedf11ad15dd4d32a12996a9ed8c7155b46c93a9b9c9bad1049766fe614bbdd languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.4.0#optional!builtin": - version: 5.4.5 - resolution: "typescript@patch:typescript@npm%3A5.4.5#optional!builtin::version=5.4.5&hash=5adc0c" +"typescript@patch:typescript@npm%3A^5.5.0#optional!builtin": + version: 5.5.3 + resolution: "typescript@patch:typescript@npm%3A5.5.3#optional!builtin::version=5.5.3&hash=b45daf" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/db2ad2a16ca829f50427eeb1da155e7a45e598eec7b086d8b4e8ba44e5a235f758e606d681c66992230d3fc3b8995865e5fd0b22a2c95486d0b3200f83072ec9 + checksum: 10c0/5a437c416251334deeaf29897157032311f3f126547cfdc4b133768b606cb0e62bcee733bb97cf74c42fe7268801aea1392d8e40988cdef112e9546eba4c03c5 + languageName: node + linkType: hard + +"uc.micro@npm:^2.0.0, uc.micro@npm:^2.1.0": + version: 2.1.0 + resolution: "uc.micro@npm:2.1.0" + checksum: 10c0/8862eddb412dda76f15db8ad1c640ccc2f47cdf8252a4a30be908d535602c8d33f9855dfcccb8b8837855c1ce1eaa563f7fa7ebe3c98fd0794351aab9b9c55fa languageName: node linkType: hard @@ -5018,26 +5824,34 @@ __metadata: "@types/react-dom": "npm:^18.2.19" "@vitejs/plugin-react": "npm:^4.2.1" autoprefixer: "npm:^10.4.17" - daisyui: "npm:^4.7.2" - jotai: "npm:^2.6.4" - postcss: "npm:^8.4.35" + daisyui: "npm:^4.12.10" + jotai: "npm:^2.8.4" + postcss: "npm:^8.4.39" react: "npm:^18.2.0" react-dom: "npm:^18.2.0" tailwindcss: "npm:^3.4.1" - typescript: "npm:^5.4.0" vite: "npm:^5.1.2" zod: "npm:^3.22.4" languageName: unknown linkType: soft -"util-deprecate@npm:^1.0.2": +"util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" checksum: 10c0/41a5bdd214df2f6c3ecf8622745e4a366c4adced864bc3c833739791aeeeb1838119af7daed4ba36428114b5c67dcda034a79c882e97e43c03e66a4dd7389942 languageName: node linkType: hard -"uuid@npm:^9.0.0, uuid@npm:^9.0.1": +"uuid@npm:^10.0.0": + version: 10.0.0 + resolution: "uuid@npm:10.0.0" + bin: + uuid: dist/bin/uuid + checksum: 10c0/eab18c27fe4ab9fb9709a5d5f40119b45f2ec8314f8d4cf12ce27e4c6f4ffa4a6321dc7db6c515068fa373c075b49691ba969f0010bf37f44c37ca40cd6bf7fe + languageName: node + linkType: hard + +"uuid@npm:^9.0.0": version: 9.0.1 resolution: "uuid@npm:9.0.1" bin: @@ -5254,24 +6068,27 @@ __metadata: languageName: node linkType: hard -"vscode-oniguruma@npm:^1.7.0": - version: 1.7.0 - resolution: "vscode-oniguruma@npm:1.7.0" - checksum: 10c0/bef0073c665ddf8c86e51da94529c905856559e9aba97a9882f951acd572da560384775941ab6e7e8db94d9c578b25fefb951e4b73c37e8712e16b0231de2689 +"vscode-uri@npm:^3.0.2": + version: 3.0.8 + resolution: "vscode-uri@npm:3.0.8" + checksum: 10c0/f7f217f526bf109589969fe6e66b71e70b937de1385a1d7bb577ca3ee7c5e820d3856a86e9ff2fa9b7a0bc56a3dd8c3a9a557d3fedd7df414bc618d5e6b567f9 languageName: node linkType: hard -"vscode-textmate@npm:^8.0.0": - version: 8.0.0 - resolution: "vscode-textmate@npm:8.0.0" - checksum: 10c0/836f7fe73fc94998a38ca193df48173a2b6eab08b4943d83c8cac9a2a0c3546cfdab4cf1b10b890ec4a4374c5bee03a885ef0e83e7fd2bd618cf00781c017c04 +"webidl-conversions@npm:^3.0.0": + version: 3.0.1 + resolution: "webidl-conversions@npm:3.0.1" + checksum: 10c0/5612d5f3e54760a797052eb4927f0ddc01383550f542ccd33d5238cfd65aeed392a45ad38364970d0a0f4fea32e1f4d231b3d8dac4a3bdd385e5cf802ae097db languageName: node linkType: hard -"vscode-uri@npm:^3.0.2": - version: 3.0.8 - resolution: "vscode-uri@npm:3.0.8" - checksum: 10c0/f7f217f526bf109589969fe6e66b71e70b937de1385a1d7bb577ca3ee7c5e820d3856a86e9ff2fa9b7a0bc56a3dd8c3a9a557d3fedd7df414bc618d5e6b567f9 +"whatwg-url@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-url@npm:5.0.0" + dependencies: + tr46: "npm:~0.0.3" + webidl-conversions: "npm:^3.0.0" + checksum: 10c0/1588bed84d10b72d5eec1d0faa0722ba1962f1821e7539c535558fb5398d223b0c50d8acab950b8c488b4ba69043fd833cc2697056b167d8ad46fac3995a55d5 languageName: node linkType: hard @@ -5370,7 +6187,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.3.4, yaml@npm:~2.4.2": +"yaml@npm:^2.2.2, yaml@npm:^2.3.4, yaml@npm:^2.4.5, yaml@npm:~2.4.2": version: 2.4.5 resolution: "yaml@npm:2.4.5" bin: @@ -5393,6 +6210,17 @@ __metadata: languageName: node linkType: hard +"zip-stream@npm:^4.1.0": + version: 4.1.1 + resolution: "zip-stream@npm:4.1.1" + dependencies: + archiver-utils: "npm:^3.0.4" + compress-commons: "npm:^4.1.2" + readable-stream: "npm:^3.6.0" + checksum: 10c0/38f91ca116a38561cf184c29e035e9453b12c30eaf574e0993107a4a5331882b58c9a7f7b97f63910664028089fbde3296d0b3682d1ccb2ad96929e68f1b2b89 + languageName: node + linkType: hard + "zod@npm:^3.22.4, zod@npm:^3.23.6": version: 3.23.8 resolution: "zod@npm:3.23.8" From 4eb6603802572033214cdf8488533070f76b4165 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Fri, 5 Jul 2024 16:12:25 +0200 Subject: [PATCH 16/50] Merge main --- e2e-tests/react-client/app/package.json | 1 - e2e-tests/ts-client/setup/setupFishjam.ts | 32 +++---- packages/ts-client/src/webrtc/transceivers.ts | 85 +++++++++++++++++++ .../ts-client/src/webrtc/webRTCEndpoint.ts | 2 +- yarn.lock | 1 - 5 files changed, 102 insertions(+), 19 deletions(-) diff --git a/e2e-tests/react-client/app/package.json b/e2e-tests/react-client/app/package.json index 601a9081..977189f4 100644 --- a/e2e-tests/react-client/app/package.json +++ b/e2e-tests/react-client/app/package.json @@ -9,7 +9,6 @@ "e2e:ui": "NODE_OPTIONS=--dns-result-order=ipv4first playwright test --ui" }, "devDependencies": { - "@playwright/test": "^1.45.1", "@types/node": "^20.11.18", "@vitest/coverage-v8": "^1.6.0", "testcontainers": "^10.3.2", diff --git a/e2e-tests/ts-client/setup/setupFishjam.ts b/e2e-tests/ts-client/setup/setupFishjam.ts index ee5b2098..3ac876cc 100644 --- a/e2e-tests/ts-client/setup/setupFishjam.ts +++ b/e2e-tests/ts-client/setup/setupFishjam.ts @@ -3,21 +3,21 @@ import { setupState } from './globalSetupState'; import { type NetworkInterfaceInfo, networkInterfaces } from 'os'; export default async function setupFishjam() { - // const EXTERNAL_IP = Object.values(networkInterfaces()) - // .flat() - // .filter((x): x is NetworkInterfaceInfo => x !== undefined) - // .filter(({ family }) => family === 'IPv4') - // .filter(({ internal }) => !internal) - // .map(({ address }) => address)[0]; + const EXTERNAL_IP = Object.values(networkInterfaces()) + .flat() + .filter((x): x is NetworkInterfaceInfo => x !== undefined) + .filter(({ family }) => family === 'IPv4') + .filter(({ internal }) => !internal) + .map(({ address }) => address)[0]; - // setupState.fishjamContainer = await new DockerComposeEnvironment( - // '../.', - // 'docker-compose-test.yaml', - // ) - // .withEnvironment({ EXTERNAL_IP, PORT: "5033" }) - // .withWaitStrategy( - // 'fishjam', - // Wait.forLogMessage('Access FishjamWeb.Endpoint at'), - // ) - // .up(); + setupState.fishjamContainer = await new DockerComposeEnvironment( + '../.', + 'docker-compose-test.yaml', + ) + .withEnvironment({ EXTERNAL_IP }) + .withWaitStrategy( + 'fishjam', + Wait.forLogMessage('Access FishjamWeb.Endpoint at'), + ) + .up(); } diff --git a/packages/ts-client/src/webrtc/transceivers.ts b/packages/ts-client/src/webrtc/transceivers.ts index e69de29b..351ee9d4 100644 --- a/packages/ts-client/src/webrtc/transceivers.ts +++ b/packages/ts-client/src/webrtc/transceivers.ts @@ -0,0 +1,85 @@ +import type { EndpointWithTrackContext, TrackContextImpl } from './internal'; +import type { RemoteTrackId } from './types'; + +type Mid = string; +type TrackId = string; +type MidToTrackId = Record; + +export const getMidToTrackId = ( + connection: RTCPeerConnection | undefined, + localTrackIdToTrack: Map< + RemoteTrackId, + TrackContextImpl + >, + midToTrackId: Map = new Map(), + localEndpoint: EndpointWithTrackContext, +): MidToTrackId | null => { + if (!connection) return null; + + // - negotiated unmuted tracks: tracks added in previous negotiation, data is being transmitted + // - not yet negotiated tracks: tracks added in this negotiation, data will be transmitted after successful negotiation + const mappingFromTransceivers = getTransceiverMapping( + connection, + localTrackIdToTrack, + ); + + // - negotiated unmuted tracks: tracks added in previous negotiation, data is being transmitted + // - negotiated muted tracks: tracks added in previous negotiation, data is not being transmitted but can be transmitted in the future + const mappingFromLocalNegotiatedTracks = getAllNegotiatedLocalTracksMapping( + midToTrackId, + localEndpoint, + ); + + return { ...mappingFromTransceivers, ...mappingFromLocalNegotiatedTracks }; +}; + +const getTrackContext = ( + localTrackIdToTrack: Map< + RemoteTrackId, + TrackContextImpl + >, + localTrackId: string, +) => + Array.from(localTrackIdToTrack.values()).find( + (trackContext) => trackContext?.track?.id === localTrackId, + )!; + +const getTransceiverMapping = ( + connection: RTCPeerConnection, + localTrackIdToTrack: Map< + RemoteTrackId, + TrackContextImpl + >, +): MidToTrackId => + connection + .getTransceivers() + .filter((transceiver) => transceiver.sender.track?.id && transceiver.mid) + .reduce( + (acc, transceiver) => { + const localTrackId = transceiver.sender.track!.id; + const mid = transceiver!.mid!; + + const trackContext = getTrackContext(localTrackIdToTrack, localTrackId); + + acc[mid] = trackContext.trackId; + + return acc; + }, + {} as Record, + ); + +const getAllNegotiatedLocalTracksMapping = ( + midToTrackId: Map = new Map(), + localEndpoint: EndpointWithTrackContext, +): MidToTrackId => { + return [...midToTrackId.entries()] + .filter(([_mid, trackId]) => localEndpoint.tracks.get(trackId)) + .reduce( + (acc, [mid, trackId]) => { + acc[mid] = trackId; + + return acc; + }, + {} as Record, + ); +}; diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index d4d0ec23..cfdf3ae0 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -31,12 +31,12 @@ import type { EndpointWithTrackContext } from './internal'; import { TrackContextImpl, isTrackKind } from './internal'; import { handleVoiceActivationDetectionNotification } from './voiceActivityDetection'; import { applyBandwidthLimitation } from './bandwidth'; -import { createTrackVariantBitratesEvent, getTrackBitrates } from './bitrate'; import { findSender, findSenderByTrack, isTrackInUse, } from './RTCPeerConnectionUtils'; +import { createTrackVariantBitratesEvent, getTrackBitrates } from './bitrate'; import { addTrackToConnection, addTransceiversIfNeeded, diff --git a/yarn.lock b/yarn.lock index 83fda72a..6b798cdc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -546,7 +546,6 @@ __metadata: version: 0.0.0-use.local resolution: "@fishjam-e2e/react-client-e2e@workspace:e2e-tests/react-client/app" dependencies: - "@playwright/test": "npm:^1.45.1" "@types/node": "npm:^20.11.18" "@vitest/coverage-v8": "npm:^1.6.0" testcontainers: "npm:^10.3.2" From 28b88717952dae35a222c9fc40903e5aadce3887 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Fri, 5 Jul 2024 16:17:50 +0200 Subject: [PATCH 17/50] Merge --- packages/ts-client/src/webrtc/transceivers.ts | 85 ------------------- .../ts-client/src/webrtc/webRTCEndpoint.ts | 5 +- 2 files changed, 1 insertion(+), 89 deletions(-) delete mode 100644 packages/ts-client/src/webrtc/transceivers.ts diff --git a/packages/ts-client/src/webrtc/transceivers.ts b/packages/ts-client/src/webrtc/transceivers.ts deleted file mode 100644 index 351ee9d4..00000000 --- a/packages/ts-client/src/webrtc/transceivers.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { EndpointWithTrackContext, TrackContextImpl } from './internal'; -import type { RemoteTrackId } from './types'; - -type Mid = string; -type TrackId = string; -type MidToTrackId = Record; - -export const getMidToTrackId = ( - connection: RTCPeerConnection | undefined, - localTrackIdToTrack: Map< - RemoteTrackId, - TrackContextImpl - >, - midToTrackId: Map = new Map(), - localEndpoint: EndpointWithTrackContext, -): MidToTrackId | null => { - if (!connection) return null; - - // - negotiated unmuted tracks: tracks added in previous negotiation, data is being transmitted - // - not yet negotiated tracks: tracks added in this negotiation, data will be transmitted after successful negotiation - const mappingFromTransceivers = getTransceiverMapping( - connection, - localTrackIdToTrack, - ); - - // - negotiated unmuted tracks: tracks added in previous negotiation, data is being transmitted - // - negotiated muted tracks: tracks added in previous negotiation, data is not being transmitted but can be transmitted in the future - const mappingFromLocalNegotiatedTracks = getAllNegotiatedLocalTracksMapping( - midToTrackId, - localEndpoint, - ); - - return { ...mappingFromTransceivers, ...mappingFromLocalNegotiatedTracks }; -}; - -const getTrackContext = ( - localTrackIdToTrack: Map< - RemoteTrackId, - TrackContextImpl - >, - localTrackId: string, -) => - Array.from(localTrackIdToTrack.values()).find( - (trackContext) => trackContext?.track?.id === localTrackId, - )!; - -const getTransceiverMapping = ( - connection: RTCPeerConnection, - localTrackIdToTrack: Map< - RemoteTrackId, - TrackContextImpl - >, -): MidToTrackId => - connection - .getTransceivers() - .filter((transceiver) => transceiver.sender.track?.id && transceiver.mid) - .reduce( - (acc, transceiver) => { - const localTrackId = transceiver.sender.track!.id; - const mid = transceiver!.mid!; - - const trackContext = getTrackContext(localTrackIdToTrack, localTrackId); - - acc[mid] = trackContext.trackId; - - return acc; - }, - {} as Record, - ); - -const getAllNegotiatedLocalTracksMapping = ( - midToTrackId: Map = new Map(), - localEndpoint: EndpointWithTrackContext, -): MidToTrackId => { - return [...midToTrackId.entries()] - .filter(([_mid, trackId]) => localEndpoint.tracks.get(trackId)) - .reduce( - (acc, [mid, trackId]) => { - acc[mid] = trackId; - - return acc; - }, - {} as Record, - ); -}; diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 83e5ad5a..9c3f1e8b 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -8,7 +8,6 @@ import { import { v4 as uuidv4 } from 'uuid'; import EventEmitter from 'events'; import type TypedEmitter from 'typed-emitter'; -import { simulcastTransceiverConfig } from './const'; import type { AddTrackCommand, Command, @@ -35,9 +34,7 @@ import { applyBandwidthLimitation } from './bandwidth'; import { createTrackVariantBitratesEvent, getTrackBitrates, - getTrackIdToTrackBitrates, } from './bitrate'; -import { getMidToTrackId } from './transceivers'; import { findSender, findSenderByTrack, isTrackInUse } from "./RTCPeerConnectionUtils"; import { addTrackToConnection, @@ -54,7 +51,7 @@ export class WebRTCEndpoint< EndpointMetadata = any, TrackMetadata = any, > extends (EventEmitter as { - new (): TypedEmitter< + new(): TypedEmitter< Required> >; }) { From f67e25d87e191348c597c76fd9ad9cbe4932b792 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Fri, 5 Jul 2024 16:18:03 +0200 Subject: [PATCH 18/50] Format --- packages/ts-client/src/webrtc/webRTCEndpoint.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 9c3f1e8b..d4d0ec23 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -31,18 +31,19 @@ import type { EndpointWithTrackContext } from './internal'; import { TrackContextImpl, isTrackKind } from './internal'; import { handleVoiceActivationDetectionNotification } from './voiceActivityDetection'; import { applyBandwidthLimitation } from './bandwidth'; +import { createTrackVariantBitratesEvent, getTrackBitrates } from './bitrate'; import { - createTrackVariantBitratesEvent, - getTrackBitrates, -} from './bitrate'; -import { findSender, findSenderByTrack, isTrackInUse } from "./RTCPeerConnectionUtils"; + findSender, + findSenderByTrack, + isTrackInUse, +} from './RTCPeerConnectionUtils'; import { addTrackToConnection, addTransceiversIfNeeded, setTransceiverDirection, - setTransceiversToReadOnly -} from "./transciever"; -import { createSdpOfferEvent } from "./sdpEvents"; + setTransceiversToReadOnly, +} from './transciever'; +import { createSdpOfferEvent } from './sdpEvents'; /** * Main class that is responsible for connecting to the RTC Engine, sending and receiving media. @@ -51,7 +52,7 @@ export class WebRTCEndpoint< EndpointMetadata = any, TrackMetadata = any, > extends (EventEmitter as { - new(): TypedEmitter< + new (): TypedEmitter< Required> >; }) { From fd09728f554320148115b292bd8339f27615d1df Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 12:03:43 +0200 Subject: [PATCH 19/50] WIP --- e2e-tests/ts-client/app/package.json | 1 + e2e-tests/ts-client/app/playwright.config.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/e2e-tests/ts-client/app/package.json b/e2e-tests/ts-client/app/package.json index 3bed14c7..7633ee70 100644 --- a/e2e-tests/ts-client/app/package.json +++ b/e2e-tests/ts-client/app/package.json @@ -14,6 +14,7 @@ "typecheck": "tsc", "e2e": "NODE_OPTIONS=--dns-result-order=ipv4first playwright test", "e2e:ui": "NODE_OPTIONS=--dns-result-order=ipv4first playwright test --ui", + "e2e:one": "NODE_OPTIONS=--dns-result-order=ipv4first playwright test -g \"Client properly sees 3 other peers\"", "preview": "vite preview" }, "dependencies": { diff --git a/e2e-tests/ts-client/app/playwright.config.ts b/e2e-tests/ts-client/app/playwright.config.ts index 70533a27..d0bdc4a8 100644 --- a/e2e-tests/ts-client/app/playwright.config.ts +++ b/e2e-tests/ts-client/app/playwright.config.ts @@ -18,7 +18,7 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + workers: process.env.CI ? 1 : 4, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: [ ["list"], @@ -56,9 +56,17 @@ export default defineConfig({ ], // default Google Chrome path on MacOS // executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + // executablePath: "/Users/kamilstasiak/Library/Caches/ms-playwright/chromium-1091/chrome-mac/Chromium.app", + executablePath: "/Users/kamilstasiak/Library/Caches/ms-playwright/chromium-1091/chrome-mac/Chromium.app/Contents/MacOS/Chromium", }, }, }, + // { + // name: "safari", + // use: { + // ...devices["Desktop Safari"] + // } + // } ], /* Run your local dev server before starting the tests */ From fd42d83e13736dd450bfca3c3041bf73f58732ea Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 16:49:26 +0200 Subject: [PATCH 20/50] Extract turn to the separate file --- e2e-tests/ts-client/app/playwright.config.ts | 3 +- packages/ts-client/src/webrtc/turn.ts | 30 ++++++++++++++++++ .../ts-client/src/webrtc/webRTCEndpoint.ts | 31 ++----------------- 3 files changed, 34 insertions(+), 30 deletions(-) create mode 100644 packages/ts-client/src/webrtc/turn.ts diff --git a/e2e-tests/ts-client/app/playwright.config.ts b/e2e-tests/ts-client/app/playwright.config.ts index d0bdc4a8..284e2687 100644 --- a/e2e-tests/ts-client/app/playwright.config.ts +++ b/e2e-tests/ts-client/app/playwright.config.ts @@ -57,7 +57,8 @@ export default defineConfig({ // default Google Chrome path on MacOS // executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", // executablePath: "/Users/kamilstasiak/Library/Caches/ms-playwright/chromium-1091/chrome-mac/Chromium.app", - executablePath: "/Users/kamilstasiak/Library/Caches/ms-playwright/chromium-1091/chrome-mac/Chromium.app/Contents/MacOS/Chromium", + executablePath: + "/Users/kamilstasiak/Library/Caches/ms-playwright/chromium-1091/chrome-mac/Chromium.app/Contents/MacOS/Chromium", }, }, }, diff --git a/packages/ts-client/src/webrtc/turn.ts b/packages/ts-client/src/webrtc/turn.ts new file mode 100644 index 00000000..d7baf4cc --- /dev/null +++ b/packages/ts-client/src/webrtc/turn.ts @@ -0,0 +1,30 @@ +export const setTurns = ( + turnServers: any[], + rtcConfig: RTCConfiguration, +): void => { + turnServers.forEach((turnServer: any) => { + let transport, uri; + if (turnServer.transport == 'tls') { + transport = 'tcp'; + uri = 'turns'; + } else { + transport = turnServer.transport; + uri = 'turn'; + } + + const rtcIceServer: RTCIceServer = { + credential: turnServer.password, + urls: uri.concat( + ':', + turnServer.serverAddr, + ':', + turnServer.serverPort, + '?transport=', + transport, + ), + username: turnServer.username, + }; + + rtcConfig.iceServers!.push(rtcIceServer); + }); +}; diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index d4d0ec23..bb4e76e1 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -44,6 +44,7 @@ import { setTransceiversToReadOnly, } from './transciever'; import { createSdpOfferEvent } from './sdpEvents'; +import { setTurns } from './turn'; /** * Main class that is responsible for connecting to the RTC Engine, sending and receiving media. @@ -1341,7 +1342,7 @@ export class WebRTCEndpoint< private onOfferData = async (offerData: MediaEvent) => { if (!this.connection) { const turnServers = offerData.data.integratedTurnServers; - this.setTurns(turnServers); + setTurns(turnServers, this.rtcConfig); this.connection = new RTCPeerConnection(this.rtcConfig); this.connection.onicecandidate = this.onLocalCandidate(); @@ -1491,34 +1492,6 @@ export class WebRTCEndpoint< }; }; - private setTurns = (turnServers: any[]): void => { - turnServers.forEach((turnServer: any) => { - let transport, uri; - if (turnServer.transport == 'tls') { - transport = 'tcp'; - uri = 'turns'; - } else { - transport = turnServer.transport; - uri = 'turn'; - } - - const rtcIceServer: RTCIceServer = { - credential: turnServer.password, - urls: uri.concat( - ':', - turnServer.serverAddr, - ':', - turnServer.serverPort, - '?transport=', - transport, - ), - username: turnServer.username, - }; - - this.rtcConfig.iceServers!.push(rtcIceServer); - }); - }; - private addEndpoint = ( endpoint: EndpointWithTrackContext, ): void => { From 1e4893cd0192fa03e5b7b3ddcdca81c0ed19f573 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 16:55:12 +0200 Subject: [PATCH 21/50] Extract `mapMediaEventTracksToTrackContextImpl` to `./internals.ts` --- packages/ts-client/src/webrtc/internal.ts | 21 ++++++++++++++ .../ts-client/src/webrtc/webRTCEndpoint.ts | 29 ++++--------------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/packages/ts-client/src/webrtc/internal.ts b/packages/ts-client/src/webrtc/internal.ts index 57bae377..df8e9db2 100644 --- a/packages/ts-client/src/webrtc/internal.ts +++ b/packages/ts-client/src/webrtc/internal.ts @@ -70,3 +70,24 @@ export type EndpointWithTrackContext = Omit< > & { tracks: Map>; }; + +export const mapMediaEventTracksToTrackContextImpl = ( + tracks: Map, + endpoint: EndpointWithTrackContext, + trackMetadataParser: MetadataParser +): Map> => { + const mappedTracks: Array< + [string, TrackContextImpl] + > = Array.from(tracks).map(([trackId, track]) => [ + trackId, + new TrackContextImpl( + endpoint, + trackId, + track.metadata, + track.simulcastConfig, + trackMetadataParser, + ), + ]); + + return new Map(mappedTracks); +}; diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index bb4e76e1..0fcf83e3 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -28,6 +28,7 @@ import type { WebRTCEndpointEvents, } from './types'; import type { EndpointWithTrackContext } from './internal'; +import { mapMediaEventTracksToTrackContextImpl } from './internal'; import { TrackContextImpl, isTrackKind } from './internal'; import { handleVoiceActivationDetectionNotification } from './voiceActivityDetection'; import { applyBandwidthLimitation } from './bandwidth'; @@ -53,7 +54,7 @@ export class WebRTCEndpoint< EndpointMetadata = any, TrackMetadata = any, > extends (EventEmitter as { - new (): TypedEmitter< + new(): TypedEmitter< Required> >; }) { @@ -174,11 +175,12 @@ export class WebRTCEndpoint< EndpointMetadata, TrackMetadata >[] = endpoints.map((endpoint) => { - const tracks = this.mapMediaEventTracksToTrackContextImpl( + const tracks = mapMediaEventTracksToTrackContextImpl( new Map>( Object.entries(endpoint.tracks), ), endpoint, + this.trackMetadataParser ); try { @@ -297,9 +299,10 @@ export class WebRTCEndpoint< endpoint = this.idToEndpoint.get(data.endpointId)!; const oldTracks = endpoint.tracks; - data.tracks = this.mapMediaEventTracksToTrackContextImpl( + data.tracks = mapMediaEventTracksToTrackContextImpl( data.tracks, endpoint, + this.trackMetadataParser ); endpoint.tracks = new Map([...endpoint.tracks, ...data.tracks]); @@ -1526,24 +1529,4 @@ export class WebRTCEndpoint< }; private getEndpointId = () => this.localEndpoint.id; - - private mapMediaEventTracksToTrackContextImpl = ( - tracks: Map, - endpoint: EndpointWithTrackContext, - ): Map> => { - const mappedTracks: Array< - [string, TrackContextImpl] - > = Array.from(tracks).map(([trackId, track]) => [ - trackId, - new TrackContextImpl( - endpoint, - trackId, - track.metadata, - track.simulcastConfig, - this.trackMetadataParser, - ), - ]); - - return new Map(mappedTracks); - }; } From 590585127d5669f87298b314316dbaa43115d9a1 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 17:40:41 +0200 Subject: [PATCH 22/50] Extract State manager to the separate file --- .../react-client/app/playwright.config.ts | 2 + e2e-tests/ts-client/app/package.json | 3 +- e2e-tests/ts-client/app/playwright.config.ts | 1 - .../app/src/VideoPlayerWithDetector.tsx | 2 +- e2e-tests/ts-client/scenarios/utils.ts | 4 +- packages/ts-client/src/webrtc/StateManager.ts | 52 +++ packages/ts-client/src/webrtc/internal.ts | 7 +- .../ts-client/src/webrtc/webRTCEndpoint.ts | 428 +++++++++--------- .../tests/events/connectedEvent.test.ts | 2 +- .../tests/events/trackAddedEvent.test.ts | 7 +- .../tests/methods/addTrackMethod.test.ts | 5 +- .../tests/methods/cleanUpMethod.test.ts | 2 +- .../tests/methods/connectMethod.test.ts | 4 +- .../tests/methods/disconnectMethod.test.ts | 2 +- packages/ts-client/tests/utils.ts | 2 +- yarn.lock | 1 + 16 files changed, 298 insertions(+), 226 deletions(-) create mode 100644 packages/ts-client/src/webrtc/StateManager.ts diff --git a/e2e-tests/react-client/app/playwright.config.ts b/e2e-tests/react-client/app/playwright.config.ts index e722a51e..7857fe30 100644 --- a/e2e-tests/react-client/app/playwright.config.ts +++ b/e2e-tests/react-client/app/playwright.config.ts @@ -50,6 +50,8 @@ export default defineConfig({ ], // default Google Chrome path on MacOS // executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + executablePath: + "/Users/kamilstasiak/Library/Caches/ms-playwright/chromium-1091/chrome-mac/Chromium.app/Contents/MacOS/Chromium", }, }, }, diff --git a/e2e-tests/ts-client/app/package.json b/e2e-tests/ts-client/app/package.json index 460b77b0..e677fa2e 100644 --- a/e2e-tests/ts-client/app/package.json +++ b/e2e-tests/ts-client/app/package.json @@ -20,7 +20,8 @@ "@fishjam-dev/ts-client": "*", "protobufjs": "^7.2.6", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "typescript": "^5.5.0" }, "devDependencies": { "@playwright/test": "^1.41.2", diff --git a/e2e-tests/ts-client/app/playwright.config.ts b/e2e-tests/ts-client/app/playwright.config.ts index 284e2687..86793c7c 100644 --- a/e2e-tests/ts-client/app/playwright.config.ts +++ b/e2e-tests/ts-client/app/playwright.config.ts @@ -56,7 +56,6 @@ export default defineConfig({ ], // default Google Chrome path on MacOS // executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", - // executablePath: "/Users/kamilstasiak/Library/Caches/ms-playwright/chromium-1091/chrome-mac/Chromium.app", executablePath: "/Users/kamilstasiak/Library/Caches/ms-playwright/chromium-1091/chrome-mac/Chromium.app/Contents/MacOS/Chromium", }, diff --git a/e2e-tests/ts-client/app/src/VideoPlayerWithDetector.tsx b/e2e-tests/ts-client/app/src/VideoPlayerWithDetector.tsx index 97de14a6..507614e2 100644 --- a/e2e-tests/ts-client/app/src/VideoPlayerWithDetector.tsx +++ b/e2e-tests/ts-client/app/src/VideoPlayerWithDetector.tsx @@ -46,7 +46,7 @@ export const VideoPlayerWithDetector = ({ stream, id, webrtc }: Props) => { }, [stream]); const getDecodedFrames = useCallback(async () => { - const connection = webrtc["connection"]; + const connection = webrtc["stateManager"]["connection"]; if (!connection) return 0; const inbound = getTrackIdentifierToInboundRtp(await connection.getStats()); diff --git a/e2e-tests/ts-client/scenarios/utils.ts b/e2e-tests/ts-client/scenarios/utils.ts index f1aa5d72..b3d42231 100644 --- a/e2e-tests/ts-client/scenarios/utils.ts +++ b/e2e-tests/ts-client/scenarios/utils.ts @@ -137,14 +137,14 @@ export const assertThatRemoteTracksAreVisible = async ( }); }; -type WebrtcClient = { webrtc?: { connection?: RTCPeerConnection } }; +type WebrtcClient = { webrtc?: { stateManager: { connection?: RTCPeerConnection } } }; type WindowType = typeof window; export const assertThatOtherVideoIsPlaying = async (page: Page) => { await test.step('Assert that media is working', async () => { const getDecodedFrames: () => Promise = () => page.evaluate(async () => { - const connection = (window as WindowType & WebrtcClient)?.webrtc + const connection = (window as WindowType & WebrtcClient)?.webrtc?.stateManager ?.connection; // connection object is available after first renegotiation (sdpOffer, sdpAnswer) if (!window || !connection) return -1; diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts new file mode 100644 index 00000000..4d04b049 --- /dev/null +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -0,0 +1,52 @@ +import type { LocalTrackId, RemoteTrackId, TrackEncoding } from './types'; +import type { EndpointWithTrackContext } from './internal'; +import type { TrackContextImpl } from './internal'; + +export class StateManager { + public trackIdToTrack: Map< + string, + TrackContextImpl + > = new Map(); + public connection?: RTCPeerConnection; + public idToEndpoint: Map< + string, + EndpointWithTrackContext + > = new Map(); + public localEndpoint: EndpointWithTrackContext< + EndpointMetadata, + TrackMetadata + > = { + id: '', + type: 'webrtc', + metadata: undefined, + rawMetadata: undefined, + tracks: new Map(), + }; + public localTrackIdToTrack: Map< + RemoteTrackId, + TrackContextImpl + > = new Map(); + public trackIdToSender: Map< + RemoteTrackId, + { + remoteTrackId: RemoteTrackId; + localTrackId: LocalTrackId | null; + sender: RTCRtpSender | null; + } + > = new Map(); + public midToTrackId: Map = new Map(); + public disabledTrackEncodings: Map = new Map(); + public rtcConfig: RTCConfiguration = { + bundlePolicy: 'max-bundle', + iceServers: [], + iceTransportPolicy: 'relay', + }; + public bandwidthEstimation: bigint = BigInt(0); + + /** + * Indicates if an ongoing renegotiation is active. + * During renegotiation, both parties are expected to actively exchange events: renegotiateTracks, offerData, sdpOffer, sdpAnswer. + */ + public ongoingRenegotiation: boolean = false; + public ongoingTrackReplacement: boolean = false; +} diff --git a/packages/ts-client/src/webrtc/internal.ts b/packages/ts-client/src/webrtc/internal.ts index df8e9db2..d8bb0973 100644 --- a/packages/ts-client/src/webrtc/internal.ts +++ b/packages/ts-client/src/webrtc/internal.ts @@ -71,10 +71,13 @@ export type EndpointWithTrackContext = Omit< tracks: Map>; }; -export const mapMediaEventTracksToTrackContextImpl = ( +export const mapMediaEventTracksToTrackContextImpl = < + EndpointMetadata, + TrackMetadata, +>( tracks: Map, endpoint: EndpointWithTrackContext, - trackMetadataParser: MetadataParser + trackMetadataParser: MetadataParser, ): Map> => { const mappedTracks: Array< [string, TrackContextImpl] diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 0fcf83e3..2d5b3274 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -18,9 +18,7 @@ import { Deferred } from './deferred'; import type { BandwidthLimit, Config, - LocalTrackId, MetadataParser, - RemoteTrackId, SimulcastConfig, TrackBandwidthLimit, TrackContext, @@ -46,6 +44,7 @@ import { } from './transciever'; import { createSdpOfferEvent } from './sdpEvents'; import { setTurns } from './turn'; +import { StateManager } from './StateManager'; /** * Main class that is responsible for connecting to the RTC Engine, sending and receiving media. @@ -54,68 +53,26 @@ export class WebRTCEndpoint< EndpointMetadata = any, TrackMetadata = any, > extends (EventEmitter as { - new(): TypedEmitter< + new (): TypedEmitter< Required> >; }) { - private trackIdToTrack: Map< - string, - TrackContextImpl - > = new Map(); - private connection?: RTCPeerConnection; - private idToEndpoint: Map< - string, - EndpointWithTrackContext - > = new Map(); - private localEndpoint: EndpointWithTrackContext< - EndpointMetadata, - TrackMetadata - > = { - id: '', - type: 'webrtc', - metadata: undefined, - rawMetadata: undefined, - tracks: new Map(), - }; - private localTrackIdToTrack: Map< - RemoteTrackId, - TrackContextImpl - > = new Map(); - private trackIdToSender: Map< - RemoteTrackId, - { - remoteTrackId: RemoteTrackId; - localTrackId: LocalTrackId | null; - sender: RTCRtpSender | null; - } - > = new Map(); - private midToTrackId: Map = new Map(); - private disabledTrackEncodings: Map = new Map(); - private rtcConfig: RTCConfiguration = { - bundlePolicy: 'max-bundle', - iceServers: [], - iceTransportPolicy: 'relay', - }; - private bandwidthEstimation: bigint = BigInt(0); - - /** - * Indicates if an ongoing renegotiation is active. - * During renegotiation, both parties are expected to actively exchange events: renegotiateTracks, offerData, sdpOffer, sdpAnswer. - */ - private ongoingRenegotiation: boolean = false; - private ongoingTrackReplacement: boolean = false; private commandsQueue: Command[] = []; private commandResolutionNotifier: Deferred | null = null; private readonly endpointMetadataParser: MetadataParser; private readonly trackMetadataParser: MetadataParser; + private stateManager: StateManager; + constructor(config?: Config) { super(); this.endpointMetadataParser = config?.endpointMetadataParser ?? ((x) => x as EndpointMetadata); this.trackMetadataParser = config?.trackMetadataParser ?? ((x) => x as TrackMetadata); + + this.stateManager = new StateManager(); } /** @@ -133,16 +90,17 @@ export class WebRTCEndpoint< */ public connect = (metadata: EndpointMetadata): void => { try { - this.localEndpoint.metadata = this.endpointMetadataParser(metadata); - this.localEndpoint.metadataParsingError = undefined; + this.stateManager.localEndpoint.metadata = + this.endpointMetadataParser(metadata); + this.stateManager.localEndpoint.metadataParsingError = undefined; } catch (error) { - this.localEndpoint.metadata = undefined; - this.localEndpoint.metadataParsingError = error; + this.stateManager.localEndpoint.metadata = undefined; + this.stateManager.localEndpoint.metadataParsingError = error; throw error; } - this.localEndpoint.rawMetadata = metadata; + this.stateManager.localEndpoint.rawMetadata = metadata; const mediaEvent = generateMediaEvent('connect', { - metadata: this.localEndpoint.metadata, + metadata: this.stateManager.localEndpoint.metadata, }); this.sendMediaEvent(mediaEvent); }; @@ -167,7 +125,7 @@ export class WebRTCEndpoint< const deserializedMediaEvent = deserializeMediaEvent(mediaEvent); switch (deserializedMediaEvent.type) { case 'connected': { - this.localEndpoint.id = deserializedMediaEvent.data.id; + this.stateManager.localEndpoint.id = deserializedMediaEvent.data.id; const endpoints: any[] = deserializedMediaEvent.data.otherEndpoints; @@ -175,12 +133,15 @@ export class WebRTCEndpoint< EndpointMetadata, TrackMetadata >[] = endpoints.map((endpoint) => { - const tracks = mapMediaEventTracksToTrackContextImpl( + const tracks = mapMediaEventTracksToTrackContextImpl< + EndpointMetadata, + TrackMetadata + >( new Map>( Object.entries(endpoint.tracks), ), endpoint, - this.trackMetadataParser + this.trackMetadataParser, ); try { @@ -213,12 +174,12 @@ export class WebRTCEndpoint< this.emit('connected', deserializedMediaEvent.data.id, otherEndpoints); otherEndpoints.forEach((endpoint) => - this.idToEndpoint.set(endpoint.id, endpoint), + this.stateManager.idToEndpoint.set(endpoint.id, endpoint), ); otherEndpoints.forEach((endpoint) => { endpoint.tracks.forEach((ctx, trackId) => { - this.trackIdToTrack.set(trackId, ctx); + this.stateManager.trackIdToTrack.set(trackId, ctx); this.emit('trackAdded', ctx); }); @@ -226,7 +187,7 @@ export class WebRTCEndpoint< break; } default: - if (this.localEndpoint.id != null) + if (this.stateManager.localEndpoint.id != null) this.handleMediaEvent(deserializedMediaEvent); } }; @@ -243,7 +204,9 @@ export class WebRTCEndpoint< public async getStatistics( selector?: MediaStreamTrack | null, ): Promise { - return (await this.connection?.getStats(selector)) ?? new Map(); + return ( + (await this.stateManager.connection?.getStats(selector)) ?? new Map() + ); } /** @@ -258,7 +221,7 @@ export class WebRTCEndpoint< string, TrackContext > { - return Object.fromEntries(this.trackIdToTrack.entries()); + return Object.fromEntries(this.stateManager.trackIdToTrack.entries()); } /** @@ -268,18 +231,18 @@ export class WebRTCEndpoint< string, EndpointWithTrackContext > { - return Object.fromEntries(this.idToEndpoint.entries()); + return Object.fromEntries(this.stateManager.idToEndpoint.entries()); } public getLocalEndpoint(): EndpointWithTrackContext< EndpointMetadata, TrackMetadata > { - return this.localEndpoint; + return this.stateManager.localEndpoint; } public getBandwidthEstimation(): bigint { - return this.bandwidthEstimation; + return this.stateManager.bandwidthEstimation; } private handleMediaEvent = (deserializedMediaEvent: MediaEvent) => { @@ -291,26 +254,26 @@ export class WebRTCEndpoint< break; } case 'tracksAdded': { - this.ongoingRenegotiation = true; + this.stateManager.ongoingRenegotiation = true; data = deserializedMediaEvent.data; if (this.getEndpointId() === data.endpointId) return; data.tracks = new Map(Object.entries(data.tracks)); - endpoint = this.idToEndpoint.get(data.endpointId)!; + endpoint = this.stateManager.idToEndpoint.get(data.endpointId)!; const oldTracks = endpoint.tracks; data.tracks = mapMediaEventTracksToTrackContextImpl( data.tracks, endpoint, - this.trackMetadataParser + this.trackMetadataParser, ); endpoint.tracks = new Map([...endpoint.tracks, ...data.tracks]); - this.idToEndpoint.set(endpoint.id, endpoint); + this.stateManager.idToEndpoint.set(endpoint.id, endpoint); Array.from(endpoint.tracks.entries()).forEach(([trackId, ctx]) => { if (!oldTracks.has(trackId)) { - this.trackIdToTrack.set(trackId, ctx); + this.stateManager.trackIdToTrack.set(trackId, ctx); this.emit('trackAdded', ctx); } @@ -318,14 +281,14 @@ export class WebRTCEndpoint< break; } case 'tracksRemoved': { - this.ongoingRenegotiation = true; + this.stateManager.ongoingRenegotiation = true; data = deserializedMediaEvent.data; const endpointId = data.endpointId; if (this.getEndpointId() === endpointId) return; const trackIds = data.trackIds as string[]; trackIds.forEach((trackId) => { - const trackContext = this.trackIdToTrack.get(trackId)!; + const trackContext = this.stateManager.trackIdToTrack.get(trackId)!; this.eraseTrack(trackId, endpointId); @@ -335,14 +298,16 @@ export class WebRTCEndpoint< } case 'sdpAnswer': - this.midToTrackId = new Map( + this.stateManager.midToTrackId = new Map( Object.entries(deserializedMediaEvent.data.midToTrackId), ); for (const trackId of Object.values( deserializedMediaEvent.data.midToTrackId, )) { - const track = this.localTrackIdToTrack.get(trackId as string); + const track = this.stateManager.localTrackIdToTrack.get( + trackId as string, + ); // if is local track if (track) { track.negotiationStatus = 'done'; @@ -361,7 +326,7 @@ export class WebRTCEndpoint< this.onAnswer(deserializedMediaEvent.data); - this.ongoingRenegotiation = false; + this.stateManager.ongoingRenegotiation = false; this.processNextCommand(); break; @@ -386,17 +351,24 @@ export class WebRTCEndpoint< break; case 'endpointRemoved': - if (deserializedMediaEvent.data.id === this.localEndpoint.id) { + if ( + deserializedMediaEvent.data.id === this.stateManager.localEndpoint.id + ) { this.cleanUp(); this.emit('disconnected'); return; } - endpoint = this.idToEndpoint.get(deserializedMediaEvent.data.id)!; + endpoint = this.stateManager.idToEndpoint.get( + deserializedMediaEvent.data.id, + )!; if (endpoint === undefined) return; Array.from(endpoint.tracks.keys()).forEach((trackId) => { - this.emit('trackRemoved', this.trackIdToTrack.get(trackId)!); + this.emit( + 'trackRemoved', + this.stateManager.trackIdToTrack.get(trackId)!, + ); }); this.eraseEndpoint(endpoint); @@ -406,7 +378,9 @@ export class WebRTCEndpoint< case 'endpointUpdated': if (this.getEndpointId() === deserializedMediaEvent.data.id) return; - endpoint = this.idToEndpoint.get(deserializedMediaEvent.data.id)!; + endpoint = this.stateManager.idToEndpoint.get( + deserializedMediaEvent.data.id, + )!; try { endpoint.metadata = this.endpointMetadataParser( deserializedMediaEvent.data.metadata, @@ -426,7 +400,7 @@ export class WebRTCEndpoint< if (this.getEndpointId() === deserializedMediaEvent.data.endpointId) return; - endpoint = this.idToEndpoint.get( + endpoint = this.stateManager.idToEndpoint.get( deserializedMediaEvent.data.endpointId, )!; if (endpoint == null) @@ -435,7 +409,7 @@ export class WebRTCEndpoint< const trackId = deserializedMediaEvent.data.trackId; const trackMetadata = deserializedMediaEvent.data.metadata; let newTrack = endpoint.tracks.get(trackId)!; - const trackContext = this.trackIdToTrack.get(trackId)!; + const trackContext = this.stateManager.trackIdToTrack.get(trackId)!; try { const parsedMetadata = this.trackMetadataParser(trackMetadata); newTrack = { @@ -466,7 +440,7 @@ export class WebRTCEndpoint< if (this.getEndpointId() === deserializedMediaEvent.data.endpointId) return; - endpoint = this.idToEndpoint.get( + endpoint = this.stateManager.idToEndpoint.get( deserializedMediaEvent.data.endpointId, )!; if (endpoint == null) @@ -485,7 +459,7 @@ export class WebRTCEndpoint< if (this.getEndpointId() === deserializedMediaEvent.data.endpointId) return; - endpoint = this.idToEndpoint.get( + endpoint = this.stateManager.idToEndpoint.get( deserializedMediaEvent.data.endpointId, )!; if (endpoint == null) @@ -503,18 +477,18 @@ export class WebRTCEndpoint< case 'tracksPriority': { const enabledTracks = ( deserializedMediaEvent.data.tracks as string[] - ).map((trackId) => this.trackIdToTrack.get(trackId)!); + ).map((trackId) => this.stateManager.trackIdToTrack.get(trackId)!); - const disabledTracks = Array.from(this.trackIdToTrack.values()).filter( - (track) => !enabledTracks.includes(track), - ); + const disabledTracks = Array.from( + this.stateManager.trackIdToTrack.values(), + ).filter((track) => !enabledTracks.includes(track)); this.emit('tracksPriorityChanged', enabledTracks, disabledTracks); break; } case 'encodingSwitched': { const trackId = deserializedMediaEvent.data.trackId; - const trackContext = this.trackIdToTrack.get(trackId)!; + const trackContext = this.stateManager.trackIdToTrack.get(trackId)!; trackContext.encoding = deserializedMediaEvent.data.encoding; trackContext.encodingReason = deserializedMediaEvent.data.reason; @@ -536,15 +510,19 @@ export class WebRTCEndpoint< case 'vadNotification': { handleVoiceActivationDetectionNotification( deserializedMediaEvent, - this.trackIdToTrack, + this.stateManager.trackIdToTrack, ); break; } case 'bandwidthEstimation': { - this.bandwidthEstimation = deserializedMediaEvent.data.estimation; + this.stateManager.bandwidthEstimation = + deserializedMediaEvent.data.estimation; - this.emit('bandwidthEstimationChanged', this.bandwidthEstimation); + this.emit( + 'bandwidthEstimationChanged', + this.stateManager.bandwidthEstimation, + ); break; } @@ -668,13 +646,17 @@ export class WebRTCEndpoint< } private processNextCommand() { - if (this.ongoingRenegotiation || this.ongoingTrackReplacement) return; + if ( + this.stateManager.ongoingRenegotiation || + this.stateManager.ongoingTrackReplacement + ) + return; if ( - this.connection && - (this.connection.signalingState !== 'stable' || - this.connection.connectionState !== 'connected' || - this.connection.iceConnectionState !== 'connected') + this.stateManager.connection && + (this.stateManager.connection.signalingState !== 'stable' || + this.stateManager.connection.connectionState !== 'connected' || + this.stateManager.connection.iceConnectionState !== 'connected') ) return; @@ -704,7 +686,7 @@ export class WebRTCEndpoint< trackMetadata, trackId, } = addTrackCommand; - const isUsedTrack = isTrackInUse(this.connection, track); + const isUsedTrack = isTrackInUse(this.stateManager.connection, track); let error; if (isUsedTrack) { @@ -725,10 +707,10 @@ export class WebRTCEndpoint< return; } - this.ongoingRenegotiation = true; + this.stateManager.ongoingRenegotiation = true; const trackContext = new TrackContextImpl( - this.localEndpoint, + this.stateManager.localEndpoint, trackId, trackMetadata, simulcastConfig, @@ -742,21 +724,21 @@ export class WebRTCEndpoint< trackContext.maxBandwidth = maxBandwidth; trackContext.trackKind = track.kind; - this.localEndpoint.tracks.set(trackId, trackContext); + this.stateManager.localEndpoint.tracks.set(trackId, trackContext); - this.localTrackIdToTrack.set(trackId, trackContext); + this.stateManager.localTrackIdToTrack.set(trackId, trackContext); - if (this.connection) { + if (this.stateManager.connection) { addTrackToConnection( trackContext, - this.disabledTrackEncodings, - this.connection, + this.stateManager.disabledTrackEncodings, + this.stateManager.connection, ); - setTransceiverDirection(this.connection); + setTransceiverDirection(this.stateManager.connection); } - this.trackIdToSender.set(trackId, { + this.stateManager.trackIdToSender.set(trackId, { remoteTrackId: trackId, localTrackId: track.id, sender: null, @@ -850,15 +832,15 @@ export class WebRTCEndpoint< // todo add validation to track.kind, you cannot replace video with audio - const trackContext = this.localTrackIdToTrack.get(trackId)!; + const trackContext = this.stateManager.localTrackIdToTrack.get(trackId)!; - const track = this.trackIdToSender.get(trackId); + const track = this.stateManager.trackIdToSender.get(trackId); const sender = track?.sender ?? null; if (!track) throw Error(`There is no track with id: ${trackId}`); if (!sender) throw Error('There is no RTCRtpSender for this track id!'); - this.ongoingTrackReplacement = true; + this.stateManager.ongoingTrackReplacement = true; trackContext.stream?.getTracks().forEach((track) => { trackContext.stream?.removeTrack(track); @@ -893,7 +875,7 @@ export class WebRTCEndpoint< // ignore } finally { this.resolvePreviousCommand(); - this.ongoingTrackReplacement = false; + this.stateManager.ongoingTrackReplacement = false; this.processNextCommand(); } } @@ -912,13 +894,16 @@ export class WebRTCEndpoint< bandwidth: BandwidthLimit, ): Promise { // FIXME: maxBandwidth in TrackContext is not updated - const trackContext = this.localTrackIdToTrack.get(trackId); + const trackContext = this.stateManager.localTrackIdToTrack.get(trackId); if (!trackContext) { return Promise.reject(`Track '${trackId}' doesn't exist`); } - const sender = findSender(this.connection, trackContext.track!.id); + const sender = findSender( + this.stateManager.connection, + trackContext.track!.id, + ); const parameters = sender.getParameters(); if (parameters.encodings.length === 0) { @@ -932,8 +917,8 @@ export class WebRTCEndpoint< .then(() => { const mediaEvent = createTrackVariantBitratesEvent( trackId, - this.connection, - this.localTrackIdToTrack, + this.stateManager.connection, + this.stateManager.localTrackIdToTrack, ); this.sendMediaEvent(mediaEvent); @@ -959,13 +944,16 @@ export class WebRTCEndpoint< rid: string, bandwidth: BandwidthLimit, ): Promise { - const trackContext = this.localTrackIdToTrack.get(trackId)!; + const trackContext = this.stateManager.localTrackIdToTrack.get(trackId)!; if (!trackContext) { return Promise.reject(`Track '${trackId}' doesn't exist`); } - const sender = findSender(this.connection, trackContext.track!.id); + const sender = findSender( + this.stateManager.connection, + trackContext.track!.id, + ); const parameters = sender.getParameters(); const encoding = parameters.encodings.find( (encoding) => encoding.rid === rid, @@ -987,8 +975,8 @@ export class WebRTCEndpoint< data: { trackId: trackId, variantBitrates: getTrackBitrates( - this.connection, - this.localTrackIdToTrack, + this.stateManager.connection, + this.stateManager.localTrackIdToTrack, trackId, ), }, @@ -1047,16 +1035,19 @@ export class WebRTCEndpoint< private removeTrackHandler(command: RemoveTrackCommand) { const { trackId } = command; - const trackContext = this.localTrackIdToTrack.get(trackId)!; - const sender = findSender(this.connection, trackContext.track!.id); + const trackContext = this.stateManager.localTrackIdToTrack.get(trackId)!; + const sender = findSender( + this.stateManager.connection, + trackContext.track!.id, + ); - this.ongoingRenegotiation = true; + this.stateManager.ongoingRenegotiation = true; - this.connection!.removeTrack(sender); + this.stateManager.connection!.removeTrack(sender); const mediaEvent = generateCustomEvent({ type: 'renegotiateTracks' }); this.sendMediaEvent(mediaEvent); - this.localTrackIdToTrack.delete(trackId); - this.localEndpoint.tracks.delete(trackId); + this.stateManager.localTrackIdToTrack.delete(trackId); + this.stateManager.localEndpoint.tracks.delete(trackId); } /** @@ -1074,7 +1065,7 @@ export class WebRTCEndpoint< * ``` */ public setTargetTrackEncoding(trackId: string, variant: TrackEncoding) { - const trackContext = this.trackIdToTrack.get(trackId); + const trackContext = this.stateManager.trackIdToTrack.get(trackId); if ( !trackContext?.simulcastConfig?.enabled || !trackContext.simulcastConfig.activeEncodings.includes(variant) @@ -1110,13 +1101,16 @@ export class WebRTCEndpoint< * ``` */ public enableTrackEncoding(trackId: string, encoding: TrackEncoding) { - const track = this.localTrackIdToTrack.get(trackId)?.track; + const track = this.stateManager.localTrackIdToTrack.get(trackId)?.track; // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain - const newDisabledTrackEncodings = this.disabledTrackEncodings + const newDisabledTrackEncodings = this.stateManager.disabledTrackEncodings .get(trackId) ?.filter((en) => en !== encoding)!; - this.disabledTrackEncodings.set(trackId, newDisabledTrackEncodings); - const sender = findSenderByTrack(this.connection, track); + this.stateManager.disabledTrackEncodings.set( + trackId, + newDisabledTrackEncodings, + ); + const sender = findSenderByTrack(this.stateManager.connection, track); const params = sender?.getParameters(); params!.encodings.filter((en) => en.rid == encoding)[0].active = true; sender?.setParameters(params!); @@ -1143,10 +1137,10 @@ export class WebRTCEndpoint< * ``` */ public disableTrackEncoding(trackId: string, encoding: TrackEncoding) { - const track = this.localTrackIdToTrack.get(trackId)?.track; - this.disabledTrackEncodings.get(trackId)!.push(encoding); + const track = this.stateManager.localTrackIdToTrack.get(trackId)?.track; + this.stateManager.disabledTrackEncodings.get(trackId)!.push(encoding); - const sender = findSenderByTrack(this.connection, track); + const sender = findSenderByTrack(this.stateManager.connection, track); const params = sender?.getParameters(); params!.encodings.filter((en) => en.rid == encoding)[0].active = false; @@ -1171,11 +1165,13 @@ export class WebRTCEndpoint< * event `endpointUpdated` will be emitted for other endpoint in the room. */ public updateEndpointMetadata = (metadata: any): void => { - this.localEndpoint.metadata = this.endpointMetadataParser(metadata); - this.localEndpoint.rawMetadata = this.localEndpoint.metadata; - this.localEndpoint.metadataParsingError = undefined; + this.stateManager.localEndpoint.metadata = + this.endpointMetadataParser(metadata); + this.stateManager.localEndpoint.rawMetadata = + this.stateManager.localEndpoint.metadata; + this.stateManager.localEndpoint.metadataParsingError = undefined; const mediaEvent = generateMediaEvent('updateEndpointMetadata', { - metadata: this.localEndpoint.metadata, + metadata: this.stateManager.localEndpoint.metadata, }); this.sendMediaEvent(mediaEvent); this.emit('localEndpointMetadataChanged', { @@ -1192,18 +1188,18 @@ export class WebRTCEndpoint< * event `trackUpdated` will be emitted for other endpoints in the room. */ public updateTrackMetadata = (trackId: string, trackMetadata: any): void => { - const trackContext = this.localTrackIdToTrack.get(trackId)!; - const prevTrack = this.localEndpoint.tracks.get(trackId)!; + const trackContext = this.stateManager.localTrackIdToTrack.get(trackId)!; + const prevTrack = this.stateManager.localEndpoint.tracks.get(trackId)!; try { trackContext.metadata = this.trackMetadataParser(trackMetadata); trackContext.rawMetadata = trackMetadata; trackContext.metadataParsingError = undefined; - this.localEndpoint.tracks.set(trackId, trackContext); + this.stateManager.localEndpoint.tracks.set(trackId, trackContext); } catch (error) { trackContext.metadata = undefined; trackContext.metadataParsingError = error; - this.localEndpoint.tracks.set(trackId, { + this.stateManager.localEndpoint.tracks.set(trackId, { ...prevTrack, metadata: undefined, metadataParsingError: error, @@ -1211,7 +1207,7 @@ export class WebRTCEndpoint< throw error; } - this.localTrackIdToTrack.set(trackId, trackContext); + this.stateManager.localTrackIdToTrack.set(trackId, trackContext); const mediaEvent = generateMediaEvent('updateTrackMetadata', { trackId, @@ -1256,22 +1252,22 @@ export class WebRTCEndpoint< * Cleans up {@link WebRTCEndpoint} instance. */ public cleanUp = () => { - if (this.connection) { - this.connection.onicecandidate = null; - this.connection.ontrack = null; - this.connection.onconnectionstatechange = null; - this.connection.onicecandidateerror = null; - this.connection.oniceconnectionstatechange = null; - this.connection.close(); + if (this.stateManager.connection) { + this.stateManager.connection.onicecandidate = null; + this.stateManager.connection.ontrack = null; + this.stateManager.connection.onconnectionstatechange = null; + this.stateManager.connection.onicecandidateerror = null; + this.stateManager.connection.oniceconnectionstatechange = null; + this.stateManager.connection.close(); this.commandResolutionNotifier?.reject('Disconnected'); this.commandResolutionNotifier = null; this.commandsQueue = []; - this.ongoingTrackReplacement = false; - this.ongoingRenegotiation = false; + this.stateManager.ongoingTrackReplacement = false; + this.stateManager.ongoingRenegotiation = false; } - this.connection = undefined; + this.stateManager.connection = undefined; }; private getTrackId(uuid: string): string { @@ -1284,10 +1280,10 @@ export class WebRTCEndpoint< }; private onAnswer = async (answer: RTCSessionDescriptionInit) => { - this.connection!.ontrack = this.onTrack(); + this.stateManager.connection!.ontrack = this.onTrack(); try { - await this.connection!.setRemoteDescription(answer); - this.disabledTrackEncodings.forEach( + await this.stateManager.connection!.setRemoteDescription(answer); + this.stateManager.disabledTrackEncodings.forEach( (encodings: TrackEncoding[], trackId: string) => { encodings.forEach((encoding: TrackEncoding) => this.disableTrackEncoding(trackId, encoding), @@ -1300,33 +1296,33 @@ export class WebRTCEndpoint< }; private async createAndSendOffer() { - const connection = this.connection; + const connection = this.stateManager.connection; if (!connection) return; try { const offer = await connection.createOffer(); - if (!this.connection) { + if (!this.stateManager.connection) { console.warn('RTCPeerConnection stopped or restarted'); return; } await connection.setLocalDescription(offer); - if (!this.connection) { + if (!this.stateManager.connection) { console.warn('RTCPeerConnection stopped or restarted'); return; } const mediaEvent = createSdpOfferEvent( offer, - this.connection, - this.localTrackIdToTrack, - this.localEndpoint, - this.midToTrackId, + this.stateManager.connection, + this.stateManager.localTrackIdToTrack, + this.stateManager.localEndpoint, + this.stateManager.midToTrackId, ); this.sendMediaEvent(mediaEvent); - for (const track of this.localTrackIdToTrack.values()) { + for (const track of this.stateManager.localTrackIdToTrack.values()) { track.negotiationStatus = 'offered'; } } catch (error) { @@ -1343,38 +1339,42 @@ export class WebRTCEndpoint< ); private onOfferData = async (offerData: MediaEvent) => { - if (!this.connection) { + if (!this.stateManager.connection) { const turnServers = offerData.data.integratedTurnServers; - setTurns(turnServers, this.rtcConfig); - - this.connection = new RTCPeerConnection(this.rtcConfig); - this.connection.onicecandidate = this.onLocalCandidate(); - this.connection.onicecandidateerror = this.onIceCandidateError as ( - event: Event, - ) => void; - this.connection.onconnectionstatechange = this.onConnectionStateChange; - this.connection.oniceconnectionstatechange = + setTurns(turnServers, this.stateManager.rtcConfig); + + this.stateManager.connection = new RTCPeerConnection( + this.stateManager.rtcConfig, + ); + this.stateManager.connection.onicecandidate = this.onLocalCandidate(); + this.stateManager.connection.onicecandidateerror = this + .onIceCandidateError as (event: Event) => void; + this.stateManager.connection.onconnectionstatechange = + this.onConnectionStateChange; + this.stateManager.connection.oniceconnectionstatechange = this.onIceConnectionStateChange; - this.connection.onicegatheringstatechange = + this.stateManager.connection.onicegatheringstatechange = this.onIceGatheringStateChange; - this.connection.onsignalingstatechange = this.onSignalingStateChange; - - Array.from(this.localTrackIdToTrack.values()).forEach((trackContext) => - addTrackToConnection( - trackContext, - this.disabledTrackEncodings, - this.connection, - ), + this.stateManager.connection.onsignalingstatechange = + this.onSignalingStateChange; + + Array.from(this.stateManager.localTrackIdToTrack.values()).forEach( + (trackContext) => + addTrackToConnection( + trackContext, + this.stateManager.disabledTrackEncodings, + this.stateManager.connection, + ), ); - setTransceiversToReadOnly(this.connection); + setTransceiversToReadOnly(this.stateManager.connection); } else { - this.connection.restartIce(); + this.stateManager.connection.restartIce(); } - this.trackIdToSender.forEach((sth) => { + this.stateManager.trackIdToSender.forEach((sth) => { if (sth.localTrackId) { - sth.sender = findSender(this.connection, sth.localTrackId); + sth.sender = findSender(this.stateManager.connection, sth.localTrackId); } }); @@ -1382,7 +1382,7 @@ export class WebRTCEndpoint< Object.entries(offerData.data.tracksTypes), ); - addTransceiversIfNeeded(this.connection, tracks); + addTransceiversIfNeeded(this.stateManager.connection, tracks); await this.createAndSendOffer(); }; @@ -1390,12 +1390,12 @@ export class WebRTCEndpoint< private onRemoteCandidate = (candidate: RTCIceCandidate) => { try { const iceCandidate = new RTCIceCandidate(candidate); - if (!this.connection) { + if (!this.stateManager.connection) { throw new Error( 'Received new remote candidate but RTCConnection is undefined', ); } - this.connection.addIceCandidate(iceCandidate); + this.stateManager.connection.addIceCandidate(iceCandidate); } catch (error) { console.error(error); } @@ -1421,7 +1421,7 @@ export class WebRTCEndpoint< }; private onConnectionStateChange = (event: Event) => { - switch (this.connection?.connectionState) { + switch (this.stateManager.connection?.connectionState) { case 'connected': this.processNextCommand(); break; @@ -1435,7 +1435,7 @@ export class WebRTCEndpoint< }; private onIceConnectionStateChange = (event: Event) => { - switch (this.connection?.iceConnectionState) { + switch (this.stateManager.connection?.iceConnectionState) { case 'disconnected': console.warn('ICE connection: disconnected'); // Requesting renegotiation on ICE connection state failed fixes RTCPeerConnection @@ -1455,7 +1455,7 @@ export class WebRTCEndpoint< }; private onIceGatheringStateChange = (_event: any) => { - switch (this.connection?.iceGatheringState) { + switch (this.stateManager.connection?.iceGatheringState) { case 'complete': this.processNextCommand(); break; @@ -1463,7 +1463,7 @@ export class WebRTCEndpoint< }; private onSignalingStateChange = (_event: any) => { - switch (this.connection?.signalingState) { + switch (this.stateManager.connection?.signalingState) { case 'stable': this.processNextCommand(); break; @@ -1475,19 +1475,24 @@ export class WebRTCEndpoint< const [stream] = event.streams; const mid = event.transceiver.mid!; - const trackId = this.midToTrackId.get(mid)!; + const trackId = this.stateManager.midToTrackId.get(mid)!; - if (this.checkIfTrackBelongToEndpoint(trackId, this.localEndpoint)) + if ( + this.checkIfTrackBelongToEndpoint( + trackId, + this.stateManager.localEndpoint, + ) + ) return; if (!isTrackKind(event.track.kind)) throw new Error('Track has no kind'); - const trackContext = this.trackIdToTrack.get(trackId)!; + const trackContext = this.stateManager.trackIdToTrack.get(trackId)!; trackContext.stream = stream; trackContext.track = event.track; trackContext.trackKind = event.track.kind; - this.idToEndpoint + this.stateManager.idToEndpoint .get(trackContext.endpoint.id) ?.tracks.set(trackId, trackContext); @@ -1503,30 +1508,35 @@ export class WebRTCEndpoint< endpoint.tracks = new Map(Object.entries(endpoint.tracks)); else endpoint.tracks = new Map(); - this.idToEndpoint.set(endpoint.id, endpoint); + this.stateManager.idToEndpoint.set(endpoint.id, endpoint); }; private eraseEndpoint = ( endpoint: EndpointWithTrackContext, ): void => { const tracksId = Array.from(endpoint.tracks.keys()); - tracksId.forEach((trackId) => this.trackIdToTrack.delete(trackId)); - Array.from(this.midToTrackId.entries()).forEach(([mid, trackId]) => { - if (tracksId.includes(trackId)) this.midToTrackId.delete(mid); - }); - this.idToEndpoint.delete(endpoint.id); + tracksId.forEach((trackId) => + this.stateManager.trackIdToTrack.delete(trackId), + ); + Array.from(this.stateManager.midToTrackId.entries()).forEach( + ([mid, trackId]) => { + if (tracksId.includes(trackId)) + this.stateManager.midToTrackId.delete(mid); + }, + ); + this.stateManager.idToEndpoint.delete(endpoint.id); }; private eraseTrack = (trackId: string, endpointId: string) => { - this.trackIdToTrack.delete(trackId); - const midToTrackId = Array.from(this.midToTrackId.entries()); + this.stateManager.trackIdToTrack.delete(trackId); + const midToTrackId = Array.from(this.stateManager.midToTrackId.entries()); const [mid, _trackId] = midToTrackId.find( ([_mid, mapTrackId]) => mapTrackId === trackId, )!; - this.midToTrackId.delete(mid); - this.idToEndpoint.get(endpointId)!.tracks.delete(trackId); - this.disabledTrackEncodings.delete(trackId); + this.stateManager.midToTrackId.delete(mid); + this.stateManager.idToEndpoint.get(endpointId)!.tracks.delete(trackId); + this.stateManager.disabledTrackEncodings.delete(trackId); }; - private getEndpointId = () => this.localEndpoint.id; + private getEndpointId = () => this.stateManager.localEndpoint.id; } diff --git a/packages/ts-client/tests/events/connectedEvent.test.ts b/packages/ts-client/tests/events/connectedEvent.test.ts index f5161b5b..31c0685f 100644 --- a/packages/ts-client/tests/events/connectedEvent.test.ts +++ b/packages/ts-client/tests/events/connectedEvent.test.ts @@ -35,7 +35,7 @@ it('Connecting to empty room set internal state', () => () => { webRTCEndpoint.receiveMediaEvent(JSON.stringify(connectedEvent)); // Then - const localEndpoint = webRTCEndpoint['localEndpoint']; + const localEndpoint = webRTCEndpoint['stateManager']['localEndpoint']; expect(localEndpoint.id).toBe(connectedEvent.data.id); }); diff --git a/packages/ts-client/tests/events/trackAddedEvent.test.ts b/packages/ts-client/tests/events/trackAddedEvent.test.ts index b237e929..75f90610 100644 --- a/packages/ts-client/tests/events/trackAddedEvent.test.ts +++ b/packages/ts-client/tests/events/trackAddedEvent.test.ts @@ -138,7 +138,7 @@ it('tracksAdded -> handle offerData with one video track from server', () => webRTCEndpoint.receiveMediaEvent(JSON.stringify(offerData)); // Then - const rtcConfig = webRTCEndpoint['rtcConfig']; + const rtcConfig = webRTCEndpoint['stateManager']['rtcConfig']; expect(rtcConfig.iceServers?.length).toBe(1); @@ -148,7 +148,8 @@ it('tracksAdded -> handle offerData with one video track from server', () => expect(addTransceiverCallback.mock.calls).toHaveLength(1); expect(addTransceiverCallback.mock.calls[0][0]).toBe('video'); - const transceivers = webRTCEndpoint['connection']?.getTransceivers(); + const transceivers = + webRTCEndpoint['stateManager']['connection']?.getTransceivers(); expect(transceivers?.length).toBe(1); expect(transceivers?.[0].direction).toBe('recvonly'); @@ -181,7 +182,7 @@ it('tracksAdded -> offerData with one track -> handle sdpAnswer data with one vi webRTCEndpoint.receiveMediaEvent(JSON.stringify(answerData)); // Then - const midToTrackId = webRTCEndpoint['midToTrackId']; + const midToTrackId = webRTCEndpoint['stateManager']['midToTrackId']; expect(midToTrackId.size).toBe(1); }); diff --git a/packages/ts-client/tests/methods/addTrackMethod.test.ts b/packages/ts-client/tests/methods/addTrackMethod.test.ts index 6e21e37d..7fbbe3f3 100644 --- a/packages/ts-client/tests/methods/addTrackMethod.test.ts +++ b/packages/ts-client/tests/methods/addTrackMethod.test.ts @@ -46,10 +46,11 @@ it('Adding track updates internal state', () => { webRTCEndpoint.addTrack(mockTrack); // Then - const localTrackIdToTrack = webRTCEndpoint['localTrackIdToTrack']; + const localTrackIdToTrack = + webRTCEndpoint['stateManager']['localTrackIdToTrack']; expect(localTrackIdToTrack.size).toBe(1); - const localEndpoint = webRTCEndpoint['localEndpoint']; + const localEndpoint = webRTCEndpoint['stateManager']['localEndpoint']; expect(localEndpoint.tracks.size).toBe(1); }); diff --git a/packages/ts-client/tests/methods/cleanUpMethod.test.ts b/packages/ts-client/tests/methods/cleanUpMethod.test.ts index a76ba05d..9dff66ec 100644 --- a/packages/ts-client/tests/methods/cleanUpMethod.test.ts +++ b/packages/ts-client/tests/methods/cleanUpMethod.test.ts @@ -13,6 +13,6 @@ it('CleanUp sets connection to undefined', async () => { webRTCEndpoint.cleanUp(); // Then - const connection = webRTCEndpoint['connection']; + const connection = webRTCEndpoint['stateManager']['connection']; expect(connection).toBe(undefined); }); diff --git a/packages/ts-client/tests/methods/connectMethod.test.ts b/packages/ts-client/tests/methods/connectMethod.test.ts index 6e73cb6f..aa8d3525 100644 --- a/packages/ts-client/tests/methods/connectMethod.test.ts +++ b/packages/ts-client/tests/methods/connectMethod.test.ts @@ -48,7 +48,9 @@ it("Method 'connect' sets metadata in local field", () => { webRTCEndpoint.connect(peerMetadata); // Then - expect(webRTCEndpoint['localEndpoint'].metadata).toMatchObject(peerMetadata); + expect( + webRTCEndpoint['stateManager']['localEndpoint'].metadata, + ).toMatchObject(peerMetadata); }); it("Method 'connect' throws when metadata is incorrect", () => { diff --git a/packages/ts-client/tests/methods/disconnectMethod.test.ts b/packages/ts-client/tests/methods/disconnectMethod.test.ts index 08be19bc..8f0c325a 100644 --- a/packages/ts-client/tests/methods/disconnectMethod.test.ts +++ b/packages/ts-client/tests/methods/disconnectMethod.test.ts @@ -14,7 +14,7 @@ it('Disconnect sets connection to undefined', async () => { webRTCEndpoint.disconnect(); // Then - const connection = webRTCEndpoint['connection']; + const connection = webRTCEndpoint['stateManager']['connection']; expect(connection).toBe(undefined); }); diff --git a/packages/ts-client/tests/utils.ts b/packages/ts-client/tests/utils.ts index c86f2d10..1dba269b 100644 --- a/packages/ts-client/tests/utils.ts +++ b/packages/ts-client/tests/utils.ts @@ -63,7 +63,7 @@ export const setupRoomWithMocks = async ( JSON.stringify(createAddLocalTrackAnswerData(trackId)), ); - const connection = webRTCEndpoint['connection']!; + const connection = webRTCEndpoint['stateManager']['connection']!; const transciever = new RTCRtpTransceiver(); // @ts-ignore diff --git a/yarn.lock b/yarn.lock index 3d30a393..68daa09f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -576,6 +576,7 @@ __metadata: react-dom: "npm:^18.2.0" testcontainers: "npm:^10.3.2" ts-proto: "npm:^1.176.0" + typescript: "npm:^5.5.0" vite: "npm:^5.1.2" vitest: "npm:^1.6.0" languageName: unknown From ee09df50de897a975004d407454504bec27aa6a9 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 18:32:55 +0200 Subject: [PATCH 23/50] Extract `onAnswer`, `disableTrackEncoding`, `onTrack`, `checkIfTrackBelongToEndpoint` to `StateManager.ts` --- packages/ts-client/src/webrtc/StateManager.ts | 100 +++++++++++++++++- .../ts-client/src/webrtc/webRTCEndpoint.ts | 80 +------------- 2 files changed, 103 insertions(+), 77 deletions(-) diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index 4d04b049..53cb3303 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -1,6 +1,9 @@ import type { LocalTrackId, RemoteTrackId, TrackEncoding } from './types'; -import type { EndpointWithTrackContext } from './internal'; -import type { TrackContextImpl } from './internal'; +import { isTrackKind } from './internal'; +import type { TrackContextImpl, EndpointWithTrackContext } from './internal'; +import { findSenderByTrack } from "./RTCPeerConnectionUtils"; +import { generateMediaEvent } from "./mediaEvent"; +import type { WebRTCEndpoint } from "./webRTCEndpoint"; export class StateManager { public trackIdToTrack: Map< @@ -49,4 +52,97 @@ export class StateManager { */ public ongoingRenegotiation: boolean = false; public ongoingTrackReplacement: boolean = false; + + private readonly webrtc: WebRTCEndpoint + + constructor( + webrtc: WebRTCEndpoint + ) { + this.webrtc = webrtc + } + + public onAnswer = async (answer: RTCSessionDescriptionInit) => { + this.connection!.ontrack = this.onTrack(); + try { + await this.connection!.setRemoteDescription(answer); + this.disabledTrackEncodings.forEach( + (encodings: TrackEncoding[], trackId: string) => { + encodings.forEach((encoding: TrackEncoding) => + this.disableTrackEncoding(trackId, encoding), + ); + }, + ); + } catch (err) { + console.error(err); + } + }; + + /** + * Disables track encoding so that it will be no longer sent to the server. + * @param {string} trackId - id of track + * @param {TrackEncoding} encoding - encoding that will be disabled + * @example + * ```ts + * const trackId = webrtc.addTrack(track, stream, {}, {enabled: true, activeEncodings: ["l", "m", "h"]}); + * webrtc.disableTrackEncoding(trackId, "l"); + * ``` + */ + public disableTrackEncoding(trackId: string, encoding: TrackEncoding) { + const track = this.localTrackIdToTrack.get(trackId)?.track; + this.disabledTrackEncodings.get(trackId)!.push(encoding); + + const sender = findSenderByTrack(this.connection, track); + + const params = sender?.getParameters(); + params!.encodings.filter((en) => en.rid == encoding)[0].active = false; + sender?.setParameters(params!); + + const mediaEvent = generateMediaEvent('disableTrackEncoding', { + trackId: trackId, + encoding: encoding, + }); + this.webrtc.sendMediaEvent(mediaEvent); + this.webrtc.emit('localTrackEncodingDisabled', { + trackId, + encoding, + }); + } + + private onTrack = () => { + return (event: RTCTrackEvent) => { + const [stream] = event.streams; + const mid = event.transceiver.mid!; + + const trackId = this.midToTrackId.get(mid)!; + + if ( + this.checkIfTrackBelongToEndpoint( + trackId, + this.localEndpoint, + ) + ) + return; + if (!isTrackKind(event.track.kind)) throw new Error('Track has no kind'); + + const trackContext = this.trackIdToTrack.get(trackId)!; + + trackContext.stream = stream; + trackContext.track = event.track; + trackContext.trackKind = event.track.kind; + + this.idToEndpoint + .get(trackContext.endpoint.id) + ?.tracks.set(trackId, trackContext); + + this.webrtc.emit('trackReady', trackContext); + }; + }; + + private checkIfTrackBelongToEndpoint = ( + trackId: string, + endpoint: EndpointWithTrackContext, + ) => + Array.from(endpoint.tracks.keys()).some((track) => + trackId.startsWith(track), + ); } diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 2d5b3274..fa47cd7b 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -72,7 +72,7 @@ export class WebRTCEndpoint< this.trackMetadataParser = config?.trackMetadataParser ?? ((x) => x as TrackMetadata); - this.stateManager = new StateManager(); + this.stateManager = new StateManager(this); } /** @@ -324,7 +324,7 @@ export class WebRTCEndpoint< } } - this.onAnswer(deserializedMediaEvent.data); + this.stateManager.onAnswer(deserializedMediaEvent.data); this.stateManager.ongoingRenegotiation = false; this.processNextCommand(); @@ -1137,24 +1137,7 @@ export class WebRTCEndpoint< * ``` */ public disableTrackEncoding(trackId: string, encoding: TrackEncoding) { - const track = this.stateManager.localTrackIdToTrack.get(trackId)?.track; - this.stateManager.disabledTrackEncodings.get(trackId)!.push(encoding); - - const sender = findSenderByTrack(this.stateManager.connection, track); - - const params = sender?.getParameters(); - params!.encodings.filter((en) => en.rid == encoding)[0].active = false; - sender?.setParameters(params!); - - const mediaEvent = generateMediaEvent('disableTrackEncoding', { - trackId: trackId, - encoding: encoding, - }); - this.sendMediaEvent(mediaEvent); - this.emit('localTrackEncodingDisabled', { - trackId, - encoding, - }); + this.stateManager.disableTrackEncoding(trackId, encoding) } /** @@ -1274,27 +1257,12 @@ export class WebRTCEndpoint< return `${this.getEndpointId()}:${uuid}`; } - private sendMediaEvent = (mediaEvent: MediaEvent) => { + // todo change to private + public sendMediaEvent = (mediaEvent: MediaEvent) => { const serializedMediaEvent = serializeMediaEvent(mediaEvent); this.emit('sendMediaEvent', serializedMediaEvent); }; - private onAnswer = async (answer: RTCSessionDescriptionInit) => { - this.stateManager.connection!.ontrack = this.onTrack(); - try { - await this.stateManager.connection!.setRemoteDescription(answer); - this.stateManager.disabledTrackEncodings.forEach( - (encodings: TrackEncoding[], trackId: string) => { - encodings.forEach((encoding: TrackEncoding) => - this.disableTrackEncoding(trackId, encoding), - ); - }, - ); - } catch (err) { - console.error(err); - } - }; - private async createAndSendOffer() { const connection = this.stateManager.connection; if (!connection) return; @@ -1330,14 +1298,6 @@ export class WebRTCEndpoint< } } - private checkIfTrackBelongToEndpoint = ( - trackId: string, - endpoint: EndpointWithTrackContext, - ) => - Array.from(endpoint.tracks.keys()).some((track) => - trackId.startsWith(track), - ); - private onOfferData = async (offerData: MediaEvent) => { if (!this.stateManager.connection) { const turnServers = offerData.data.integratedTurnServers; @@ -1470,36 +1430,6 @@ export class WebRTCEndpoint< } }; - private onTrack = () => { - return (event: RTCTrackEvent) => { - const [stream] = event.streams; - const mid = event.transceiver.mid!; - - const trackId = this.stateManager.midToTrackId.get(mid)!; - - if ( - this.checkIfTrackBelongToEndpoint( - trackId, - this.stateManager.localEndpoint, - ) - ) - return; - if (!isTrackKind(event.track.kind)) throw new Error('Track has no kind'); - - const trackContext = this.stateManager.trackIdToTrack.get(trackId)!; - - trackContext.stream = stream; - trackContext.track = event.track; - trackContext.trackKind = event.track.kind; - - this.stateManager.idToEndpoint - .get(trackContext.endpoint.id) - ?.tracks.set(trackId, trackContext); - - this.emit('trackReady', trackContext); - }; - }; - private addEndpoint = ( endpoint: EndpointWithTrackContext, ): void => { From 02e5f47b32eed1582ac78279960919dd5e22e2a8 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 18:49:11 +0200 Subject: [PATCH 24/50] Extract `onTracksAdded` to `StateManager.ts` --- packages/ts-client/src/webrtc/StateManager.ts | 38 +++++++++++++++++-- .../ts-client/src/webrtc/webRTCEndpoint.ts | 25 +----------- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index 53cb3303..6d32ae08 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -1,5 +1,5 @@ -import type { LocalTrackId, RemoteTrackId, TrackEncoding } from './types'; -import { isTrackKind } from './internal'; +import type { LocalTrackId, MetadataParser, RemoteTrackId, TrackEncoding } from './types'; +import { isTrackKind, mapMediaEventTracksToTrackContextImpl } from './internal'; import type { TrackContextImpl, EndpointWithTrackContext } from './internal'; import { findSenderByTrack } from "./RTCPeerConnectionUtils"; import { generateMediaEvent } from "./mediaEvent"; @@ -53,12 +53,16 @@ export class StateManager { public ongoingRenegotiation: boolean = false; public ongoingTrackReplacement: boolean = false; + // temporary for webrtc.emit and webrtc.sendMediaEvent private readonly webrtc: WebRTCEndpoint + private readonly endpointMetadataParser: MetadataParser; + private readonly trackMetadataParser: MetadataParser; constructor( - webrtc: WebRTCEndpoint - ) { + webrtc: WebRTCEndpoint, endpointMetadataParser: MetadataParser, trackMetadataParser: MetadataParser) { this.webrtc = webrtc + this.endpointMetadataParser = endpointMetadataParser; + this.trackMetadataParser = trackMetadataParser; } public onAnswer = async (answer: RTCSessionDescriptionInit) => { @@ -145,4 +149,30 @@ export class StateManager { Array.from(endpoint.tracks.keys()).some((track) => trackId.startsWith(track), ); + + public onTracksAdded(data: any) { + if (this.getEndpointId() === data.endpointId) return; + data.tracks = new Map(Object.entries(data.tracks)); + const endpoint: EndpointWithTrackContext = this.idToEndpoint.get(data.endpointId)!; + const oldTracks = endpoint.tracks; + + data.tracks = mapMediaEventTracksToTrackContextImpl( + data.tracks, + endpoint, + this.trackMetadataParser, + ); + + endpoint.tracks = new Map([...endpoint.tracks, ...data.tracks]); + + this.idToEndpoint.set(endpoint.id, endpoint); + Array.from(endpoint.tracks.entries()).forEach(([trackId, ctx]) => { + if (!oldTracks.has(trackId)) { + this.trackIdToTrack.set(trackId, ctx); + + this.webrtc.emit('trackAdded', ctx); + } + }); + } + + private getEndpointId = () => this.localEndpoint.id; } diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index fa47cd7b..83d99ab1 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -72,7 +72,7 @@ export class WebRTCEndpoint< this.trackMetadataParser = config?.trackMetadataParser ?? ((x) => x as TrackMetadata); - this.stateManager = new StateManager(this); + this.stateManager = new StateManager(this, this.endpointMetadataParser, this.trackMetadataParser); } /** @@ -256,28 +256,7 @@ export class WebRTCEndpoint< case 'tracksAdded': { this.stateManager.ongoingRenegotiation = true; - data = deserializedMediaEvent.data; - if (this.getEndpointId() === data.endpointId) return; - data.tracks = new Map(Object.entries(data.tracks)); - endpoint = this.stateManager.idToEndpoint.get(data.endpointId)!; - const oldTracks = endpoint.tracks; - - data.tracks = mapMediaEventTracksToTrackContextImpl( - data.tracks, - endpoint, - this.trackMetadataParser, - ); - - endpoint.tracks = new Map([...endpoint.tracks, ...data.tracks]); - - this.stateManager.idToEndpoint.set(endpoint.id, endpoint); - Array.from(endpoint.tracks.entries()).forEach(([trackId, ctx]) => { - if (!oldTracks.has(trackId)) { - this.stateManager.trackIdToTrack.set(trackId, ctx); - - this.emit('trackAdded', ctx); - } - }); + this.stateManager.onTracksAdded(deserializedMediaEvent.data) break; } case 'tracksRemoved': { From 007ae8a15d95b88680037375a4dd451a79c9c9cf Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 18:52:42 +0200 Subject: [PATCH 25/50] Extract `onTracksRemoved` and `eraseTrack` to `StateManager.ts` --- packages/ts-client/src/webrtc/StateManager.ts | 24 +++++++++++++++++++ .../ts-client/src/webrtc/webRTCEndpoint.ts | 24 +------------------ 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index 6d32ae08..dcb6f832 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -174,5 +174,29 @@ export class StateManager { }); } + public onTracksRemoved(data: any) { + const endpointId = data.endpointId; + if (this.getEndpointId() === endpointId) return; + const trackIds = data.trackIds as string[]; + trackIds.forEach((trackId) => { + const trackContext = this.trackIdToTrack.get(trackId)!; + + this.eraseTrack(trackId, endpointId); + + this.webrtc.emit('trackRemoved', trackContext); + }); + } + + private eraseTrack = (trackId: string, endpointId: string) => { + this.trackIdToTrack.delete(trackId); + const midToTrackId = Array.from(this.midToTrackId.entries()); + const [mid, _trackId] = midToTrackId.find( + ([_mid, mapTrackId]) => mapTrackId === trackId, + )!; + this.midToTrackId.delete(mid); + this.idToEndpoint.get(endpointId)!.tracks.delete(trackId); + this.disabledTrackEncodings.delete(trackId); + }; + private getEndpointId = () => this.localEndpoint.id; } diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 83d99ab1..d6094605 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -247,7 +247,6 @@ export class WebRTCEndpoint< private handleMediaEvent = (deserializedMediaEvent: MediaEvent) => { let endpoint: EndpointWithTrackContext; - let data; switch (deserializedMediaEvent.type) { case 'offerData': { this.onOfferData(deserializedMediaEvent); @@ -262,17 +261,7 @@ export class WebRTCEndpoint< case 'tracksRemoved': { this.stateManager.ongoingRenegotiation = true; - data = deserializedMediaEvent.data; - const endpointId = data.endpointId; - if (this.getEndpointId() === endpointId) return; - const trackIds = data.trackIds as string[]; - trackIds.forEach((trackId) => { - const trackContext = this.stateManager.trackIdToTrack.get(trackId)!; - - this.eraseTrack(trackId, endpointId); - - this.emit('trackRemoved', trackContext); - }); + this.stateManager.onTracksRemoved(deserializedMediaEvent.data) break; } @@ -1436,16 +1425,5 @@ export class WebRTCEndpoint< this.stateManager.idToEndpoint.delete(endpoint.id); }; - private eraseTrack = (trackId: string, endpointId: string) => { - this.stateManager.trackIdToTrack.delete(trackId); - const midToTrackId = Array.from(this.stateManager.midToTrackId.entries()); - const [mid, _trackId] = midToTrackId.find( - ([_mid, mapTrackId]) => mapTrackId === trackId, - )!; - this.stateManager.midToTrackId.delete(mid); - this.stateManager.idToEndpoint.get(endpointId)!.tracks.delete(trackId); - this.stateManager.disabledTrackEncodings.delete(trackId); - }; - private getEndpointId = () => this.stateManager.localEndpoint.id; } From 7476c29de4c8a5537c7fcf1ba5f0ea98d99a32c8 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 18:58:10 +0200 Subject: [PATCH 26/50] Extract `onSdpAnswer` to `StateManager.ts` --- packages/ts-client/src/webrtc/StateManager.ts | 27 +++++++++++++++++- .../ts-client/src/webrtc/webRTCEndpoint.ts | 28 +------------------ 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index dcb6f832..7ec9c533 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -65,7 +65,7 @@ export class StateManager { this.trackMetadataParser = trackMetadataParser; } - public onAnswer = async (answer: RTCSessionDescriptionInit) => { + private onAnswer = async (answer: RTCSessionDescriptionInit) => { this.connection!.ontrack = this.onTrack(); try { await this.connection!.setRemoteDescription(answer); @@ -187,6 +187,31 @@ export class StateManager { }); } + public onSdpAnswer(data: any) { + this.midToTrackId = new Map(Object.entries(data.midToTrackId),); + + for (const trackId of Object.values(data.midToTrackId,)) { + const track = this.localTrackIdToTrack.get(trackId as string); + + // if is local track + if (track) { + track.negotiationStatus = 'done'; + + if (track.pendingMetadataUpdate) { + const mediaEvent = generateMediaEvent('updateTrackMetadata', { + trackId, + trackMetadata: track.metadata, + }); + this.webrtc.sendMediaEvent(mediaEvent); + } + + track.pendingMetadataUpdate = false; + } + } + + this.onAnswer(data); + } + private eraseTrack = (trackId: string, endpointId: string) => { this.trackIdToTrack.delete(trackId); const midToTrackId = Array.from(this.midToTrackId.entries()); diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index d6094605..1bf54ead 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -266,33 +266,7 @@ export class WebRTCEndpoint< } case 'sdpAnswer': - this.stateManager.midToTrackId = new Map( - Object.entries(deserializedMediaEvent.data.midToTrackId), - ); - - for (const trackId of Object.values( - deserializedMediaEvent.data.midToTrackId, - )) { - const track = this.stateManager.localTrackIdToTrack.get( - trackId as string, - ); - // if is local track - if (track) { - track.negotiationStatus = 'done'; - - if (track.pendingMetadataUpdate) { - const mediaEvent = generateMediaEvent('updateTrackMetadata', { - trackId, - trackMetadata: track.metadata, - }); - this.sendMediaEvent(mediaEvent); - } - - track.pendingMetadataUpdate = false; - } - } - - this.stateManager.onAnswer(deserializedMediaEvent.data); + this.stateManager.onSdpAnswer(deserializedMediaEvent.data) this.stateManager.ongoingRenegotiation = false; this.processNextCommand(); From 2c556b064334b9ea71c4d61adb41bd757e510727 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 19:04:57 +0200 Subject: [PATCH 27/50] Extract `onEndpointAdded`, `onEndpointUpdated` and `addEndpoint` to `StateManager.ts` --- packages/ts-client/src/webrtc/StateManager.ts | 45 +++++++++++++++++++ .../ts-client/src/webrtc/webRTCEndpoint.ts | 43 +----------------- 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index 7ec9c533..73b59fa8 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -212,6 +212,51 @@ export class StateManager { this.onAnswer(data); } + public onEndpointAdded(endpoint: EndpointWithTrackContext) { + if (endpoint.id === this.getEndpointId()) return; + endpoint.rawMetadata = endpoint.metadata; + try { + endpoint.metadataParsingError = undefined; + endpoint.metadata = this.endpointMetadataParser(endpoint.rawMetadata); + } catch (error) { + endpoint.metadataParsingError = error; + endpoint.metadata = undefined; + } + this.addEndpoint(endpoint); + + this.webrtc.emit('endpointAdded', endpoint); + } + + public onEndpointUpdated(data: any) { + if (this.getEndpointId() === data.id) return; + const endpoint: EndpointWithTrackContext = this.idToEndpoint.get(data.id)!; + + try { + endpoint.metadata = this.endpointMetadataParser( + data.metadata, + ); + endpoint.metadataParsingError = undefined; + } catch (error) { + endpoint.metadata = undefined; + endpoint.metadataParsingError = error; + } + endpoint.rawMetadata = data.metadata; + this.addEndpoint(endpoint); + + this.webrtc.emit('endpointUpdated', endpoint); + } + + private addEndpoint = ( + endpoint: EndpointWithTrackContext, + ): void => { + // #TODO remove this line after fixing deserialization + if (Object.prototype.hasOwnProperty.call(endpoint, 'trackIdToMetadata')) + endpoint.tracks = new Map(Object.entries(endpoint.tracks)); + else endpoint.tracks = new Map(); + + this.idToEndpoint.set(endpoint.id, endpoint); + }; + private eraseTrack = (trackId: string, endpointId: string) => { this.trackIdToTrack.delete(trackId); const midToTrackId = Array.from(this.midToTrackId.entries()); diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 1bf54ead..aadbb390 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -277,19 +277,7 @@ export class WebRTCEndpoint< break; case 'endpointAdded': - endpoint = deserializedMediaEvent.data; - if (endpoint.id === this.getEndpointId()) return; - endpoint.rawMetadata = endpoint.metadata; - try { - endpoint.metadataParsingError = undefined; - endpoint.metadata = this.endpointMetadataParser(endpoint.rawMetadata); - } catch (error) { - endpoint.metadataParsingError = error; - endpoint.metadata = undefined; - } - this.addEndpoint(endpoint); - - this.emit('endpointAdded', endpoint); + this.stateManager.onEndpointAdded(deserializedMediaEvent.data) break; case 'endpointRemoved': @@ -319,23 +307,7 @@ export class WebRTCEndpoint< break; case 'endpointUpdated': - if (this.getEndpointId() === deserializedMediaEvent.data.id) return; - endpoint = this.stateManager.idToEndpoint.get( - deserializedMediaEvent.data.id, - )!; - try { - endpoint.metadata = this.endpointMetadataParser( - deserializedMediaEvent.data.metadata, - ); - endpoint.metadataParsingError = undefined; - } catch (error) { - endpoint.metadata = undefined; - endpoint.metadataParsingError = error; - } - endpoint.rawMetadata = deserializedMediaEvent.data.metadata; - this.addEndpoint(endpoint); - - this.emit('endpointUpdated', endpoint); + this.stateManager.onEndpointUpdated(deserializedMediaEvent.data) break; case 'trackUpdated': { @@ -1372,17 +1344,6 @@ export class WebRTCEndpoint< } }; - private addEndpoint = ( - endpoint: EndpointWithTrackContext, - ): void => { - // #TODO remove this line after fixing deserialization - if (Object.prototype.hasOwnProperty.call(endpoint, 'trackIdToMetadata')) - endpoint.tracks = new Map(Object.entries(endpoint.tracks)); - else endpoint.tracks = new Map(); - - this.stateManager.idToEndpoint.set(endpoint.id, endpoint); - }; - private eraseEndpoint = ( endpoint: EndpointWithTrackContext, ): void => { From 214f2ddca9268c01d5bef91e20f49f377597c7f1 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 19:10:29 +0200 Subject: [PATCH 28/50] Extract `onEndpointRemoved` and `eraseEndpoint` to `StateManager.ts` --- packages/ts-client/src/webrtc/StateManager.ts | 32 +++++++++++++++++ .../ts-client/src/webrtc/webRTCEndpoint.ts | 36 ++----------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index 73b59fa8..3e8b7630 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -246,6 +246,22 @@ export class StateManager { this.webrtc.emit('endpointUpdated', endpoint); } + public onEndpointRemoved(data: any) { + const endpoint: EndpointWithTrackContext = this.idToEndpoint.get(data.id)!; + if (endpoint === undefined) return; + + Array.from(endpoint.tracks.keys()).forEach((trackId) => { + this.webrtc.emit( + 'trackRemoved', + this.trackIdToTrack.get(trackId)!, + ); + }); + + this.eraseEndpoint(endpoint); + + this.webrtc.emit('endpointRemoved', endpoint); + } + private addEndpoint = ( endpoint: EndpointWithTrackContext, ): void => { @@ -257,6 +273,22 @@ export class StateManager { this.idToEndpoint.set(endpoint.id, endpoint); }; + private eraseEndpoint = ( + endpoint: EndpointWithTrackContext, + ): void => { + const tracksId = Array.from(endpoint.tracks.keys()); + tracksId.forEach((trackId) => + this.trackIdToTrack.delete(trackId), + ); + Array.from(this.midToTrackId.entries()).forEach( + ([mid, trackId]) => { + if (tracksId.includes(trackId)) + this.midToTrackId.delete(mid); + }, + ); + this.idToEndpoint.delete(endpoint.id); + }; + private eraseTrack = (trackId: string, endpointId: string) => { this.trackIdToTrack.delete(trackId); const midToTrackId = Array.from(this.midToTrackId.entries()); diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index aadbb390..3a241da8 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -281,29 +281,13 @@ export class WebRTCEndpoint< break; case 'endpointRemoved': - if ( - deserializedMediaEvent.data.id === this.stateManager.localEndpoint.id - ) { + if (deserializedMediaEvent.data.id === this.stateManager.localEndpoint.id) { this.cleanUp(); this.emit('disconnected'); return; } - endpoint = this.stateManager.idToEndpoint.get( - deserializedMediaEvent.data.id, - )!; - if (endpoint === undefined) return; - - Array.from(endpoint.tracks.keys()).forEach((trackId) => { - this.emit( - 'trackRemoved', - this.stateManager.trackIdToTrack.get(trackId)!, - ); - }); - - this.eraseEndpoint(endpoint); - - this.emit('endpointRemoved', endpoint); + this.stateManager.onEndpointRemoved(deserializedMediaEvent.data) break; case 'endpointUpdated': @@ -1344,21 +1328,5 @@ export class WebRTCEndpoint< } }; - private eraseEndpoint = ( - endpoint: EndpointWithTrackContext, - ): void => { - const tracksId = Array.from(endpoint.tracks.keys()); - tracksId.forEach((trackId) => - this.stateManager.trackIdToTrack.delete(trackId), - ); - Array.from(this.stateManager.midToTrackId.entries()).forEach( - ([mid, trackId]) => { - if (tracksId.includes(trackId)) - this.stateManager.midToTrackId.delete(mid); - }, - ); - this.stateManager.idToEndpoint.delete(endpoint.id); - }; - private getEndpointId = () => this.stateManager.localEndpoint.id; } From a201025142bf3fb453e0a3d231a394bc2db16c31 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 19:18:53 +0200 Subject: [PATCH 29/50] Extract `onTrackUpdated` to `StateManager.ts` --- packages/ts-client/src/webrtc/StateManager.ts | 39 +++++++++++++++++++ .../ts-client/src/webrtc/webRTCEndpoint.ts | 37 +----------------- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index 3e8b7630..64369066 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -262,6 +262,45 @@ export class StateManager { this.webrtc.emit('endpointRemoved', endpoint); } + public onTrackUpdated(data: any) { + if (this.getEndpointId() === data.endpointId) + return; + + const endpoint: EndpointWithTrackContext = this.idToEndpoint.get(data.endpointId)!; + + if (endpoint == null) + throw `Endpoint with id: ${data.endpointId} doesn't exist`; + + const trackId = data.trackId; + const trackMetadata = data.metadata; + let newTrack = endpoint.tracks.get(trackId)!; + const trackContext = this.trackIdToTrack.get(trackId)!; + + try { + const parsedMetadata = this.trackMetadataParser(trackMetadata); + newTrack = { + ...newTrack, + metadata: parsedMetadata, + metadataParsingError: undefined, + }; + trackContext.metadata = parsedMetadata; + trackContext.metadataParsingError = undefined; + } catch (error) { + newTrack = { + ...newTrack, + metadata: undefined, + metadataParsingError: error, + }; + trackContext.metadataParsingError = error; + trackContext.metadata = undefined; + } + newTrack = { ...newTrack, rawMetadata: trackMetadata }; + trackContext.rawMetadata = trackMetadata; + endpoint.tracks.set(trackId, newTrack); + + this.webrtc.emit('trackUpdated', trackContext); + } + private addEndpoint = ( endpoint: EndpointWithTrackContext, ): void => { diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 3a241da8..909901d0 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -295,42 +295,7 @@ export class WebRTCEndpoint< break; case 'trackUpdated': { - if (this.getEndpointId() === deserializedMediaEvent.data.endpointId) - return; - - endpoint = this.stateManager.idToEndpoint.get( - deserializedMediaEvent.data.endpointId, - )!; - if (endpoint == null) - throw `Endpoint with id: ${deserializedMediaEvent.data.endpointId} doesn't exist`; - - const trackId = deserializedMediaEvent.data.trackId; - const trackMetadata = deserializedMediaEvent.data.metadata; - let newTrack = endpoint.tracks.get(trackId)!; - const trackContext = this.stateManager.trackIdToTrack.get(trackId)!; - try { - const parsedMetadata = this.trackMetadataParser(trackMetadata); - newTrack = { - ...newTrack, - metadata: parsedMetadata, - metadataParsingError: undefined, - }; - trackContext.metadata = parsedMetadata; - trackContext.metadataParsingError = undefined; - } catch (error) { - newTrack = { - ...newTrack, - metadata: undefined, - metadataParsingError: error, - }; - trackContext.metadataParsingError = error; - trackContext.metadata = undefined; - } - newTrack = { ...newTrack, rawMetadata: trackMetadata }; - trackContext.rawMetadata = trackMetadata; - endpoint.tracks.set(trackId, newTrack); - - this.emit('trackUpdated', trackContext); + this.stateManager.onTrackUpdated(deserializedMediaEvent.data) break; } From 0e9200044880c4d14ec596988613ff59b201bb82 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 19:21:43 +0200 Subject: [PATCH 30/50] Extract `onTrackEncodingDisabled` to `StateManager.ts` --- packages/ts-client/src/webrtc/StateManager.ts | 15 +++++++++++++++ packages/ts-client/src/webrtc/webRTCEndpoint.ts | 16 +--------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index 64369066..e6ca515d 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -301,6 +301,21 @@ export class StateManager { this.webrtc.emit('trackUpdated', trackContext); } + public onTrackEncodingDisabled(data: any) { + if (this.getEndpointId() === data.endpointId) return; + + const endpoint: EndpointWithTrackContext = this.idToEndpoint.get(data.endpointId)!; + + if (endpoint == null) throw `Endpoint with id: ${data.endpointId} doesn't exist`; + + const trackId = data.trackId; + const encoding = data.encoding; + + const trackContext = endpoint.tracks.get(trackId)!; + + this.webrtc.emit('trackEncodingDisabled', trackContext, encoding); + } + private addEndpoint = ( endpoint: EndpointWithTrackContext, ): void => { diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 909901d0..660ccb2e 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -300,21 +300,7 @@ export class WebRTCEndpoint< } case 'trackEncodingDisabled': { - if (this.getEndpointId() === deserializedMediaEvent.data.endpointId) - return; - - endpoint = this.stateManager.idToEndpoint.get( - deserializedMediaEvent.data.endpointId, - )!; - if (endpoint == null) - throw `Endpoint with id: ${deserializedMediaEvent.data.endpointId} doesn't exist`; - - const trackId = deserializedMediaEvent.data.trackId; - const encoding = deserializedMediaEvent.data.encoding; - - const trackContext = endpoint.tracks.get(trackId)!; - - this.emit('trackEncodingDisabled', trackContext, encoding); + this.stateManager.onTrackEncodingDisabled(deserializedMediaEvent.data) break; } From c0e6b5be15c817761c00dca1268d634ac04712a9 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 19:25:19 +0200 Subject: [PATCH 31/50] Extract `onTrackEncodingEnabled` to `StateManager.ts` --- packages/ts-client/src/webrtc/StateManager.ts | 15 +++++++++++++++ packages/ts-client/src/webrtc/webRTCEndpoint.ts | 17 ++--------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index e6ca515d..4ab11371 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -316,6 +316,21 @@ export class StateManager { this.webrtc.emit('trackEncodingDisabled', trackContext, encoding); } + public onTrackEncodingEnabled(data: any) { + if (this.getEndpointId() === data.endpointId) return; + + const endpoint: EndpointWithTrackContext = this.idToEndpoint.get(data.endpointId)!; + + if (endpoint == null) throw `Endpoint with id: ${data.endpointId} doesn't exist`; + + const trackId = data.trackId; + const encoding = data.encoding; + + const trackContext = endpoint.tracks.get(trackId)!; + + this.webrtc.emit('trackEncodingEnabled', trackContext, encoding); + } + private addEndpoint = ( endpoint: EndpointWithTrackContext, ): void => { diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 660ccb2e..b6dcb938 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -305,21 +305,7 @@ export class WebRTCEndpoint< } case 'trackEncodingEnabled': { - if (this.getEndpointId() === deserializedMediaEvent.data.endpointId) - return; - - endpoint = this.stateManager.idToEndpoint.get( - deserializedMediaEvent.data.endpointId, - )!; - if (endpoint == null) - throw `Endpoint with id: ${deserializedMediaEvent.data.endpointId} doesn't exist`; - - const trackId = deserializedMediaEvent.data.trackId; - const encoding = deserializedMediaEvent.data.encoding; - - const trackContext = endpoint.tracks.get(trackId)!; - - this.emit('trackEncodingEnabled', trackContext, encoding); + this.stateManager.onTrackEncodingEnabled(deserializedMediaEvent.data) break; } @@ -335,6 +321,7 @@ export class WebRTCEndpoint< this.emit('tracksPriorityChanged', enabledTracks, disabledTracks); break; } + case 'encodingSwitched': { const trackId = deserializedMediaEvent.data.trackId; const trackContext = this.stateManager.trackIdToTrack.get(trackId)!; From bc529487be753d7cb402abd7982bfc6776482bd4 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 19:27:19 +0200 Subject: [PATCH 32/50] Add comment to `tracksPriority` --- packages/ts-client/src/webrtc/webRTCEndpoint.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index b6dcb938..43d7c72c 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -309,6 +309,7 @@ export class WebRTCEndpoint< break; } + // todo The event may not be implemented in the Fishjam case 'tracksPriority': { const enabledTracks = ( deserializedMediaEvent.data.tracks as string[] From 203fc80822707396aa0d3c8a622066401c8a73f3 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 19:29:33 +0200 Subject: [PATCH 33/50] Extract `onEncodingSwitched` to `StateManager.ts` --- packages/ts-client/src/webrtc/StateManager.ts | 9 +++++++++ packages/ts-client/src/webrtc/webRTCEndpoint.ts | 7 +------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index 4ab11371..8ec37852 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -331,6 +331,15 @@ export class StateManager { this.webrtc.emit('trackEncodingEnabled', trackContext, encoding); } + public onEncodingSwitched(data: any) { + const trackId = data.trackId; + const trackContext = this.trackIdToTrack.get(trackId)!; + trackContext.encoding = data.encoding; + trackContext.encodingReason = data.reason; + + trackContext.emit('encodingChanged', trackContext); + } + private addEndpoint = ( endpoint: EndpointWithTrackContext, ): void => { diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 43d7c72c..12766249 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -324,12 +324,7 @@ export class WebRTCEndpoint< } case 'encodingSwitched': { - const trackId = deserializedMediaEvent.data.trackId; - const trackContext = this.stateManager.trackIdToTrack.get(trackId)!; - trackContext.encoding = deserializedMediaEvent.data.encoding; - trackContext.encodingReason = deserializedMediaEvent.data.reason; - - trackContext.emit('encodingChanged', trackContext); + this.stateManager.onEncodingSwitched(deserializedMediaEvent.data) break; } case 'custom': From ca5533fab0ed8dee18d90a07963164412eea19fd Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 19:35:52 +0200 Subject: [PATCH 34/50] Move `onVadNotification` to `StateManager.ts` --- packages/ts-client/src/webrtc/StateManager.ts | 13 +++++++++ .../src/webrtc/voiceActivityDetection.ts | 27 +------------------ .../ts-client/src/webrtc/webRTCEndpoint.ts | 9 ++----- 3 files changed, 16 insertions(+), 33 deletions(-) diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index 8ec37852..aeac364c 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -4,6 +4,7 @@ import type { TrackContextImpl, EndpointWithTrackContext } from './internal'; import { findSenderByTrack } from "./RTCPeerConnectionUtils"; import { generateMediaEvent } from "./mediaEvent"; import type { WebRTCEndpoint } from "./webRTCEndpoint"; +import { isVadStatus } from "./voiceActivityDetection"; export class StateManager { public trackIdToTrack: Map< @@ -340,6 +341,18 @@ export class StateManager { trackContext.emit('encodingChanged', trackContext); } + public onVadNotification(data: any) { + const trackId = data.trackId; + const ctx = this.trackIdToTrack.get(trackId)!; + const vadStatus = data.status; + if (isVadStatus(vadStatus)) { + ctx.vadStatus = vadStatus; + ctx.emit('voiceActivityChanged', ctx); + } else { + console.warn('Received unknown vad status: ', vadStatus); + } + }; + private addEndpoint = ( endpoint: EndpointWithTrackContext, ): void => { diff --git a/packages/ts-client/src/webrtc/voiceActivityDetection.ts b/packages/ts-client/src/webrtc/voiceActivityDetection.ts index e656dc8f..42be8d9f 100644 --- a/packages/ts-client/src/webrtc/voiceActivityDetection.ts +++ b/packages/ts-client/src/webrtc/voiceActivityDetection.ts @@ -1,30 +1,5 @@ -import type { MediaEvent } from './mediaEvent'; -import type { TrackContextImpl } from './internal'; import type { VadStatus } from './types'; const vadStatuses = ['speech', 'silence'] as const; -const isVadStatus = (status: string): status is VadStatus => { - return vadStatuses.includes(status as VadStatus); -}; - -export const handleVoiceActivationDetectionNotification = < - EndpointMetadata, - TrackMetadata, ->( - deserializedMediaEvent: MediaEvent, - trackIdToTrack: Map< - string, - TrackContextImpl - >, -) => { - const trackId = deserializedMediaEvent.data.trackId; - const ctx = trackIdToTrack.get(trackId)!; - const vadStatus = deserializedMediaEvent.data.status; - if (isVadStatus(vadStatus)) { - ctx.vadStatus = vadStatus; - ctx.emit('voiceActivityChanged', ctx); - } else { - console.warn('Received unknown vad status: ', vadStatus); - } -}; +export const isVadStatus = (status: string): status is VadStatus => vadStatuses.includes(status as VadStatus); diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 12766249..14d32037 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -28,7 +28,6 @@ import type { import type { EndpointWithTrackContext } from './internal'; import { mapMediaEventTracksToTrackContextImpl } from './internal'; import { TrackContextImpl, isTrackKind } from './internal'; -import { handleVoiceActivationDetectionNotification } from './voiceActivityDetection'; import { applyBandwidthLimitation } from './bandwidth'; import { createTrackVariantBitratesEvent, getTrackBitrates } from './bitrate'; import { @@ -53,7 +52,7 @@ export class WebRTCEndpoint< EndpointMetadata = any, TrackMetadata = any, > extends (EventEmitter as { - new (): TypedEmitter< + new(): TypedEmitter< Required> >; }) { @@ -246,7 +245,6 @@ export class WebRTCEndpoint< } private handleMediaEvent = (deserializedMediaEvent: MediaEvent) => { - let endpoint: EndpointWithTrackContext; switch (deserializedMediaEvent.type) { case 'offerData': { this.onOfferData(deserializedMediaEvent); @@ -340,10 +338,7 @@ export class WebRTCEndpoint< break; case 'vadNotification': { - handleVoiceActivationDetectionNotification( - deserializedMediaEvent, - this.stateManager.trackIdToTrack, - ); + this.stateManager.onVadNotification(deserializedMediaEvent.data); break; } From 96bc56912d7d7cac7bdff9c46ab6479ce8380364 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 19:38:37 +0200 Subject: [PATCH 35/50] Extract `onBandwidthEstimation` to `StateManager.ts` --- packages/ts-client/src/webrtc/StateManager.ts | 6 ++++++ packages/ts-client/src/webrtc/webRTCEndpoint.ts | 8 +------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index aeac364c..05ddc5b5 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -353,6 +353,12 @@ export class StateManager { } }; + public onBandwidthEstimation(data: any) { + this.bandwidthEstimation = data.estimation; + + this.webrtc.emit('bandwidthEstimationChanged', this.bandwidthEstimation); + } + private addEndpoint = ( endpoint: EndpointWithTrackContext, ): void => { diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 14d32037..a6db8e05 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -343,13 +343,7 @@ export class WebRTCEndpoint< } case 'bandwidthEstimation': { - this.stateManager.bandwidthEstimation = - deserializedMediaEvent.data.estimation; - - this.emit( - 'bandwidthEstimationChanged', - this.stateManager.bandwidthEstimation, - ); + this.stateManager.onBandwidthEstimation(deserializedMediaEvent.data); break; } From 2e1091ae72a7c68b85dfd4c6c3f9204795159200 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 20:08:13 +0200 Subject: [PATCH 36/50] Extract `NegotiationManager` to `StateManager.ts` --- .../ts-client/src/webrtc/NegotiationManager.ts | 7 +++++++ packages/ts-client/src/webrtc/StateManager.ts | 5 ----- packages/ts-client/src/webrtc/webRTCEndpoint.ts | 17 ++++++++++------- 3 files changed, 17 insertions(+), 12 deletions(-) create mode 100644 packages/ts-client/src/webrtc/NegotiationManager.ts diff --git a/packages/ts-client/src/webrtc/NegotiationManager.ts b/packages/ts-client/src/webrtc/NegotiationManager.ts new file mode 100644 index 00000000..1631323f --- /dev/null +++ b/packages/ts-client/src/webrtc/NegotiationManager.ts @@ -0,0 +1,7 @@ +export class NegotiationManager { + /** + * Indicates if an ongoing renegotiation is active. + * During renegotiation, both parties are expected to actively exchange events: renegotiateTracks, offerData, sdpOffer, sdpAnswer. + */ + public ongoingRenegotiation: boolean = false; +} diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index 05ddc5b5..788b13aa 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -47,11 +47,6 @@ export class StateManager { }; public bandwidthEstimation: bigint = BigInt(0); - /** - * Indicates if an ongoing renegotiation is active. - * During renegotiation, both parties are expected to actively exchange events: renegotiateTracks, offerData, sdpOffer, sdpAnswer. - */ - public ongoingRenegotiation: boolean = false; public ongoingTrackReplacement: boolean = false; // temporary for webrtc.emit and webrtc.sendMediaEvent diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index a6db8e05..399acd47 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -44,6 +44,7 @@ import { import { createSdpOfferEvent } from './sdpEvents'; import { setTurns } from './turn'; import { StateManager } from './StateManager'; +import { NegotiationManager } from "./NegotiationManager"; /** * Main class that is responsible for connecting to the RTC Engine, sending and receiving media. @@ -63,6 +64,7 @@ export class WebRTCEndpoint< private readonly trackMetadataParser: MetadataParser; private stateManager: StateManager; + private negotiationManager: NegotiationManager; constructor(config?: Config) { super(); @@ -72,6 +74,7 @@ export class WebRTCEndpoint< config?.trackMetadataParser ?? ((x) => x as TrackMetadata); this.stateManager = new StateManager(this, this.endpointMetadataParser, this.trackMetadataParser); + this.negotiationManager = new NegotiationManager(); } /** @@ -251,13 +254,13 @@ export class WebRTCEndpoint< break; } case 'tracksAdded': { - this.stateManager.ongoingRenegotiation = true; + this.negotiationManager.ongoingRenegotiation = true; this.stateManager.onTracksAdded(deserializedMediaEvent.data) break; } case 'tracksRemoved': { - this.stateManager.ongoingRenegotiation = true; + this.negotiationManager.ongoingRenegotiation = true; this.stateManager.onTracksRemoved(deserializedMediaEvent.data) break; @@ -266,7 +269,7 @@ export class WebRTCEndpoint< case 'sdpAnswer': this.stateManager.onSdpAnswer(deserializedMediaEvent.data) - this.stateManager.ongoingRenegotiation = false; + this.negotiationManager.ongoingRenegotiation = false; this.processNextCommand(); break; @@ -468,7 +471,7 @@ export class WebRTCEndpoint< private processNextCommand() { if ( - this.stateManager.ongoingRenegotiation || + this.negotiationManager.ongoingRenegotiation || this.stateManager.ongoingTrackReplacement ) return; @@ -528,7 +531,7 @@ export class WebRTCEndpoint< return; } - this.stateManager.ongoingRenegotiation = true; + this.negotiationManager.ongoingRenegotiation = true; const trackContext = new TrackContextImpl( this.stateManager.localEndpoint, @@ -862,7 +865,7 @@ export class WebRTCEndpoint< trackContext.track!.id, ); - this.stateManager.ongoingRenegotiation = true; + this.negotiationManager.ongoingRenegotiation = true; this.stateManager.connection!.removeTrack(sender); const mediaEvent = generateCustomEvent({ type: 'renegotiateTracks' }); @@ -1068,7 +1071,7 @@ export class WebRTCEndpoint< this.commandResolutionNotifier = null; this.commandsQueue = []; this.stateManager.ongoingTrackReplacement = false; - this.stateManager.ongoingRenegotiation = false; + this.negotiationManager.ongoingRenegotiation = false; } this.stateManager.connection = undefined; From 42cd9dd524f93fa88f8f8363fb4c1d0e7856f024 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 20:27:20 +0200 Subject: [PATCH 37/50] Extract `CommandsQueue` to the separate file --- .../ts-client/src/webrtc/CommandsQueue.ts | 229 +++++++++++++++++ packages/ts-client/src/webrtc/StateManager.ts | 2 +- .../ts-client/src/webrtc/webRTCEndpoint.ts | 237 ++---------------- 3 files changed, 246 insertions(+), 222 deletions(-) create mode 100644 packages/ts-client/src/webrtc/CommandsQueue.ts diff --git a/packages/ts-client/src/webrtc/CommandsQueue.ts b/packages/ts-client/src/webrtc/CommandsQueue.ts new file mode 100644 index 00000000..204ab99e --- /dev/null +++ b/packages/ts-client/src/webrtc/CommandsQueue.ts @@ -0,0 +1,229 @@ +import type { AddTrackCommand, Command, RemoveTrackCommand, ReplaceTackCommand } from "./commands"; +import type { Deferred } from "./deferred"; +import { findSender, isTrackInUse } from "./RTCPeerConnectionUtils"; +import { isTrackKind, TrackContextImpl } from "./internal"; +import { addTrackToConnection, setTransceiverDirection } from "./transciever"; +import { generateCustomEvent, generateMediaEvent } from "./mediaEvent"; +import type { StateManager } from "./StateManager"; +import type { NegotiationManager } from "./NegotiationManager"; +import type { WebRTCEndpoint } from "./webRTCEndpoint"; +import type { MetadataParser } from "./types"; + +export class CommandsQueue { + private readonly stateManager: StateManager; + private negotiationManager: NegotiationManager; + private webrtc: WebRTCEndpoint; + private readonly trackMetadataParser: MetadataParser; + + constructor( + webrtc: WebRTCEndpoint, + stateManager: StateManager, + negotiationManager: NegotiationManager, + trackMetadataParser: MetadataParser + ) { + this.webrtc = webrtc; + this.stateManager = stateManager; + this.negotiationManager = negotiationManager; + this.trackMetadataParser = trackMetadataParser; + } + + private commandsQueue: Command[] = []; + private commandResolutionNotifier: Deferred | null = null; + + public pushCommand(command: Command) { + this.commandsQueue.push(command); + this.processNextCommand(); + } + + public processNextCommand() { + if ( + this.negotiationManager.ongoingRenegotiation || + this.stateManager.ongoingTrackReplacement + ) + return; + + if ( + this.stateManager.connection && + (this.stateManager.connection.signalingState !== 'stable' || + this.stateManager.connection.connectionState !== 'connected' || + this.stateManager.connection.iceConnectionState !== 'connected') + ) + return; + + this.resolvePreviousCommand(); + + const command = this.commandsQueue.shift(); + + if (!command) return; + + this.commandResolutionNotifier = command.resolutionNotifier; + this.handleCommand(command); + } + + private handleCommand(command: Command) { + switch (command.commandType) { + case 'ADD-TRACK': + this.addTrackHandler(command); + break; + case 'REMOVE-TRACK': + this.removeTrackHandler(command); + break; + case 'REPLACE-TRACK': + this.replaceTrackHandler(command); + break; + } + } + + private addTrackHandler(addTrackCommand: AddTrackCommand) { + const { + simulcastConfig, + maxBandwidth, + track, + stream, + trackMetadata, + trackId, + } = addTrackCommand; + const isUsedTrack = isTrackInUse(this.stateManager.connection, track); + + let error; + if (isUsedTrack) { + error = + "This track was already added to peerConnection, it can't be added again!"; + } + + if (!simulcastConfig.enabled && !(typeof maxBandwidth === 'number')) + error = + 'Invalid type of `maxBandwidth` argument for a non-simulcast track, expected: number'; + if (this.stateManager.getEndpointId() === '') + error = 'Cannot add tracks before being accepted by the server'; + + if (error) { + this.commandResolutionNotifier?.reject(error); + this.commandResolutionNotifier = null; + this.processNextCommand(); + return; + } + + this.negotiationManager.ongoingRenegotiation = true; + + const trackContext = new TrackContextImpl( + this.stateManager.localEndpoint, + trackId, + trackMetadata, + simulcastConfig, + this.trackMetadataParser, + ); + + if (!isTrackKind(track.kind)) throw new Error('Track has no kind'); + + trackContext.track = track; + trackContext.stream = stream; + trackContext.maxBandwidth = maxBandwidth; + trackContext.trackKind = track.kind; + + this.stateManager.localEndpoint.tracks.set(trackId, trackContext); + + this.stateManager.localTrackIdToTrack.set(trackId, trackContext); + + if (this.stateManager.connection) { + addTrackToConnection( + trackContext, + this.stateManager.disabledTrackEncodings, + this.stateManager.connection, + ); + + setTransceiverDirection(this.stateManager.connection); + } + + this.stateManager.trackIdToSender.set(trackId, { + remoteTrackId: trackId, + localTrackId: track.id, + sender: null, + }); + const mediaEvent = generateCustomEvent({ type: 'renegotiateTracks' }); + this.webrtc.sendMediaEvent(mediaEvent); + } + + private removeTrackHandler(command: RemoveTrackCommand) { + const { trackId } = command; + const trackContext = this.stateManager.localTrackIdToTrack.get(trackId)!; + const sender = findSender( + this.stateManager.connection, + trackContext.track!.id, + ); + + this.negotiationManager.ongoingRenegotiation = true; + + this.stateManager.connection!.removeTrack(sender); + const mediaEvent = generateCustomEvent({ type: 'renegotiateTracks' }); + this.webrtc.sendMediaEvent(mediaEvent); + this.stateManager.localTrackIdToTrack.delete(trackId); + this.stateManager.localEndpoint.tracks.delete(trackId); + } + + private async replaceTrackHandler(command: ReplaceTackCommand) { + const { trackId, newTrack, newTrackMetadata } = command; + + // todo add validation to track.kind, you cannot replace video with audio + + const trackContext = this.stateManager.localTrackIdToTrack.get(trackId)!; + + const track = this.stateManager.trackIdToSender.get(trackId); + const sender = track?.sender ?? null; + + if (!track) throw Error(`There is no track with id: ${trackId}`); + if (!sender) throw Error('There is no RTCRtpSender for this track id!'); + + this.stateManager.ongoingTrackReplacement = true; + + trackContext.stream?.getTracks().forEach((track) => { + trackContext.stream?.removeTrack(track); + }); + + if (newTrack) { + trackContext.stream?.addTrack(newTrack); + } + + if (trackContext.track && !newTrack) { + const mediaEvent = generateMediaEvent('muteTrack', { trackId: trackId }); + this.webrtc.sendMediaEvent(mediaEvent); + this.webrtc.emit('localTrackMuted', { trackId: trackId }); + } else if (!trackContext.track && newTrack) { + const mediaEvent = generateMediaEvent('unmuteTrack', { + trackId: trackId, + }); + this.webrtc.sendMediaEvent(mediaEvent); + this.webrtc.emit('localTrackUnmuted', { trackId: trackId }); + } + + track.localTrackId = newTrack?.id ?? null; + + try { + await sender.replaceTrack(newTrack); + trackContext.track = newTrack; + + if (newTrackMetadata) { + this.webrtc.updateTrackMetadata(trackId, newTrackMetadata); + } + } catch (error) { + // ignore + } finally { + this.resolvePreviousCommand(); + this.stateManager.ongoingTrackReplacement = false; + this.processNextCommand(); + } + } + + private resolvePreviousCommand() { + if (this.commandResolutionNotifier) { + this.commandResolutionNotifier.resolve(); + this.commandResolutionNotifier = null; + } + } + + public clenUp() { + this.commandResolutionNotifier?.reject('Disconnected'); + this.commandResolutionNotifier = null; + this.commandsQueue = []; + } +} diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index 788b13aa..7d757b63 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -392,5 +392,5 @@ export class StateManager { this.disabledTrackEncodings.delete(trackId); }; - private getEndpointId = () => this.localEndpoint.id; + public getEndpointId = () => this.localEndpoint.id; } diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 399acd47..ab105624 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -8,12 +8,6 @@ import { import { v4 as uuidv4 } from 'uuid'; import EventEmitter from 'events'; import type TypedEmitter from 'typed-emitter'; -import type { - AddTrackCommand, - Command, - RemoveTrackCommand, - ReplaceTackCommand, -} from './commands'; import { Deferred } from './deferred'; import type { BandwidthLimit, @@ -27,24 +21,22 @@ import type { } from './types'; import type { EndpointWithTrackContext } from './internal'; import { mapMediaEventTracksToTrackContextImpl } from './internal'; -import { TrackContextImpl, isTrackKind } from './internal'; import { applyBandwidthLimitation } from './bandwidth'; import { createTrackVariantBitratesEvent, getTrackBitrates } from './bitrate'; import { findSender, findSenderByTrack, - isTrackInUse, } from './RTCPeerConnectionUtils'; import { addTrackToConnection, addTransceiversIfNeeded, - setTransceiverDirection, setTransceiversToReadOnly, } from './transciever'; import { createSdpOfferEvent } from './sdpEvents'; import { setTurns } from './turn'; import { StateManager } from './StateManager'; import { NegotiationManager } from "./NegotiationManager"; +import { CommandsQueue } from "./CommandsQueue"; /** * Main class that is responsible for connecting to the RTC Engine, sending and receiving media. @@ -57,14 +49,12 @@ export class WebRTCEndpoint< Required> >; }) { - private commandsQueue: Command[] = []; - private commandResolutionNotifier: Deferred | null = null; - private readonly endpointMetadataParser: MetadataParser; private readonly trackMetadataParser: MetadataParser; - private stateManager: StateManager; - private negotiationManager: NegotiationManager; + private readonly stateManager: StateManager; + private readonly negotiationManager: NegotiationManager; + private readonly commandsQueue: CommandsQueue; constructor(config?: Config) { super(); @@ -75,6 +65,7 @@ export class WebRTCEndpoint< this.stateManager = new StateManager(this, this.endpointMetadataParser, this.trackMetadataParser); this.negotiationManager = new NegotiationManager(); + this.commandsQueue = new CommandsQueue(this, this.stateManager, this.negotiationManager, this.trackMetadataParser) } /** @@ -270,7 +261,7 @@ export class WebRTCEndpoint< this.stateManager.onSdpAnswer(deserializedMediaEvent.data) this.negotiationManager.ongoingRenegotiation = false; - this.processNextCommand(); + this.commandsQueue.processNextCommand(); break; case 'candidate': @@ -423,7 +414,7 @@ export class WebRTCEndpoint< stream.addTrack(track); - this.pushCommand({ + this.commandsQueue.pushCommand({ commandType: 'ADD-TRACK', trackId, track, @@ -450,127 +441,6 @@ export class WebRTCEndpoint< }); } - private pushCommand(command: Command) { - this.commandsQueue.push(command); - this.processNextCommand(); - } - - private handleCommand(command: Command) { - switch (command.commandType) { - case 'ADD-TRACK': - this.addTrackHandler(command); - break; - case 'REMOVE-TRACK': - this.removeTrackHandler(command); - break; - case 'REPLACE-TRACK': - this.replaceTrackHandler(command); - break; - } - } - - private processNextCommand() { - if ( - this.negotiationManager.ongoingRenegotiation || - this.stateManager.ongoingTrackReplacement - ) - return; - - if ( - this.stateManager.connection && - (this.stateManager.connection.signalingState !== 'stable' || - this.stateManager.connection.connectionState !== 'connected' || - this.stateManager.connection.iceConnectionState !== 'connected') - ) - return; - - this.resolvePreviousCommand(); - - const command = this.commandsQueue.shift(); - - if (!command) return; - - this.commandResolutionNotifier = command.resolutionNotifier; - this.handleCommand(command); - } - - private resolvePreviousCommand() { - if (this.commandResolutionNotifier) { - this.commandResolutionNotifier.resolve(); - this.commandResolutionNotifier = null; - } - } - - private addTrackHandler(addTrackCommand: AddTrackCommand) { - const { - simulcastConfig, - maxBandwidth, - track, - stream, - trackMetadata, - trackId, - } = addTrackCommand; - const isUsedTrack = isTrackInUse(this.stateManager.connection, track); - - let error; - if (isUsedTrack) { - error = - "This track was already added to peerConnection, it can't be added again!"; - } - - if (!simulcastConfig.enabled && !(typeof maxBandwidth === 'number')) - error = - 'Invalid type of `maxBandwidth` argument for a non-simulcast track, expected: number'; - if (this.getEndpointId() === '') - error = 'Cannot add tracks before being accepted by the server'; - - if (error) { - this.commandResolutionNotifier?.reject(error); - this.commandResolutionNotifier = null; - this.processNextCommand(); - return; - } - - this.negotiationManager.ongoingRenegotiation = true; - - const trackContext = new TrackContextImpl( - this.stateManager.localEndpoint, - trackId, - trackMetadata, - simulcastConfig, - this.trackMetadataParser, - ); - - if (!isTrackKind(track.kind)) throw new Error('Track has no kind'); - - trackContext.track = track; - trackContext.stream = stream; - trackContext.maxBandwidth = maxBandwidth; - trackContext.trackKind = track.kind; - - this.stateManager.localEndpoint.tracks.set(trackId, trackContext); - - this.stateManager.localTrackIdToTrack.set(trackId, trackContext); - - if (this.stateManager.connection) { - addTrackToConnection( - trackContext, - this.stateManager.disabledTrackEncodings, - this.stateManager.connection, - ); - - setTransceiverDirection(this.stateManager.connection); - } - - this.stateManager.trackIdToSender.set(trackId, { - remoteTrackId: trackId, - localTrackId: track.id, - sender: null, - }); - const mediaEvent = generateCustomEvent({ type: 'renegotiateTracks' }); - this.sendMediaEvent(mediaEvent); - } - /** * Replaces a track that is being sent to the RTC Engine. * @param trackId - Audio or video track. @@ -630,7 +500,7 @@ export class WebRTCEndpoint< ? this.trackMetadataParser(newTrackMetadata) : undefined; - this.pushCommand({ + this.commandsQueue.pushCommand({ commandType: 'REPLACE-TRACK', trackId, newTrack, @@ -649,61 +519,6 @@ export class WebRTCEndpoint< }); } - private async replaceTrackHandler( - command: ReplaceTackCommand, - ) { - const { trackId, newTrack, newTrackMetadata } = command; - - // todo add validation to track.kind, you cannot replace video with audio - - const trackContext = this.stateManager.localTrackIdToTrack.get(trackId)!; - - const track = this.stateManager.trackIdToSender.get(trackId); - const sender = track?.sender ?? null; - - if (!track) throw Error(`There is no track with id: ${trackId}`); - if (!sender) throw Error('There is no RTCRtpSender for this track id!'); - - this.stateManager.ongoingTrackReplacement = true; - - trackContext.stream?.getTracks().forEach((track) => { - trackContext.stream?.removeTrack(track); - }); - - if (newTrack) { - trackContext.stream?.addTrack(newTrack); - } - - if (trackContext.track && !newTrack) { - const mediaEvent = generateMediaEvent('muteTrack', { trackId: trackId }); - this.sendMediaEvent(mediaEvent); - this.emit('localTrackMuted', { trackId: trackId }); - } else if (!trackContext.track && newTrack) { - const mediaEvent = generateMediaEvent('unmuteTrack', { - trackId: trackId, - }); - this.sendMediaEvent(mediaEvent); - this.emit('localTrackUnmuted', { trackId: trackId }); - } - - track.localTrackId = newTrack?.id ?? null; - - try { - await sender.replaceTrack(newTrack); - trackContext.track = newTrack; - - if (newTrackMetadata) { - this.updateTrackMetadata(trackId, newTrackMetadata); - } - } catch (error) { - // ignore - } finally { - this.resolvePreviousCommand(); - this.stateManager.ongoingTrackReplacement = false; - this.processNextCommand(); - } - } - /** * Updates maximum bandwidth for the track identified by trackId. * This value directly translates to quality of the stream and, in case of video, to the amount of RTP packets being sent. @@ -845,7 +660,7 @@ export class WebRTCEndpoint< */ public removeTrack(trackId: string): Promise { const resolutionNotifier = new Deferred(); - this.pushCommand({ + this.commandsQueue.pushCommand({ commandType: 'REMOVE-TRACK', trackId, resolutionNotifier, @@ -857,23 +672,6 @@ export class WebRTCEndpoint< }); } - private removeTrackHandler(command: RemoveTrackCommand) { - const { trackId } = command; - const trackContext = this.stateManager.localTrackIdToTrack.get(trackId)!; - const sender = findSender( - this.stateManager.connection, - trackContext.track!.id, - ); - - this.negotiationManager.ongoingRenegotiation = true; - - this.stateManager.connection!.removeTrack(sender); - const mediaEvent = generateCustomEvent({ type: 'renegotiateTracks' }); - this.sendMediaEvent(mediaEvent); - this.stateManager.localTrackIdToTrack.delete(trackId); - this.stateManager.localEndpoint.tracks.delete(trackId); - } - /** * Sets track variant that server should send to the client library. * @@ -1067,9 +865,8 @@ export class WebRTCEndpoint< this.stateManager.connection.oniceconnectionstatechange = null; this.stateManager.connection.close(); - this.commandResolutionNotifier?.reject('Disconnected'); - this.commandResolutionNotifier = null; - this.commandsQueue = []; + this.commandsQueue.clenUp() + this.stateManager.ongoingTrackReplacement = false; this.negotiationManager.ongoingRenegotiation = false; } @@ -1078,7 +875,7 @@ export class WebRTCEndpoint< }; private getTrackId(uuid: string): string { - return `${this.getEndpointId()}:${uuid}`; + return `${this.stateManager.getEndpointId()}:${uuid}`; } // todo change to private @@ -1207,7 +1004,7 @@ export class WebRTCEndpoint< private onConnectionStateChange = (event: Event) => { switch (this.stateManager.connection?.connectionState) { case 'connected': - this.processNextCommand(); + this.commandsQueue.processNextCommand(); break; case 'failed': this.emit('connectionError', { @@ -1233,7 +1030,7 @@ export class WebRTCEndpoint< }); break; case 'connected': - this.processNextCommand(); + this.commandsQueue.processNextCommand(); break; } }; @@ -1241,7 +1038,7 @@ export class WebRTCEndpoint< private onIceGatheringStateChange = (_event: any) => { switch (this.stateManager.connection?.iceGatheringState) { case 'complete': - this.processNextCommand(); + this.commandsQueue.processNextCommand(); break; } }; @@ -1249,10 +1046,8 @@ export class WebRTCEndpoint< private onSignalingStateChange = (_event: any) => { switch (this.stateManager.connection?.signalingState) { case 'stable': - this.processNextCommand(); + this.commandsQueue.processNextCommand(); break; } }; - - private getEndpointId = () => this.stateManager.localEndpoint.id; } From 96db59038dc9508fe9d9112e3b9b63f70dd6d740 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 20:56:56 +0200 Subject: [PATCH 38/50] Move CommandsQueue.ts listener logic to CommandsQueue --- .../ts-client/src/webrtc/CommandsQueue.ts | 48 ++++++++++ .../ts-client/src/webrtc/webRTCEndpoint.ts | 89 ++++++++----------- 2 files changed, 83 insertions(+), 54 deletions(-) diff --git a/packages/ts-client/src/webrtc/CommandsQueue.ts b/packages/ts-client/src/webrtc/CommandsQueue.ts index 204ab99e..4289718b 100644 --- a/packages/ts-client/src/webrtc/CommandsQueue.ts +++ b/packages/ts-client/src/webrtc/CommandsQueue.ts @@ -15,6 +15,8 @@ export class CommandsQueue { private webrtc: WebRTCEndpoint; private readonly trackMetadataParser: MetadataParser; + private clearConnectionCallbacks: (() => void) | null = null + constructor( webrtc: WebRTCEndpoint, stateManager: StateManager, @@ -27,6 +29,51 @@ export class CommandsQueue { this.trackMetadataParser = trackMetadataParser; } + public setupEventListeners(connection: RTCPeerConnection) { + const onSignalingStateChange = () => { + switch (this.stateManager.connection?.signalingState) { + case 'stable': + this.processNextCommand(); + break; + } + } + + const onIceGatheringStateChange = () => { + switch (this.stateManager.connection?.iceGatheringState) { + case 'complete': + this.processNextCommand(); + break; + } + } + + const onConnectionStateChange = () => { + switch (connection.connectionState) { + case 'connected': + this.processNextCommand(); + break; + } + } + const onIceConnectionStateChange = () => { + switch (this.stateManager.connection?.iceConnectionState) { + case 'connected': + this.processNextCommand(); + break; + } + } + + this.clearConnectionCallbacks = () => { + connection.removeEventListener("signalingstatechange", onSignalingStateChange) + connection.removeEventListener("icegatheringstatechange", onIceGatheringStateChange) + connection.removeEventListener("connectionstatechange", onConnectionStateChange) + connection.removeEventListener("iceconnectionstatechange", onIceConnectionStateChange) + } + + connection.addEventListener("icegatheringstatechange", onIceConnectionStateChange) + connection.addEventListener("connectionstatechange", onConnectionStateChange); + connection.addEventListener("iceconnectionstatechange", onIceConnectionStateChange) + connection.addEventListener("signalingstatechange", onSignalingStateChange ) + } + private commandsQueue: Command[] = []; private commandResolutionNotifier: Deferred | null = null; @@ -225,5 +272,6 @@ export class CommandsQueue { this.commandResolutionNotifier?.reject('Disconnected'); this.commandResolutionNotifier = null; this.commandsQueue = []; + this.clearConnectionCallbacks?.() } } diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index ab105624..60695117 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -56,6 +56,8 @@ export class WebRTCEndpoint< private readonly negotiationManager: NegotiationManager; private readonly commandsQueue: CommandsQueue; + private clearConnectionCallbacks: (() => void) | null = null + constructor(config?: Config) { super(); this.endpointMetadataParser = @@ -858,11 +860,7 @@ export class WebRTCEndpoint< */ public cleanUp = () => { if (this.stateManager.connection) { - this.stateManager.connection.onicecandidate = null; - this.stateManager.connection.ontrack = null; - this.stateManager.connection.onconnectionstatechange = null; - this.stateManager.connection.onicecandidateerror = null; - this.stateManager.connection.oniceconnectionstatechange = null; + this.clearConnectionCallbacks?.() this.stateManager.connection.close(); this.commandsQueue.clenUp() @@ -924,20 +922,26 @@ export class WebRTCEndpoint< const turnServers = offerData.data.integratedTurnServers; setTurns(turnServers, this.stateManager.rtcConfig); - this.stateManager.connection = new RTCPeerConnection( - this.stateManager.rtcConfig, - ); - this.stateManager.connection.onicecandidate = this.onLocalCandidate(); - this.stateManager.connection.onicecandidateerror = this - .onIceCandidateError as (event: Event) => void; - this.stateManager.connection.onconnectionstatechange = - this.onConnectionStateChange; - this.stateManager.connection.oniceconnectionstatechange = - this.onIceConnectionStateChange; - this.stateManager.connection.onicegatheringstatechange = - this.onIceGatheringStateChange; - this.stateManager.connection.onsignalingstatechange = - this.onSignalingStateChange; + this.stateManager.connection = new RTCPeerConnection(this.stateManager.rtcConfig); + + const onIceCandidate = (event: RTCPeerConnectionIceEvent) => this.onLocalCandidate(event) + const onIceCandidateError = (event: RTCPeerConnectionIceErrorEvent) => this.onIceCandidateError(event) + const onConnectionStateChange = (event: Event) => this.onConnectionStateChange(event) + const onIceConnectionStateChange = (event: Event) => this.onIceConnectionStateChange(event) + + this.clearConnectionCallbacks = () => { + this.stateManager.connection?.removeEventListener("icecandidate", onIceCandidate) + this.stateManager.connection?.removeEventListener("icecandidateerror", onIceCandidateError) + this.stateManager.connection?.removeEventListener("connectionstatechange", onConnectionStateChange) + this.stateManager.connection?.removeEventListener("iceconnectionstatechange", onIceConnectionStateChange) + } + + this.stateManager.connection.addEventListener("icecandidate", onIceCandidate) + this.stateManager.connection.addEventListener("icecandidateerror", onIceCandidateError) + this.stateManager.connection.addEventListener("connectionstatechange", onConnectionStateChange) + this.stateManager.connection.addEventListener("iceconnectionstatechange", onIceConnectionStateChange) + + this.commandsQueue.setupEventListeners(this.stateManager.connection) Array.from(this.stateManager.localTrackIdToTrack.values()).forEach( (trackContext) => @@ -982,30 +986,26 @@ export class WebRTCEndpoint< } }; - private onLocalCandidate = () => { - return (event: RTCPeerConnectionIceEvent) => { - if (event.candidate) { - const mediaEvent = generateCustomEvent({ - type: 'candidate', - data: { - candidate: event.candidate.candidate, - sdpMLineIndex: event.candidate.sdpMLineIndex, - }, - }); - this.sendMediaEvent(mediaEvent); - } - }; + private onLocalCandidate = (event: RTCPeerConnectionIceEvent) => { + if (event.candidate) { + const mediaEvent = generateCustomEvent({ + type: 'candidate', + data: { + candidate: event.candidate.candidate, + sdpMLineIndex: event.candidate.sdpMLineIndex, + }, + }); + this.sendMediaEvent(mediaEvent); + } }; + private onIceCandidateError = (event: RTCPeerConnectionIceErrorEvent) => { console.warn(event); }; private onConnectionStateChange = (event: Event) => { switch (this.stateManager.connection?.connectionState) { - case 'connected': - this.commandsQueue.processNextCommand(); - break; case 'failed': this.emit('connectionError', { message: 'RTCPeerConnection failed', @@ -1029,25 +1029,6 @@ export class WebRTCEndpoint< event, }); break; - case 'connected': - this.commandsQueue.processNextCommand(); - break; - } - }; - - private onIceGatheringStateChange = (_event: any) => { - switch (this.stateManager.connection?.iceGatheringState) { - case 'complete': - this.commandsQueue.processNextCommand(); - break; - } - }; - - private onSignalingStateChange = (_event: any) => { - switch (this.stateManager.connection?.signalingState) { - case 'stable': - this.commandsQueue.processNextCommand(); - break; } }; } From aa413959434b49f7465b55244fbf7b392fdf7a0e Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 20:58:09 +0200 Subject: [PATCH 39/50] Fix format --- .../ts-client/src/webrtc/CommandsQueue.ts | 82 ++++++++---- packages/ts-client/src/webrtc/StateManager.ts | 90 ++++++------- .../src/webrtc/voiceActivityDetection.ts | 3 +- .../ts-client/src/webrtc/webRTCEndpoint.ts | 122 ++++++++++++------ 4 files changed, 184 insertions(+), 113 deletions(-) diff --git a/packages/ts-client/src/webrtc/CommandsQueue.ts b/packages/ts-client/src/webrtc/CommandsQueue.ts index 4289718b..dc318218 100644 --- a/packages/ts-client/src/webrtc/CommandsQueue.ts +++ b/packages/ts-client/src/webrtc/CommandsQueue.ts @@ -1,13 +1,18 @@ -import type { AddTrackCommand, Command, RemoveTrackCommand, ReplaceTackCommand } from "./commands"; -import type { Deferred } from "./deferred"; -import { findSender, isTrackInUse } from "./RTCPeerConnectionUtils"; -import { isTrackKind, TrackContextImpl } from "./internal"; -import { addTrackToConnection, setTransceiverDirection } from "./transciever"; -import { generateCustomEvent, generateMediaEvent } from "./mediaEvent"; -import type { StateManager } from "./StateManager"; -import type { NegotiationManager } from "./NegotiationManager"; -import type { WebRTCEndpoint } from "./webRTCEndpoint"; -import type { MetadataParser } from "./types"; +import type { + AddTrackCommand, + Command, + RemoveTrackCommand, + ReplaceTackCommand, +} from './commands'; +import type { Deferred } from './deferred'; +import { findSender, isTrackInUse } from './RTCPeerConnectionUtils'; +import { isTrackKind, TrackContextImpl } from './internal'; +import { addTrackToConnection, setTransceiverDirection } from './transciever'; +import { generateCustomEvent, generateMediaEvent } from './mediaEvent'; +import type { StateManager } from './StateManager'; +import type { NegotiationManager } from './NegotiationManager'; +import type { WebRTCEndpoint } from './webRTCEndpoint'; +import type { MetadataParser } from './types'; export class CommandsQueue { private readonly stateManager: StateManager; @@ -15,13 +20,13 @@ export class CommandsQueue { private webrtc: WebRTCEndpoint; private readonly trackMetadataParser: MetadataParser; - private clearConnectionCallbacks: (() => void) | null = null + private clearConnectionCallbacks: (() => void) | null = null; constructor( webrtc: WebRTCEndpoint, stateManager: StateManager, negotiationManager: NegotiationManager, - trackMetadataParser: MetadataParser + trackMetadataParser: MetadataParser, ) { this.webrtc = webrtc; this.stateManager = stateManager; @@ -36,7 +41,7 @@ export class CommandsQueue { this.processNextCommand(); break; } - } + }; const onIceGatheringStateChange = () => { switch (this.stateManager.connection?.iceGatheringState) { @@ -44,7 +49,7 @@ export class CommandsQueue { this.processNextCommand(); break; } - } + }; const onConnectionStateChange = () => { switch (connection.connectionState) { @@ -52,26 +57,47 @@ export class CommandsQueue { this.processNextCommand(); break; } - } + }; const onIceConnectionStateChange = () => { switch (this.stateManager.connection?.iceConnectionState) { case 'connected': this.processNextCommand(); break; } - } + }; this.clearConnectionCallbacks = () => { - connection.removeEventListener("signalingstatechange", onSignalingStateChange) - connection.removeEventListener("icegatheringstatechange", onIceGatheringStateChange) - connection.removeEventListener("connectionstatechange", onConnectionStateChange) - connection.removeEventListener("iceconnectionstatechange", onIceConnectionStateChange) - } + connection.removeEventListener( + 'signalingstatechange', + onSignalingStateChange, + ); + connection.removeEventListener( + 'icegatheringstatechange', + onIceGatheringStateChange, + ); + connection.removeEventListener( + 'connectionstatechange', + onConnectionStateChange, + ); + connection.removeEventListener( + 'iceconnectionstatechange', + onIceConnectionStateChange, + ); + }; - connection.addEventListener("icegatheringstatechange", onIceConnectionStateChange) - connection.addEventListener("connectionstatechange", onConnectionStateChange); - connection.addEventListener("iceconnectionstatechange", onIceConnectionStateChange) - connection.addEventListener("signalingstatechange", onSignalingStateChange ) + connection.addEventListener( + 'icegatheringstatechange', + onIceConnectionStateChange, + ); + connection.addEventListener( + 'connectionstatechange', + onConnectionStateChange, + ); + connection.addEventListener( + 'iceconnectionstatechange', + onIceConnectionStateChange, + ); + connection.addEventListener('signalingstatechange', onSignalingStateChange); } private commandsQueue: Command[] = []; @@ -208,7 +234,9 @@ export class CommandsQueue { this.stateManager.localEndpoint.tracks.delete(trackId); } - private async replaceTrackHandler(command: ReplaceTackCommand) { + private async replaceTrackHandler( + command: ReplaceTackCommand, + ) { const { trackId, newTrack, newTrackMetadata } = command; // todo add validation to track.kind, you cannot replace video with audio @@ -272,6 +300,6 @@ export class CommandsQueue { this.commandResolutionNotifier?.reject('Disconnected'); this.commandResolutionNotifier = null; this.commandsQueue = []; - this.clearConnectionCallbacks?.() + this.clearConnectionCallbacks?.(); } } diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index 7d757b63..f565af62 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -1,10 +1,15 @@ -import type { LocalTrackId, MetadataParser, RemoteTrackId, TrackEncoding } from './types'; +import type { + LocalTrackId, + MetadataParser, + RemoteTrackId, + TrackEncoding, +} from './types'; import { isTrackKind, mapMediaEventTracksToTrackContextImpl } from './internal'; import type { TrackContextImpl, EndpointWithTrackContext } from './internal'; -import { findSenderByTrack } from "./RTCPeerConnectionUtils"; -import { generateMediaEvent } from "./mediaEvent"; -import type { WebRTCEndpoint } from "./webRTCEndpoint"; -import { isVadStatus } from "./voiceActivityDetection"; +import { findSenderByTrack } from './RTCPeerConnectionUtils'; +import { generateMediaEvent } from './mediaEvent'; +import type { WebRTCEndpoint } from './webRTCEndpoint'; +import { isVadStatus } from './voiceActivityDetection'; export class StateManager { public trackIdToTrack: Map< @@ -50,13 +55,16 @@ export class StateManager { public ongoingTrackReplacement: boolean = false; // temporary for webrtc.emit and webrtc.sendMediaEvent - private readonly webrtc: WebRTCEndpoint + private readonly webrtc: WebRTCEndpoint; private readonly endpointMetadataParser: MetadataParser; private readonly trackMetadataParser: MetadataParser; constructor( - webrtc: WebRTCEndpoint, endpointMetadataParser: MetadataParser, trackMetadataParser: MetadataParser) { - this.webrtc = webrtc + webrtc: WebRTCEndpoint, + endpointMetadataParser: MetadataParser, + trackMetadataParser: MetadataParser, + ) { + this.webrtc = webrtc; this.endpointMetadataParser = endpointMetadataParser; this.trackMetadataParser = trackMetadataParser; } @@ -115,12 +123,7 @@ export class StateManager { const trackId = this.midToTrackId.get(mid)!; - if ( - this.checkIfTrackBelongToEndpoint( - trackId, - this.localEndpoint, - ) - ) + if (this.checkIfTrackBelongToEndpoint(trackId, this.localEndpoint)) return; if (!isTrackKind(event.track.kind)) throw new Error('Track has no kind'); @@ -149,7 +152,8 @@ export class StateManager { public onTracksAdded(data: any) { if (this.getEndpointId() === data.endpointId) return; data.tracks = new Map(Object.entries(data.tracks)); - const endpoint: EndpointWithTrackContext = this.idToEndpoint.get(data.endpointId)!; + const endpoint: EndpointWithTrackContext = + this.idToEndpoint.get(data.endpointId)!; const oldTracks = endpoint.tracks; data.tracks = mapMediaEventTracksToTrackContextImpl( @@ -184,9 +188,9 @@ export class StateManager { } public onSdpAnswer(data: any) { - this.midToTrackId = new Map(Object.entries(data.midToTrackId),); + this.midToTrackId = new Map(Object.entries(data.midToTrackId)); - for (const trackId of Object.values(data.midToTrackId,)) { + for (const trackId of Object.values(data.midToTrackId)) { const track = this.localTrackIdToTrack.get(trackId as string); // if is local track @@ -208,7 +212,9 @@ export class StateManager { this.onAnswer(data); } - public onEndpointAdded(endpoint: EndpointWithTrackContext) { + public onEndpointAdded( + endpoint: EndpointWithTrackContext, + ) { if (endpoint.id === this.getEndpointId()) return; endpoint.rawMetadata = endpoint.metadata; try { @@ -225,12 +231,11 @@ export class StateManager { public onEndpointUpdated(data: any) { if (this.getEndpointId() === data.id) return; - const endpoint: EndpointWithTrackContext = this.idToEndpoint.get(data.id)!; + const endpoint: EndpointWithTrackContext = + this.idToEndpoint.get(data.id)!; try { - endpoint.metadata = this.endpointMetadataParser( - data.metadata, - ); + endpoint.metadata = this.endpointMetadataParser(data.metadata); endpoint.metadataParsingError = undefined; } catch (error) { endpoint.metadata = undefined; @@ -243,14 +248,12 @@ export class StateManager { } public onEndpointRemoved(data: any) { - const endpoint: EndpointWithTrackContext = this.idToEndpoint.get(data.id)!; + const endpoint: EndpointWithTrackContext = + this.idToEndpoint.get(data.id)!; if (endpoint === undefined) return; Array.from(endpoint.tracks.keys()).forEach((trackId) => { - this.webrtc.emit( - 'trackRemoved', - this.trackIdToTrack.get(trackId)!, - ); + this.webrtc.emit('trackRemoved', this.trackIdToTrack.get(trackId)!); }); this.eraseEndpoint(endpoint); @@ -259,10 +262,10 @@ export class StateManager { } public onTrackUpdated(data: any) { - if (this.getEndpointId() === data.endpointId) - return; + if (this.getEndpointId() === data.endpointId) return; - const endpoint: EndpointWithTrackContext = this.idToEndpoint.get(data.endpointId)!; + const endpoint: EndpointWithTrackContext = + this.idToEndpoint.get(data.endpointId)!; if (endpoint == null) throw `Endpoint with id: ${data.endpointId} doesn't exist`; @@ -300,9 +303,11 @@ export class StateManager { public onTrackEncodingDisabled(data: any) { if (this.getEndpointId() === data.endpointId) return; - const endpoint: EndpointWithTrackContext = this.idToEndpoint.get(data.endpointId)!; + const endpoint: EndpointWithTrackContext = + this.idToEndpoint.get(data.endpointId)!; - if (endpoint == null) throw `Endpoint with id: ${data.endpointId} doesn't exist`; + if (endpoint == null) + throw `Endpoint with id: ${data.endpointId} doesn't exist`; const trackId = data.trackId; const encoding = data.encoding; @@ -315,9 +320,11 @@ export class StateManager { public onTrackEncodingEnabled(data: any) { if (this.getEndpointId() === data.endpointId) return; - const endpoint: EndpointWithTrackContext = this.idToEndpoint.get(data.endpointId)!; + const endpoint: EndpointWithTrackContext = + this.idToEndpoint.get(data.endpointId)!; - if (endpoint == null) throw `Endpoint with id: ${data.endpointId} doesn't exist`; + if (endpoint == null) + throw `Endpoint with id: ${data.endpointId} doesn't exist`; const trackId = data.trackId; const encoding = data.encoding; @@ -346,7 +353,7 @@ export class StateManager { } else { console.warn('Received unknown vad status: ', vadStatus); } - }; + } public onBandwidthEstimation(data: any) { this.bandwidthEstimation = data.estimation; @@ -369,15 +376,10 @@ export class StateManager { endpoint: EndpointWithTrackContext, ): void => { const tracksId = Array.from(endpoint.tracks.keys()); - tracksId.forEach((trackId) => - this.trackIdToTrack.delete(trackId), - ); - Array.from(this.midToTrackId.entries()).forEach( - ([mid, trackId]) => { - if (tracksId.includes(trackId)) - this.midToTrackId.delete(mid); - }, - ); + tracksId.forEach((trackId) => this.trackIdToTrack.delete(trackId)); + Array.from(this.midToTrackId.entries()).forEach(([mid, trackId]) => { + if (tracksId.includes(trackId)) this.midToTrackId.delete(mid); + }); this.idToEndpoint.delete(endpoint.id); }; diff --git a/packages/ts-client/src/webrtc/voiceActivityDetection.ts b/packages/ts-client/src/webrtc/voiceActivityDetection.ts index 42be8d9f..1a94ca96 100644 --- a/packages/ts-client/src/webrtc/voiceActivityDetection.ts +++ b/packages/ts-client/src/webrtc/voiceActivityDetection.ts @@ -2,4 +2,5 @@ import type { VadStatus } from './types'; const vadStatuses = ['speech', 'silence'] as const; -export const isVadStatus = (status: string): status is VadStatus => vadStatuses.includes(status as VadStatus); +export const isVadStatus = (status: string): status is VadStatus => + vadStatuses.includes(status as VadStatus); diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 60695117..9e683f64 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -23,10 +23,7 @@ import type { EndpointWithTrackContext } from './internal'; import { mapMediaEventTracksToTrackContextImpl } from './internal'; import { applyBandwidthLimitation } from './bandwidth'; import { createTrackVariantBitratesEvent, getTrackBitrates } from './bitrate'; -import { - findSender, - findSenderByTrack, -} from './RTCPeerConnectionUtils'; +import { findSender, findSenderByTrack } from './RTCPeerConnectionUtils'; import { addTrackToConnection, addTransceiversIfNeeded, @@ -35,8 +32,8 @@ import { import { createSdpOfferEvent } from './sdpEvents'; import { setTurns } from './turn'; import { StateManager } from './StateManager'; -import { NegotiationManager } from "./NegotiationManager"; -import { CommandsQueue } from "./CommandsQueue"; +import { NegotiationManager } from './NegotiationManager'; +import { CommandsQueue } from './CommandsQueue'; /** * Main class that is responsible for connecting to the RTC Engine, sending and receiving media. @@ -45,7 +42,7 @@ export class WebRTCEndpoint< EndpointMetadata = any, TrackMetadata = any, > extends (EventEmitter as { - new(): TypedEmitter< + new (): TypedEmitter< Required> >; }) { @@ -54,9 +51,12 @@ export class WebRTCEndpoint< private readonly stateManager: StateManager; private readonly negotiationManager: NegotiationManager; - private readonly commandsQueue: CommandsQueue; + private readonly commandsQueue: CommandsQueue< + EndpointMetadata, + TrackMetadata + >; - private clearConnectionCallbacks: (() => void) | null = null + private clearConnectionCallbacks: (() => void) | null = null; constructor(config?: Config) { super(); @@ -65,9 +65,18 @@ export class WebRTCEndpoint< this.trackMetadataParser = config?.trackMetadataParser ?? ((x) => x as TrackMetadata); - this.stateManager = new StateManager(this, this.endpointMetadataParser, this.trackMetadataParser); + this.stateManager = new StateManager( + this, + this.endpointMetadataParser, + this.trackMetadataParser, + ); this.negotiationManager = new NegotiationManager(); - this.commandsQueue = new CommandsQueue(this, this.stateManager, this.negotiationManager, this.trackMetadataParser) + this.commandsQueue = new CommandsQueue( + this, + this.stateManager, + this.negotiationManager, + this.trackMetadataParser, + ); } /** @@ -249,18 +258,18 @@ export class WebRTCEndpoint< case 'tracksAdded': { this.negotiationManager.ongoingRenegotiation = true; - this.stateManager.onTracksAdded(deserializedMediaEvent.data) + this.stateManager.onTracksAdded(deserializedMediaEvent.data); break; } case 'tracksRemoved': { this.negotiationManager.ongoingRenegotiation = true; - this.stateManager.onTracksRemoved(deserializedMediaEvent.data) + this.stateManager.onTracksRemoved(deserializedMediaEvent.data); break; } case 'sdpAnswer': - this.stateManager.onSdpAnswer(deserializedMediaEvent.data) + this.stateManager.onSdpAnswer(deserializedMediaEvent.data); this.negotiationManager.ongoingRenegotiation = false; this.commandsQueue.processNextCommand(); @@ -271,35 +280,37 @@ export class WebRTCEndpoint< break; case 'endpointAdded': - this.stateManager.onEndpointAdded(deserializedMediaEvent.data) + this.stateManager.onEndpointAdded(deserializedMediaEvent.data); break; case 'endpointRemoved': - if (deserializedMediaEvent.data.id === this.stateManager.localEndpoint.id) { + if ( + deserializedMediaEvent.data.id === this.stateManager.localEndpoint.id + ) { this.cleanUp(); this.emit('disconnected'); return; } - this.stateManager.onEndpointRemoved(deserializedMediaEvent.data) + this.stateManager.onEndpointRemoved(deserializedMediaEvent.data); break; case 'endpointUpdated': - this.stateManager.onEndpointUpdated(deserializedMediaEvent.data) + this.stateManager.onEndpointUpdated(deserializedMediaEvent.data); break; case 'trackUpdated': { - this.stateManager.onTrackUpdated(deserializedMediaEvent.data) + this.stateManager.onTrackUpdated(deserializedMediaEvent.data); break; } case 'trackEncodingDisabled': { - this.stateManager.onTrackEncodingDisabled(deserializedMediaEvent.data) + this.stateManager.onTrackEncodingDisabled(deserializedMediaEvent.data); break; } case 'trackEncodingEnabled': { - this.stateManager.onTrackEncodingEnabled(deserializedMediaEvent.data) + this.stateManager.onTrackEncodingEnabled(deserializedMediaEvent.data); break; } @@ -318,7 +329,7 @@ export class WebRTCEndpoint< } case 'encodingSwitched': { - this.stateManager.onEncodingSwitched(deserializedMediaEvent.data) + this.stateManager.onEncodingSwitched(deserializedMediaEvent.data); break; } case 'custom': @@ -761,7 +772,7 @@ export class WebRTCEndpoint< * ``` */ public disableTrackEncoding(trackId: string, encoding: TrackEncoding) { - this.stateManager.disableTrackEncoding(trackId, encoding) + this.stateManager.disableTrackEncoding(trackId, encoding); } /** @@ -860,10 +871,10 @@ export class WebRTCEndpoint< */ public cleanUp = () => { if (this.stateManager.connection) { - this.clearConnectionCallbacks?.() + this.clearConnectionCallbacks?.(); this.stateManager.connection.close(); - this.commandsQueue.clenUp() + this.commandsQueue.clenUp(); this.stateManager.ongoingTrackReplacement = false; this.negotiationManager.ongoingRenegotiation = false; @@ -922,26 +933,56 @@ export class WebRTCEndpoint< const turnServers = offerData.data.integratedTurnServers; setTurns(turnServers, this.stateManager.rtcConfig); - this.stateManager.connection = new RTCPeerConnection(this.stateManager.rtcConfig); + this.stateManager.connection = new RTCPeerConnection( + this.stateManager.rtcConfig, + ); - const onIceCandidate = (event: RTCPeerConnectionIceEvent) => this.onLocalCandidate(event) - const onIceCandidateError = (event: RTCPeerConnectionIceErrorEvent) => this.onIceCandidateError(event) - const onConnectionStateChange = (event: Event) => this.onConnectionStateChange(event) - const onIceConnectionStateChange = (event: Event) => this.onIceConnectionStateChange(event) + const onIceCandidate = (event: RTCPeerConnectionIceEvent) => + this.onLocalCandidate(event); + const onIceCandidateError = (event: RTCPeerConnectionIceErrorEvent) => + this.onIceCandidateError(event); + const onConnectionStateChange = (event: Event) => + this.onConnectionStateChange(event); + const onIceConnectionStateChange = (event: Event) => + this.onIceConnectionStateChange(event); this.clearConnectionCallbacks = () => { - this.stateManager.connection?.removeEventListener("icecandidate", onIceCandidate) - this.stateManager.connection?.removeEventListener("icecandidateerror", onIceCandidateError) - this.stateManager.connection?.removeEventListener("connectionstatechange", onConnectionStateChange) - this.stateManager.connection?.removeEventListener("iceconnectionstatechange", onIceConnectionStateChange) - } + this.stateManager.connection?.removeEventListener( + 'icecandidate', + onIceCandidate, + ); + this.stateManager.connection?.removeEventListener( + 'icecandidateerror', + onIceCandidateError, + ); + this.stateManager.connection?.removeEventListener( + 'connectionstatechange', + onConnectionStateChange, + ); + this.stateManager.connection?.removeEventListener( + 'iceconnectionstatechange', + onIceConnectionStateChange, + ); + }; - this.stateManager.connection.addEventListener("icecandidate", onIceCandidate) - this.stateManager.connection.addEventListener("icecandidateerror", onIceCandidateError) - this.stateManager.connection.addEventListener("connectionstatechange", onConnectionStateChange) - this.stateManager.connection.addEventListener("iceconnectionstatechange", onIceConnectionStateChange) + this.stateManager.connection.addEventListener( + 'icecandidate', + onIceCandidate, + ); + this.stateManager.connection.addEventListener( + 'icecandidateerror', + onIceCandidateError, + ); + this.stateManager.connection.addEventListener( + 'connectionstatechange', + onConnectionStateChange, + ); + this.stateManager.connection.addEventListener( + 'iceconnectionstatechange', + onIceConnectionStateChange, + ); - this.commandsQueue.setupEventListeners(this.stateManager.connection) + this.commandsQueue.setupEventListeners(this.stateManager.connection); Array.from(this.stateManager.localTrackIdToTrack.values()).forEach( (trackContext) => @@ -999,7 +1040,6 @@ export class WebRTCEndpoint< } }; - private onIceCandidateError = (event: RTCPeerConnectionIceErrorEvent) => { console.warn(event); }; From d2d288e42b24c4bb1b5477a2b7ba6642369af0d8 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 23:39:45 +0200 Subject: [PATCH 40/50] Move `removeTrackHandler` to StateManager.ts --- .../ts-client/src/webrtc/CommandsQueue.ts | 23 +++---------------- packages/ts-client/src/webrtc/StateManager.ts | 21 +++++++++++++++-- packages/ts-client/src/webrtc/commands.ts | 7 ++++++ .../ts-client/src/webrtc/webRTCEndpoint.ts | 11 ++++++--- 4 files changed, 37 insertions(+), 25 deletions(-) diff --git a/packages/ts-client/src/webrtc/CommandsQueue.ts b/packages/ts-client/src/webrtc/CommandsQueue.ts index dc318218..63890b40 100644 --- a/packages/ts-client/src/webrtc/CommandsQueue.ts +++ b/packages/ts-client/src/webrtc/CommandsQueue.ts @@ -138,12 +138,12 @@ export class CommandsQueue { case 'ADD-TRACK': this.addTrackHandler(command); break; - case 'REMOVE-TRACK': - this.removeTrackHandler(command); - break; case 'REPLACE-TRACK': this.replaceTrackHandler(command); break; + case "COMMAND-WITH-HANDLER": + command.handler() + break; } } @@ -217,23 +217,6 @@ export class CommandsQueue { this.webrtc.sendMediaEvent(mediaEvent); } - private removeTrackHandler(command: RemoveTrackCommand) { - const { trackId } = command; - const trackContext = this.stateManager.localTrackIdToTrack.get(trackId)!; - const sender = findSender( - this.stateManager.connection, - trackContext.track!.id, - ); - - this.negotiationManager.ongoingRenegotiation = true; - - this.stateManager.connection!.removeTrack(sender); - const mediaEvent = generateCustomEvent({ type: 'renegotiateTracks' }); - this.webrtc.sendMediaEvent(mediaEvent); - this.stateManager.localTrackIdToTrack.delete(trackId); - this.stateManager.localEndpoint.tracks.delete(trackId); - } - private async replaceTrackHandler( command: ReplaceTackCommand, ) { diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index f565af62..b2e3998e 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -6,10 +6,11 @@ import type { } from './types'; import { isTrackKind, mapMediaEventTracksToTrackContextImpl } from './internal'; import type { TrackContextImpl, EndpointWithTrackContext } from './internal'; -import { findSenderByTrack } from './RTCPeerConnectionUtils'; -import { generateMediaEvent } from './mediaEvent'; +import { findSender, findSenderByTrack } from './RTCPeerConnectionUtils'; +import { generateCustomEvent, generateMediaEvent } from './mediaEvent'; import type { WebRTCEndpoint } from './webRTCEndpoint'; import { isVadStatus } from './voiceActivityDetection'; +import { NegotiationManager } from "./NegotiationManager"; export class StateManager { public trackIdToTrack: Map< @@ -56,15 +57,18 @@ export class StateManager { // temporary for webrtc.emit and webrtc.sendMediaEvent private readonly webrtc: WebRTCEndpoint; + private readonly negotiationManager: NegotiationManager; private readonly endpointMetadataParser: MetadataParser; private readonly trackMetadataParser: MetadataParser; constructor( webrtc: WebRTCEndpoint, + negotiationManager: NegotiationManager, endpointMetadataParser: MetadataParser, trackMetadataParser: MetadataParser, ) { this.webrtc = webrtc; + this.negotiationManager = negotiationManager; this.endpointMetadataParser = endpointMetadataParser; this.trackMetadataParser = trackMetadataParser; } @@ -361,6 +365,19 @@ export class StateManager { this.webrtc.emit('bandwidthEstimationChanged', this.bandwidthEstimation); } + public removeTrackHandler(trackId: string) { + const trackContext = this.localTrackIdToTrack.get(trackId)!; + const sender = findSender(this.connection, trackContext.track!.id,); + + this.negotiationManager.ongoingRenegotiation = true; + + this.connection!.removeTrack(sender); + const mediaEvent = generateCustomEvent({ type: 'renegotiateTracks' }); + this.webrtc.sendMediaEvent(mediaEvent); + this.localTrackIdToTrack.delete(trackId); + this.localEndpoint.tracks.delete(trackId); + } + private addEndpoint = ( endpoint: EndpointWithTrackContext, ): void => { diff --git a/packages/ts-client/src/webrtc/commands.ts b/packages/ts-client/src/webrtc/commands.ts index 8c4bb850..de4398ad 100644 --- a/packages/ts-client/src/webrtc/commands.ts +++ b/packages/ts-client/src/webrtc/commands.ts @@ -26,7 +26,14 @@ export type ReplaceTackCommand = { resolutionNotifier: Deferred; }; +export type CommandWithHandler = { + commandType: 'COMMAND-WITH-HANDLER'; + handler: () => void; + resolutionNotifier: Deferred; +}; + export type Command = + | CommandWithHandler | AddTrackCommand | RemoveTrackCommand | ReplaceTackCommand; diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 9e683f64..a31ad599 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -65,12 +65,13 @@ export class WebRTCEndpoint< this.trackMetadataParser = config?.trackMetadataParser ?? ((x) => x as TrackMetadata); + this.negotiationManager = new NegotiationManager(); this.stateManager = new StateManager( this, + this.negotiationManager, this.endpointMetadataParser, this.trackMetadataParser, ); - this.negotiationManager = new NegotiationManager(); this.commandsQueue = new CommandsQueue( this, this.stateManager, @@ -673,11 +674,15 @@ export class WebRTCEndpoint< */ public removeTrack(trackId: string): Promise { const resolutionNotifier = new Deferred(); + this.commandsQueue.pushCommand({ - commandType: 'REMOVE-TRACK', - trackId, + commandType: 'COMMAND-WITH-HANDLER', + handler: () => { + this.stateManager.removeTrackHandler(trackId) + }, resolutionNotifier, }); + return resolutionNotifier.promise.then(() => { this.emit('localTrackRemoved', { trackId, From 8c2a66d5be89280fb4a98952e11ca09482a3cf8d Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Mon, 8 Jul 2024 23:54:36 +0200 Subject: [PATCH 41/50] Move `replaceTrackHandler` to StateManager.ts --- .../ts-client/src/webrtc/CommandsQueue.ts | 63 ++----------------- packages/ts-client/src/webrtc/StateManager.ts | 57 +++++++++++++++++ packages/ts-client/src/webrtc/commands.ts | 1 + .../ts-client/src/webrtc/webRTCEndpoint.ts | 18 ++++-- 4 files changed, 76 insertions(+), 63 deletions(-) diff --git a/packages/ts-client/src/webrtc/CommandsQueue.ts b/packages/ts-client/src/webrtc/CommandsQueue.ts index 63890b40..02937ba6 100644 --- a/packages/ts-client/src/webrtc/CommandsQueue.ts +++ b/packages/ts-client/src/webrtc/CommandsQueue.ts @@ -138,11 +138,13 @@ export class CommandsQueue { case 'ADD-TRACK': this.addTrackHandler(command); break; - case 'REPLACE-TRACK': - this.replaceTrackHandler(command); - break; case "COMMAND-WITH-HANDLER": command.handler() + if(command.resolve === "immediately") { + this.resolvePreviousCommand(); + this.processNextCommand(); + } + break; } } @@ -217,61 +219,6 @@ export class CommandsQueue { this.webrtc.sendMediaEvent(mediaEvent); } - private async replaceTrackHandler( - command: ReplaceTackCommand, - ) { - const { trackId, newTrack, newTrackMetadata } = command; - - // todo add validation to track.kind, you cannot replace video with audio - - const trackContext = this.stateManager.localTrackIdToTrack.get(trackId)!; - - const track = this.stateManager.trackIdToSender.get(trackId); - const sender = track?.sender ?? null; - - if (!track) throw Error(`There is no track with id: ${trackId}`); - if (!sender) throw Error('There is no RTCRtpSender for this track id!'); - - this.stateManager.ongoingTrackReplacement = true; - - trackContext.stream?.getTracks().forEach((track) => { - trackContext.stream?.removeTrack(track); - }); - - if (newTrack) { - trackContext.stream?.addTrack(newTrack); - } - - if (trackContext.track && !newTrack) { - const mediaEvent = generateMediaEvent('muteTrack', { trackId: trackId }); - this.webrtc.sendMediaEvent(mediaEvent); - this.webrtc.emit('localTrackMuted', { trackId: trackId }); - } else if (!trackContext.track && newTrack) { - const mediaEvent = generateMediaEvent('unmuteTrack', { - trackId: trackId, - }); - this.webrtc.sendMediaEvent(mediaEvent); - this.webrtc.emit('localTrackUnmuted', { trackId: trackId }); - } - - track.localTrackId = newTrack?.id ?? null; - - try { - await sender.replaceTrack(newTrack); - trackContext.track = newTrack; - - if (newTrackMetadata) { - this.webrtc.updateTrackMetadata(trackId, newTrackMetadata); - } - } catch (error) { - // ignore - } finally { - this.resolvePreviousCommand(); - this.stateManager.ongoingTrackReplacement = false; - this.processNextCommand(); - } - } - private resolvePreviousCommand() { if (this.commandResolutionNotifier) { this.commandResolutionNotifier.resolve(); diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index b2e3998e..c7fd0a47 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -11,6 +11,7 @@ import { generateCustomEvent, generateMediaEvent } from './mediaEvent'; import type { WebRTCEndpoint } from './webRTCEndpoint'; import { isVadStatus } from './voiceActivityDetection'; import { NegotiationManager } from "./NegotiationManager"; +import type { ReplaceTackCommand } from "./commands"; export class StateManager { public trackIdToTrack: Map< @@ -378,6 +379,62 @@ export class StateManager { this.localEndpoint.tracks.delete(trackId); } + public async replaceTrackHandler( + trackId: string, + newTrack: MediaStreamTrack | null, + newTrackMetadata?: TrackMetadata, + ) { + // todo add validation to track.kind, you cannot replace video with audio + + const trackContext = this.localTrackIdToTrack.get(trackId)!; + + const track = this.trackIdToSender.get(trackId); + const sender = track?.sender ?? null; + + if (!track) throw Error(`There is no track with id: ${trackId}`); + if (!sender) throw Error('There is no RTCRtpSender for this track id!'); + + this.ongoingTrackReplacement = true; + + trackContext.stream?.getTracks().forEach((track) => { + trackContext.stream?.removeTrack(track); + }); + + if (newTrack) { + trackContext.stream?.addTrack(newTrack); + } + + if (trackContext.track && !newTrack) { + const mediaEvent = generateMediaEvent('muteTrack', { trackId: trackId }); + this.webrtc.sendMediaEvent(mediaEvent); + this.webrtc.emit('localTrackMuted', { trackId: trackId }); + } else if (!trackContext.track && newTrack) { + const mediaEvent = generateMediaEvent('unmuteTrack', { + trackId: trackId, + }); + this.webrtc.sendMediaEvent(mediaEvent); + this.webrtc.emit('localTrackUnmuted', { trackId: trackId }); + } + + track.localTrackId = newTrack?.id ?? null; + + try { + await sender.replaceTrack(newTrack); + trackContext.track = newTrack; + + if (newTrackMetadata) { + this.webrtc.updateTrackMetadata(trackId, newTrackMetadata); + } + } catch (error) { + // ignore + } finally { + // this.resolvePreviousCommand(); + this.ongoingTrackReplacement = false; + // this.processNextCommand(); + } + } + + private addEndpoint = ( endpoint: EndpointWithTrackContext, ): void => { diff --git a/packages/ts-client/src/webrtc/commands.ts b/packages/ts-client/src/webrtc/commands.ts index de4398ad..1fb7f5c1 100644 --- a/packages/ts-client/src/webrtc/commands.ts +++ b/packages/ts-client/src/webrtc/commands.ts @@ -30,6 +30,7 @@ export type CommandWithHandler = { commandType: 'COMMAND-WITH-HANDLER'; handler: () => void; resolutionNotifier: Deferred; + resolve: "after-renegotiation" | "immediately"; }; export type Command = diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index a31ad599..ba77b4cb 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -42,7 +42,7 @@ export class WebRTCEndpoint< EndpointMetadata = any, TrackMetadata = any, > extends (EventEmitter as { - new (): TypedEmitter< + new(): TypedEmitter< Required> >; }) { @@ -508,6 +508,7 @@ export class WebRTCEndpoint< newTrackMetadata?: any, ): Promise { const resolutionNotifier = new Deferred(); + try { const newMetadata = newTrackMetadata !== undefined @@ -515,15 +516,21 @@ export class WebRTCEndpoint< : undefined; this.commandsQueue.pushCommand({ - commandType: 'REPLACE-TRACK', - trackId, - newTrack, - newTrackMetadata: newMetadata, + commandType: 'COMMAND-WITH-HANDLER', + handler: () => { + this.stateManager.replaceTrackHandler( + trackId, + newTrack, + newMetadata, + ) + }, resolutionNotifier, + resolve: "immediately" }); } catch (error) { resolutionNotifier.reject(error); } + return resolutionNotifier.promise.then(() => { this.emit('localTrackReplaced', { trackId, @@ -681,6 +688,7 @@ export class WebRTCEndpoint< this.stateManager.removeTrackHandler(trackId) }, resolutionNotifier, + resolve: "after-renegotiation" }); return resolutionNotifier.promise.then(() => { From e558f7937c1bf7c0bf75e0dbba959a4046b9973b Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Tue, 9 Jul 2024 00:20:11 +0200 Subject: [PATCH 42/50] Move `addTrackHandler` to StateManager.ts --- .../ts-client/src/webrtc/CommandsQueue.ts | 108 ++---------------- packages/ts-client/src/webrtc/StateManager.ts | 82 ++++++++++++- packages/ts-client/src/webrtc/commands.ts | 36 +----- .../ts-client/src/webrtc/webRTCEndpoint.ts | 25 ++-- 4 files changed, 103 insertions(+), 148 deletions(-) diff --git a/packages/ts-client/src/webrtc/CommandsQueue.ts b/packages/ts-client/src/webrtc/CommandsQueue.ts index 02937ba6..b38ab2a8 100644 --- a/packages/ts-client/src/webrtc/CommandsQueue.ts +++ b/packages/ts-client/src/webrtc/CommandsQueue.ts @@ -1,37 +1,20 @@ -import type { - AddTrackCommand, - Command, - RemoveTrackCommand, - ReplaceTackCommand, -} from './commands'; +import type { Command } from './commands'; import type { Deferred } from './deferred'; -import { findSender, isTrackInUse } from './RTCPeerConnectionUtils'; -import { isTrackKind, TrackContextImpl } from './internal'; -import { addTrackToConnection, setTransceiverDirection } from './transciever'; -import { generateCustomEvent, generateMediaEvent } from './mediaEvent'; import type { StateManager } from './StateManager'; import type { NegotiationManager } from './NegotiationManager'; -import type { WebRTCEndpoint } from './webRTCEndpoint'; -import type { MetadataParser } from './types'; export class CommandsQueue { private readonly stateManager: StateManager; private negotiationManager: NegotiationManager; - private webrtc: WebRTCEndpoint; - private readonly trackMetadataParser: MetadataParser; private clearConnectionCallbacks: (() => void) | null = null; constructor( - webrtc: WebRTCEndpoint, stateManager: StateManager, negotiationManager: NegotiationManager, - trackMetadataParser: MetadataParser, ) { - this.webrtc = webrtc; this.stateManager = stateManager; this.negotiationManager = negotiationManager; - this.trackMetadataParser = trackMetadataParser; } public setupEventListeners(connection: RTCPeerConnection) { @@ -100,10 +83,10 @@ export class CommandsQueue { connection.addEventListener('signalingstatechange', onSignalingStateChange); } - private commandsQueue: Command[] = []; + private commandsQueue: Command[] = []; private commandResolutionNotifier: Deferred | null = null; - public pushCommand(command: Command) { + public pushCommand(command: Command) { this.commandsQueue.push(command); this.processNextCommand(); } @@ -133,90 +116,21 @@ export class CommandsQueue { this.handleCommand(command); } - private handleCommand(command: Command) { - switch (command.commandType) { - case 'ADD-TRACK': - this.addTrackHandler(command); - break; - case "COMMAND-WITH-HANDLER": - command.handler() - if(command.resolve === "immediately") { - this.resolvePreviousCommand(); - this.processNextCommand(); - } - - break; - } - } - - private addTrackHandler(addTrackCommand: AddTrackCommand) { - const { - simulcastConfig, - maxBandwidth, - track, - stream, - trackMetadata, - trackId, - } = addTrackCommand; - const isUsedTrack = isTrackInUse(this.stateManager.connection, track); - - let error; - if (isUsedTrack) { - error = - "This track was already added to peerConnection, it can't be added again!"; - } - - if (!simulcastConfig.enabled && !(typeof maxBandwidth === 'number')) - error = - 'Invalid type of `maxBandwidth` argument for a non-simulcast track, expected: number'; - if (this.stateManager.getEndpointId() === '') - error = 'Cannot add tracks before being accepted by the server'; + private handleCommand(command: Command) { + const error = command.validate?.() if (error) { this.commandResolutionNotifier?.reject(error); this.commandResolutionNotifier = null; this.processNextCommand(); - return; - } - - this.negotiationManager.ongoingRenegotiation = true; + } else { + command.handler() - const trackContext = new TrackContextImpl( - this.stateManager.localEndpoint, - trackId, - trackMetadata, - simulcastConfig, - this.trackMetadataParser, - ); - - if (!isTrackKind(track.kind)) throw new Error('Track has no kind'); - - trackContext.track = track; - trackContext.stream = stream; - trackContext.maxBandwidth = maxBandwidth; - trackContext.trackKind = track.kind; - - this.stateManager.localEndpoint.tracks.set(trackId, trackContext); - - this.stateManager.localTrackIdToTrack.set(trackId, trackContext); - - if (this.stateManager.connection) { - addTrackToConnection( - trackContext, - this.stateManager.disabledTrackEncodings, - this.stateManager.connection, - ); - - setTransceiverDirection(this.stateManager.connection); + if (command.resolve === "immediately") { + this.resolvePreviousCommand(); + this.processNextCommand(); + } } - - this.stateManager.trackIdToSender.set(trackId, { - remoteTrackId: trackId, - localTrackId: track.id, - sender: null, - }); - const mediaEvent = generateCustomEvent({ type: 'renegotiateTracks' }); - this.webrtc.sendMediaEvent(mediaEvent); } private resolvePreviousCommand() { diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index c7fd0a47..31589754 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -1,17 +1,18 @@ import type { LocalTrackId, MetadataParser, - RemoteTrackId, + RemoteTrackId, SimulcastConfig, TrackBandwidthLimit, TrackEncoding, } from './types'; import { isTrackKind, mapMediaEventTracksToTrackContextImpl } from './internal'; -import type { TrackContextImpl, EndpointWithTrackContext } from './internal'; -import { findSender, findSenderByTrack } from './RTCPeerConnectionUtils'; +import type { EndpointWithTrackContext } from './internal'; +import { TrackContextImpl } from './internal'; +import { findSender, findSenderByTrack, isTrackInUse } from './RTCPeerConnectionUtils'; import { generateCustomEvent, generateMediaEvent } from './mediaEvent'; import type { WebRTCEndpoint } from './webRTCEndpoint'; import { isVadStatus } from './voiceActivityDetection'; -import { NegotiationManager } from "./NegotiationManager"; -import type { ReplaceTackCommand } from "./commands"; +import type { NegotiationManager } from "./NegotiationManager"; +import { addTrackToConnection, setTransceiverDirection } from "./transciever"; export class StateManager { public trackIdToTrack: Map< @@ -366,6 +367,77 @@ export class StateManager { this.webrtc.emit('bandwidthEstimationChanged', this.bandwidthEstimation); } + public validateAddTrack( + track: MediaStreamTrack, + simulcastConfig: SimulcastConfig, + maxBandwidth: TrackBandwidthLimit, + ): string | null { + const isUsedTrack = isTrackInUse(this.connection, track); + + let error; + if (isUsedTrack) { + error = + "This track was already added to peerConnection, it can't be added again!"; + } + + if (!simulcastConfig.enabled && !(typeof maxBandwidth === 'number')) + error = + 'Invalid type of `maxBandwidth` argument for a non-simulcast track, expected: number'; + if (this.getEndpointId() === '') + error = 'Cannot add tracks before being accepted by the server'; + + return error ?? null + } + + public addTrackHandler( + trackId: string, + track: MediaStreamTrack, + stream: MediaStream, + trackMetadata: TrackMetadata | undefined, + simulcastConfig: SimulcastConfig, + maxBandwidth: TrackBandwidthLimit, + ) { + this.negotiationManager.ongoingRenegotiation = true; + + const trackContext = new TrackContextImpl( + this.localEndpoint, + trackId, + trackMetadata, + simulcastConfig, + this.trackMetadataParser, + ); + + if (!isTrackKind(track.kind)) throw new Error('Track has no kind'); + + trackContext.track = track; + trackContext.stream = stream; + trackContext.maxBandwidth = maxBandwidth; + trackContext.trackKind = track.kind; + + this.localEndpoint.tracks.set(trackId, trackContext); + + this.localTrackIdToTrack.set(trackId, trackContext); + + if (this.connection) { + addTrackToConnection( + trackContext, + this.disabledTrackEncodings, + this.connection, + ); + + setTransceiverDirection(this.connection); + } + + this.trackIdToSender.set(trackId, { + remoteTrackId: trackId, + localTrackId: track.id, + sender: null, + }); + const mediaEvent = generateCustomEvent({ type: 'renegotiateTracks' }); + this.webrtc.sendMediaEvent(mediaEvent); + } + + public removeTrackHandler(trackId: string) { const trackContext = this.localTrackIdToTrack.get(trackId)!; const sender = findSender(this.connection, trackContext.track!.id,); diff --git a/packages/ts-client/src/webrtc/commands.ts b/packages/ts-client/src/webrtc/commands.ts index 1fb7f5c1..0744a5dd 100644 --- a/packages/ts-client/src/webrtc/commands.ts +++ b/packages/ts-client/src/webrtc/commands.ts @@ -1,40 +1,10 @@ -import type { SimulcastConfig, TrackBandwidthLimit } from './types'; import type { Deferred } from './deferred'; -export type AddTrackCommand = { - commandType: 'ADD-TRACK'; - trackId: string; - track: MediaStreamTrack; - stream: MediaStream; - trackMetadata?: TrackMetadata; - simulcastConfig: SimulcastConfig; - maxBandwidth: TrackBandwidthLimit; - resolutionNotifier: Deferred; -}; - -export type RemoveTrackCommand = { - commandType: 'REMOVE-TRACK'; - trackId: string; - resolutionNotifier: Deferred; -}; - -export type ReplaceTackCommand = { - commandType: 'REPLACE-TRACK'; - trackId: string; - newTrack: MediaStreamTrack | null; - newTrackMetadata?: TrackMetadata; - resolutionNotifier: Deferred; -}; - -export type CommandWithHandler = { +export type Command = { commandType: 'COMMAND-WITH-HANDLER'; handler: () => void; + validate?: () => string | null; resolutionNotifier: Deferred; resolve: "after-renegotiation" | "immediately"; -}; +} -export type Command = - | CommandWithHandler - | AddTrackCommand - | RemoveTrackCommand - | ReplaceTackCommand; diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index ba77b4cb..16df0ecb 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -72,12 +72,7 @@ export class WebRTCEndpoint< this.endpointMetadataParser, this.trackMetadataParser, ); - this.commandsQueue = new CommandsQueue( - this, - this.stateManager, - this.negotiationManager, - this.trackMetadataParser, - ); + this.commandsQueue = new CommandsQueue(this.stateManager, this.negotiationManager,); } /** @@ -429,13 +424,17 @@ export class WebRTCEndpoint< stream.addTrack(track); this.commandsQueue.pushCommand({ - commandType: 'ADD-TRACK', - trackId, - track, - stream, - trackMetadata: parsedMetadata, - simulcastConfig, - maxBandwidth, + commandType: 'COMMAND-WITH-HANDLER', + handler: () => { + this.stateManager.addTrackHandler(trackId, + track, + stream, + parsedMetadata, + simulcastConfig, + maxBandwidth) + }, + validate: () => this.stateManager.validateAddTrack(track, simulcastConfig, maxBandwidth), + resolve: "after-renegotiation", resolutionNotifier, }); } catch (error) { From e1408e0156083e28794dc71655cb53f5345b30d7 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Tue, 9 Jul 2024 08:38:18 +0200 Subject: [PATCH 43/50] Fix format and lint --- .../ts-client/src/webrtc/CommandsQueue.ts | 6 ++-- packages/ts-client/src/webrtc/StateManager.ts | 20 ++++++----- packages/ts-client/src/webrtc/commands.ts | 5 ++- .../ts-client/src/webrtc/webRTCEndpoint.ts | 34 +++++++++++-------- 4 files changed, 37 insertions(+), 28 deletions(-) diff --git a/packages/ts-client/src/webrtc/CommandsQueue.ts b/packages/ts-client/src/webrtc/CommandsQueue.ts index b38ab2a8..1b6917b2 100644 --- a/packages/ts-client/src/webrtc/CommandsQueue.ts +++ b/packages/ts-client/src/webrtc/CommandsQueue.ts @@ -117,16 +117,16 @@ export class CommandsQueue { } private handleCommand(command: Command) { - const error = command.validate?.() + const error = command.validate?.(); if (error) { this.commandResolutionNotifier?.reject(error); this.commandResolutionNotifier = null; this.processNextCommand(); } else { - command.handler() + command.handler(); - if (command.resolve === "immediately") { + if (command.resolve === 'immediately') { this.resolvePreviousCommand(); this.processNextCommand(); } diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index 31589754..d6131b90 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -1,18 +1,24 @@ import type { LocalTrackId, MetadataParser, - RemoteTrackId, SimulcastConfig, TrackBandwidthLimit, + RemoteTrackId, + SimulcastConfig, + TrackBandwidthLimit, TrackEncoding, } from './types'; import { isTrackKind, mapMediaEventTracksToTrackContextImpl } from './internal'; import type { EndpointWithTrackContext } from './internal'; import { TrackContextImpl } from './internal'; -import { findSender, findSenderByTrack, isTrackInUse } from './RTCPeerConnectionUtils'; +import { + findSender, + findSenderByTrack, + isTrackInUse, +} from './RTCPeerConnectionUtils'; import { generateCustomEvent, generateMediaEvent } from './mediaEvent'; import type { WebRTCEndpoint } from './webRTCEndpoint'; import { isVadStatus } from './voiceActivityDetection'; -import type { NegotiationManager } from "./NegotiationManager"; -import { addTrackToConnection, setTransceiverDirection } from "./transciever"; +import type { NegotiationManager } from './NegotiationManager'; +import { addTrackToConnection, setTransceiverDirection } from './transciever'; export class StateManager { public trackIdToTrack: Map< @@ -386,7 +392,7 @@ export class StateManager { if (this.getEndpointId() === '') error = 'Cannot add tracks before being accepted by the server'; - return error ?? null + return error ?? null; } public addTrackHandler( @@ -437,10 +443,9 @@ export class StateManager { this.webrtc.sendMediaEvent(mediaEvent); } - public removeTrackHandler(trackId: string) { const trackContext = this.localTrackIdToTrack.get(trackId)!; - const sender = findSender(this.connection, trackContext.track!.id,); + const sender = findSender(this.connection, trackContext.track!.id); this.negotiationManager.ongoingRenegotiation = true; @@ -506,7 +511,6 @@ export class StateManager { } } - private addEndpoint = ( endpoint: EndpointWithTrackContext, ): void => { diff --git a/packages/ts-client/src/webrtc/commands.ts b/packages/ts-client/src/webrtc/commands.ts index 0744a5dd..ea07e63f 100644 --- a/packages/ts-client/src/webrtc/commands.ts +++ b/packages/ts-client/src/webrtc/commands.ts @@ -5,6 +5,5 @@ export type Command = { handler: () => void; validate?: () => string | null; resolutionNotifier: Deferred; - resolve: "after-renegotiation" | "immediately"; -} - + resolve: 'after-renegotiation' | 'immediately'; +}; diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 16df0ecb..717a8e0b 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -42,7 +42,7 @@ export class WebRTCEndpoint< EndpointMetadata = any, TrackMetadata = any, > extends (EventEmitter as { - new(): TypedEmitter< + new (): TypedEmitter< Required> >; }) { @@ -72,7 +72,10 @@ export class WebRTCEndpoint< this.endpointMetadataParser, this.trackMetadataParser, ); - this.commandsQueue = new CommandsQueue(this.stateManager, this.negotiationManager,); + this.commandsQueue = new CommandsQueue( + this.stateManager, + this.negotiationManager, + ); } /** @@ -426,15 +429,22 @@ export class WebRTCEndpoint< this.commandsQueue.pushCommand({ commandType: 'COMMAND-WITH-HANDLER', handler: () => { - this.stateManager.addTrackHandler(trackId, + this.stateManager.addTrackHandler( + trackId, track, stream, parsedMetadata, simulcastConfig, - maxBandwidth) + maxBandwidth, + ); }, - validate: () => this.stateManager.validateAddTrack(track, simulcastConfig, maxBandwidth), - resolve: "after-renegotiation", + validate: () => + this.stateManager.validateAddTrack( + track, + simulcastConfig, + maxBandwidth, + ), + resolve: 'after-renegotiation', resolutionNotifier, }); } catch (error) { @@ -517,14 +527,10 @@ export class WebRTCEndpoint< this.commandsQueue.pushCommand({ commandType: 'COMMAND-WITH-HANDLER', handler: () => { - this.stateManager.replaceTrackHandler( - trackId, - newTrack, - newMetadata, - ) + this.stateManager.replaceTrackHandler(trackId, newTrack, newMetadata); }, resolutionNotifier, - resolve: "immediately" + resolve: 'immediately', }); } catch (error) { resolutionNotifier.reject(error); @@ -684,10 +690,10 @@ export class WebRTCEndpoint< this.commandsQueue.pushCommand({ commandType: 'COMMAND-WITH-HANDLER', handler: () => { - this.stateManager.removeTrackHandler(trackId) + this.stateManager.removeTrackHandler(trackId); }, resolutionNotifier, - resolve: "after-renegotiation" + resolve: 'after-renegotiation', }); return resolutionNotifier.promise.then(() => { From f7df3a31425e0ec29401809f168e2c299f7d52c7 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Tue, 9 Jul 2024 08:42:20 +0200 Subject: [PATCH 44/50] Revert e2e test file --- e2e-tests/react-client/app/playwright.config.ts | 2 -- e2e-tests/ts-client/app/playwright.config.ts | 8 -------- 2 files changed, 10 deletions(-) diff --git a/e2e-tests/react-client/app/playwright.config.ts b/e2e-tests/react-client/app/playwright.config.ts index 7857fe30..e722a51e 100644 --- a/e2e-tests/react-client/app/playwright.config.ts +++ b/e2e-tests/react-client/app/playwright.config.ts @@ -50,8 +50,6 @@ export default defineConfig({ ], // default Google Chrome path on MacOS // executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", - executablePath: - "/Users/kamilstasiak/Library/Caches/ms-playwright/chromium-1091/chrome-mac/Chromium.app/Contents/MacOS/Chromium", }, }, }, diff --git a/e2e-tests/ts-client/app/playwright.config.ts b/e2e-tests/ts-client/app/playwright.config.ts index 86793c7c..c5e7bd76 100644 --- a/e2e-tests/ts-client/app/playwright.config.ts +++ b/e2e-tests/ts-client/app/playwright.config.ts @@ -56,17 +56,9 @@ export default defineConfig({ ], // default Google Chrome path on MacOS // executablePath: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", - executablePath: - "/Users/kamilstasiak/Library/Caches/ms-playwright/chromium-1091/chrome-mac/Chromium.app/Contents/MacOS/Chromium", }, }, }, - // { - // name: "safari", - // use: { - // ...devices["Desktop Safari"] - // } - // } ], /* Run your local dev server before starting the tests */ From 34e58f0616264bdb0cd68f2081a544b99d955621 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Tue, 9 Jul 2024 08:43:44 +0200 Subject: [PATCH 45/50] Remove commendType --- packages/ts-client/src/webrtc/commands.ts | 1 - packages/ts-client/src/webrtc/webRTCEndpoint.ts | 3 --- 2 files changed, 4 deletions(-) diff --git a/packages/ts-client/src/webrtc/commands.ts b/packages/ts-client/src/webrtc/commands.ts index ea07e63f..c334e3da 100644 --- a/packages/ts-client/src/webrtc/commands.ts +++ b/packages/ts-client/src/webrtc/commands.ts @@ -1,7 +1,6 @@ import type { Deferred } from './deferred'; export type Command = { - commandType: 'COMMAND-WITH-HANDLER'; handler: () => void; validate?: () => string | null; resolutionNotifier: Deferred; diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 717a8e0b..6f9f54ca 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -427,7 +427,6 @@ export class WebRTCEndpoint< stream.addTrack(track); this.commandsQueue.pushCommand({ - commandType: 'COMMAND-WITH-HANDLER', handler: () => { this.stateManager.addTrackHandler( trackId, @@ -525,7 +524,6 @@ export class WebRTCEndpoint< : undefined; this.commandsQueue.pushCommand({ - commandType: 'COMMAND-WITH-HANDLER', handler: () => { this.stateManager.replaceTrackHandler(trackId, newTrack, newMetadata); }, @@ -688,7 +686,6 @@ export class WebRTCEndpoint< const resolutionNotifier = new Deferred(); this.commandsQueue.pushCommand({ - commandType: 'COMMAND-WITH-HANDLER', handler: () => { this.stateManager.removeTrackHandler(trackId); }, From 266f6d019941c7950dc6c936ce8dce9b1c4f035c Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Tue, 9 Jul 2024 11:13:34 +0200 Subject: [PATCH 46/50] Apply PR suggestions --- e2e-tests/ts-client/app/package.json | 4 +- e2e-tests/ts-client/app/playwright.config.ts | 2 +- .../ts-client/src/webrtc/CommandsQueue.ts | 39 +++++++------ .../src/webrtc/RTCPeerConnectionUtils.ts | 2 +- packages/ts-client/src/webrtc/sdpEvents.ts | 15 ++--- packages/ts-client/src/webrtc/transciever.ts | 55 ++++++++----------- .../ts-client/src/webrtc/webRTCEndpoint.ts | 2 +- 7 files changed, 60 insertions(+), 59 deletions(-) diff --git a/e2e-tests/ts-client/app/package.json b/e2e-tests/ts-client/app/package.json index e677fa2e..825b31e9 100644 --- a/e2e-tests/ts-client/app/package.json +++ b/e2e-tests/ts-client/app/package.json @@ -20,8 +20,7 @@ "@fishjam-dev/ts-client": "*", "protobufjs": "^7.2.6", "react": "^18.2.0", - "react-dom": "^18.2.0", - "typescript": "^5.5.0" + "react-dom": "^18.2.0" }, "devDependencies": { "@playwright/test": "^1.41.2", @@ -37,6 +36,7 @@ "eslint-plugin-react-refresh": "^0.4.5", "testcontainers": "^10.3.2", "ts-proto": "^1.176.0", + "typescript": "^5.5.0", "vite": "^5.1.2", "vitest": "^1.6.0" } diff --git a/e2e-tests/ts-client/app/playwright.config.ts b/e2e-tests/ts-client/app/playwright.config.ts index c5e7bd76..70533a27 100644 --- a/e2e-tests/ts-client/app/playwright.config.ts +++ b/e2e-tests/ts-client/app/playwright.config.ts @@ -18,7 +18,7 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : 4, + workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: [ ["list"], diff --git a/packages/ts-client/src/webrtc/CommandsQueue.ts b/packages/ts-client/src/webrtc/CommandsQueue.ts index 1b6917b2..871aff48 100644 --- a/packages/ts-client/src/webrtc/CommandsQueue.ts +++ b/packages/ts-client/src/webrtc/CommandsQueue.ts @@ -91,20 +91,27 @@ export class CommandsQueue { this.processNextCommand(); } - public processNextCommand() { - if ( + private isConnectionUnstable() { + const connection = this.stateManager.connection; + if (connection === undefined) return false; + + const isSignalingUnstable = connection.signalingState !== 'stable'; + const isConnectionNotConnected = connection.connectionState !== 'connected'; + const isIceNotConnected = connection.iceConnectionState !== 'connected'; + + return isSignalingUnstable && isConnectionNotConnected && isIceNotConnected; + } + + private isNegotiationInProgress() { + return ( this.negotiationManager.ongoingRenegotiation || this.stateManager.ongoingTrackReplacement - ) - return; + ); + } - if ( - this.stateManager.connection && - (this.stateManager.connection.signalingState !== 'stable' || - this.stateManager.connection.connectionState !== 'connected' || - this.stateManager.connection.iceConnectionState !== 'connected') - ) - return; + public processNextCommand() { + if (this.isNegotiationInProgress()) return; + if (this.isConnectionUnstable()) return; this.resolvePreviousCommand(); @@ -134,13 +141,13 @@ export class CommandsQueue { } private resolvePreviousCommand() { - if (this.commandResolutionNotifier) { - this.commandResolutionNotifier.resolve(); - this.commandResolutionNotifier = null; - } + if (!this.commandResolutionNotifier) return; + + this.commandResolutionNotifier.resolve(); + this.commandResolutionNotifier = null; } - public clenUp() { + public cleanUp() { this.commandResolutionNotifier?.reject('Disconnected'); this.commandResolutionNotifier = null; this.commandsQueue = []; diff --git a/packages/ts-client/src/webrtc/RTCPeerConnectionUtils.ts b/packages/ts-client/src/webrtc/RTCPeerConnectionUtils.ts index 7f58b262..4519964c 100644 --- a/packages/ts-client/src/webrtc/RTCPeerConnectionUtils.ts +++ b/packages/ts-client/src/webrtc/RTCPeerConnectionUtils.ts @@ -10,7 +10,7 @@ export const findSenderByTrack = ( connection: RTCPeerConnection | undefined, track: MediaStreamTrack | null | undefined, ): RTCRtpSender | undefined => - connection?.getSenders().filter((sender) => sender.track === track)[0]; + connection?.getSenders().find((sender) => sender.track === track); export const isTrackInUse = ( connection: RTCPeerConnection | undefined, diff --git a/packages/ts-client/src/webrtc/sdpEvents.ts b/packages/ts-client/src/webrtc/sdpEvents.ts index 91b384bd..01529097 100644 --- a/packages/ts-client/src/webrtc/sdpEvents.ts +++ b/packages/ts-client/src/webrtc/sdpEvents.ts @@ -35,10 +35,11 @@ export const createSdpOfferEvent = ( const getTrackIdToMetadata = ( tracks: Map>, -): Record => { - const trackIdToMetadata: Record = {}; - Array.from(tracks.entries()).forEach(([trackId, { metadata }]) => { - trackIdToMetadata[trackId] = metadata; - }); - return trackIdToMetadata; -}; +): Record => + Array.from(tracks.entries()).reduce( + (previousValue, [trackId, { metadata }]) => ({ + ...previousValue, + [trackId]: metadata, + }), + {} as Record, + ); diff --git a/packages/ts-client/src/webrtc/transciever.ts b/packages/ts-client/src/webrtc/transciever.ts index 8c9a75f8..3bbc05ab 100644 --- a/packages/ts-client/src/webrtc/transciever.ts +++ b/packages/ts-client/src/webrtc/transciever.ts @@ -3,6 +3,20 @@ import { simulcastTransceiverConfig } from './const'; import { applyBandwidthLimitation } from './bandwidth'; import type { EndpointWithTrackContext, TrackContextImpl } from './internal'; +const getNeededTransceiversTypes = ( + type: string, + recvTransceivers: RTCRtpTransceiver[], + serverTracks: Map, +): string[] => { + const typeNumber = serverTracks.get(type) ?? 0; + + const typeTransceiversNumber = recvTransceivers.filter( + (elem) => elem.receiver.track.kind === type, + ).length; + + return Array(typeNumber - typeTransceiversNumber).fill(type); +}; + export const addTransceiversIfNeeded = ( connection: RTCPeerConnection | undefined, serverTracks: Map, @@ -10,24 +24,14 @@ export const addTransceiversIfNeeded = ( const recvTransceivers = connection! .getTransceivers() .filter((elem) => elem.direction === 'recvonly'); - let toAdd: string[] = []; - - const getNeededTransceiversTypes = (type: string): string[] => { - let typeNumber = serverTracks.get(type); - typeNumber = typeNumber !== undefined ? typeNumber : 0; - const typeTransceiversNumber = recvTransceivers.filter( - (elem) => elem.receiver.track.kind === type, - ).length; - return Array(typeNumber - typeTransceiversNumber).fill(type); - }; - const audio = getNeededTransceiversTypes('audio'); - const video = getNeededTransceiversTypes('video'); - toAdd = toAdd.concat(audio); - toAdd = toAdd.concat(video); - - for (const kind of toAdd) - connection?.addTransceiver(kind, { direction: 'recvonly' }); + ['audio', 'video'] + .flatMap((type) => + getNeededTransceiversTypes(type, recvTransceivers, serverTracks), + ) + .forEach((kind) => + connection?.addTransceiver(kind, { direction: 'recvonly' }), + ); }; export const setTransceiverDirection = (connection: RTCPeerConnection) => { @@ -59,18 +63,11 @@ const createTransceiverConfig = ( trackContext: TrackContext, disabledTrackEncodingsMap: Map, ): RTCRtpTransceiverInit => { - let transceiverConfig: RTCRtpTransceiverInit; - if (trackContext.track!.kind === 'audio') { - transceiverConfig = createAudioTransceiverConfig(trackContext); - } else { - transceiverConfig = createVideoTransceiverConfig( - trackContext, - disabledTrackEncodingsMap, - ); + return createAudioTransceiverConfig(trackContext); } - return transceiverConfig; + return createVideoTransceiverConfig(trackContext, disabledTrackEncodingsMap); }; const createAudioTransceiverConfig = ( @@ -200,11 +197,7 @@ const getAllNegotiatedLocalTracksMapping = ( return [...midToTrackId.entries()] .filter(([_mid, trackId]) => localEndpoint.tracks.get(trackId)) .reduce( - (acc, [mid, trackId]) => { - acc[mid] = trackId; - - return acc; - }, + (acc, [mid, trackId]) => ({ ...acc, [mid]: trackId }), {} as Record, ); }; diff --git a/packages/ts-client/src/webrtc/webRTCEndpoint.ts b/packages/ts-client/src/webrtc/webRTCEndpoint.ts index 6f9f54ca..3ca6d442 100644 --- a/packages/ts-client/src/webrtc/webRTCEndpoint.ts +++ b/packages/ts-client/src/webrtc/webRTCEndpoint.ts @@ -889,7 +889,7 @@ export class WebRTCEndpoint< this.clearConnectionCallbacks?.(); this.stateManager.connection.close(); - this.commandsQueue.clenUp(); + this.commandsQueue.cleanUp(); this.stateManager.ongoingTrackReplacement = false; this.negotiationManager.ongoingRenegotiation = false; From a45058636938d223844b5d075d13b214be11e070 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Tue, 9 Jul 2024 14:29:46 +0200 Subject: [PATCH 47/50] Apply PR suggestions --- .../ts-client/src/webrtc/CommandsQueue.ts | 32 +++--- packages/ts-client/src/webrtc/StateManager.ts | 103 +++++++++--------- packages/ts-client/src/webrtc/transciever.ts | 19 ++-- packages/ts-client/src/webrtc/turn.ts | 53 ++++----- 4 files changed, 104 insertions(+), 103 deletions(-) diff --git a/packages/ts-client/src/webrtc/CommandsQueue.ts b/packages/ts-client/src/webrtc/CommandsQueue.ts index 871aff48..99446e9b 100644 --- a/packages/ts-client/src/webrtc/CommandsQueue.ts +++ b/packages/ts-client/src/webrtc/CommandsQueue.ts @@ -17,7 +17,7 @@ export class CommandsQueue { this.negotiationManager = negotiationManager; } - public setupEventListeners(connection: RTCPeerConnection) { + public setupEventListeners = (connection: RTCPeerConnection) => { const onSignalingStateChange = () => { switch (this.stateManager.connection?.signalingState) { case 'stable': @@ -81,17 +81,17 @@ export class CommandsQueue { onIceConnectionStateChange, ); connection.addEventListener('signalingstatechange', onSignalingStateChange); - } + }; private commandsQueue: Command[] = []; private commandResolutionNotifier: Deferred | null = null; - public pushCommand(command: Command) { + public pushCommand = (command: Command) => { this.commandsQueue.push(command); this.processNextCommand(); - } + }; - private isConnectionUnstable() { + private isConnectionUnstable = () => { const connection = this.stateManager.connection; if (connection === undefined) return false; @@ -100,16 +100,16 @@ export class CommandsQueue { const isIceNotConnected = connection.iceConnectionState !== 'connected'; return isSignalingUnstable && isConnectionNotConnected && isIceNotConnected; - } + }; - private isNegotiationInProgress() { + private isNegotiationInProgress = () => { return ( this.negotiationManager.ongoingRenegotiation || this.stateManager.ongoingTrackReplacement ); - } + }; - public processNextCommand() { + public processNextCommand = () => { if (this.isNegotiationInProgress()) return; if (this.isConnectionUnstable()) return; @@ -121,9 +121,9 @@ export class CommandsQueue { this.commandResolutionNotifier = command.resolutionNotifier; this.handleCommand(command); - } + }; - private handleCommand(command: Command) { + private handleCommand = (command: Command) => { const error = command.validate?.(); if (error) { @@ -138,19 +138,19 @@ export class CommandsQueue { this.processNextCommand(); } } - } + }; - private resolvePreviousCommand() { + private resolvePreviousCommand = () => { if (!this.commandResolutionNotifier) return; this.commandResolutionNotifier.resolve(); this.commandResolutionNotifier = null; - } + }; - public cleanUp() { + public cleanUp = () => { this.commandResolutionNotifier?.reject('Disconnected'); this.commandResolutionNotifier = null; this.commandsQueue = []; this.clearConnectionCallbacks?.(); - } + }; } diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index d6131b90..9b44e230 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -6,9 +6,12 @@ import type { TrackBandwidthLimit, TrackEncoding, } from './types'; -import { isTrackKind, mapMediaEventTracksToTrackContextImpl } from './internal'; import type { EndpointWithTrackContext } from './internal'; -import { TrackContextImpl } from './internal'; +import { + isTrackKind, + mapMediaEventTracksToTrackContextImpl, + TrackContextImpl, +} from './internal'; import { findSender, findSenderByTrack, @@ -107,7 +110,7 @@ export class StateManager { * webrtc.disableTrackEncoding(trackId, "l"); * ``` */ - public disableTrackEncoding(trackId: string, encoding: TrackEncoding) { + public disableTrackEncoding = (trackId: string, encoding: TrackEncoding) => { const track = this.localTrackIdToTrack.get(trackId)?.track; this.disabledTrackEncodings.get(trackId)!.push(encoding); @@ -126,7 +129,7 @@ export class StateManager { trackId, encoding, }); - } + }; private onTrack = () => { return (event: RTCTrackEvent) => { @@ -161,7 +164,7 @@ export class StateManager { trackId.startsWith(track), ); - public onTracksAdded(data: any) { + public onTracksAdded = (data: any) => { if (this.getEndpointId() === data.endpointId) return; data.tracks = new Map(Object.entries(data.tracks)); const endpoint: EndpointWithTrackContext = @@ -184,9 +187,9 @@ export class StateManager { this.webrtc.emit('trackAdded', ctx); } }); - } + }; - public onTracksRemoved(data: any) { + public onTracksRemoved = (data: any) => { const endpointId = data.endpointId; if (this.getEndpointId() === endpointId) return; const trackIds = data.trackIds as string[]; @@ -197,9 +200,9 @@ export class StateManager { this.webrtc.emit('trackRemoved', trackContext); }); - } + }; - public onSdpAnswer(data: any) { + public onSdpAnswer = (data: any) => { this.midToTrackId = new Map(Object.entries(data.midToTrackId)); for (const trackId of Object.values(data.midToTrackId)) { @@ -222,11 +225,11 @@ export class StateManager { } this.onAnswer(data); - } + }; - public onEndpointAdded( + public onEndpointAdded = ( endpoint: EndpointWithTrackContext, - ) { + ) => { if (endpoint.id === this.getEndpointId()) return; endpoint.rawMetadata = endpoint.metadata; try { @@ -239,9 +242,9 @@ export class StateManager { this.addEndpoint(endpoint); this.webrtc.emit('endpointAdded', endpoint); - } + }; - public onEndpointUpdated(data: any) { + public onEndpointUpdated = (data: any) => { if (this.getEndpointId() === data.id) return; const endpoint: EndpointWithTrackContext = this.idToEndpoint.get(data.id)!; @@ -257,9 +260,9 @@ export class StateManager { this.addEndpoint(endpoint); this.webrtc.emit('endpointUpdated', endpoint); - } + }; - public onEndpointRemoved(data: any) { + public onEndpointRemoved = (data: any) => { const endpoint: EndpointWithTrackContext = this.idToEndpoint.get(data.id)!; if (endpoint === undefined) return; @@ -271,9 +274,9 @@ export class StateManager { this.eraseEndpoint(endpoint); this.webrtc.emit('endpointRemoved', endpoint); - } + }; - public onTrackUpdated(data: any) { + public onTrackUpdated = (data: any) => { if (this.getEndpointId() === data.endpointId) return; const endpoint: EndpointWithTrackContext = @@ -310,9 +313,9 @@ export class StateManager { endpoint.tracks.set(trackId, newTrack); this.webrtc.emit('trackUpdated', trackContext); - } + }; - public onTrackEncodingDisabled(data: any) { + public onTrackEncodingDisabled = (data: any) => { if (this.getEndpointId() === data.endpointId) return; const endpoint: EndpointWithTrackContext = @@ -327,9 +330,9 @@ export class StateManager { const trackContext = endpoint.tracks.get(trackId)!; this.webrtc.emit('trackEncodingDisabled', trackContext, encoding); - } + }; - public onTrackEncodingEnabled(data: any) { + public onTrackEncodingEnabled = (data: any) => { if (this.getEndpointId() === data.endpointId) return; const endpoint: EndpointWithTrackContext = @@ -344,18 +347,18 @@ export class StateManager { const trackContext = endpoint.tracks.get(trackId)!; this.webrtc.emit('trackEncodingEnabled', trackContext, encoding); - } + }; - public onEncodingSwitched(data: any) { + public onEncodingSwitched = (data: any) => { const trackId = data.trackId; const trackContext = this.trackIdToTrack.get(trackId)!; trackContext.encoding = data.encoding; trackContext.encodingReason = data.reason; trackContext.emit('encodingChanged', trackContext); - } + }; - public onVadNotification(data: any) { + public onVadNotification = (data: any) => { const trackId = data.trackId; const ctx = this.trackIdToTrack.get(trackId)!; const vadStatus = data.status; @@ -365,44 +368,42 @@ export class StateManager { } else { console.warn('Received unknown vad status: ', vadStatus); } - } + }; - public onBandwidthEstimation(data: any) { + public onBandwidthEstimation = (data: any) => { this.bandwidthEstimation = data.estimation; this.webrtc.emit('bandwidthEstimationChanged', this.bandwidthEstimation); - } + }; - public validateAddTrack( + public validateAddTrack = ( track: MediaStreamTrack, simulcastConfig: SimulcastConfig, maxBandwidth: TrackBandwidthLimit, - ): string | null { - const isUsedTrack = isTrackInUse(this.connection, track); + ): string | null => { + if (this.getEndpointId() === '') { + return 'Cannot add tracks before being accepted by the server'; + } - let error; - if (isUsedTrack) { - error = - "This track was already added to peerConnection, it can't be added again!"; + if (!simulcastConfig.enabled && !(typeof maxBandwidth === 'number')) { + return 'Invalid type of `maxBandwidth` argument for a non-simulcast track, expected: number'; } - if (!simulcastConfig.enabled && !(typeof maxBandwidth === 'number')) - error = - 'Invalid type of `maxBandwidth` argument for a non-simulcast track, expected: number'; - if (this.getEndpointId() === '') - error = 'Cannot add tracks before being accepted by the server'; + if (isTrackInUse(this.connection, track)) { + return "This track was already added to peerConnection, it can't be added again!"; + } - return error ?? null; - } + return null; + }; - public addTrackHandler( + public addTrackHandler = ( trackId: string, track: MediaStreamTrack, stream: MediaStream, trackMetadata: TrackMetadata | undefined, simulcastConfig: SimulcastConfig, maxBandwidth: TrackBandwidthLimit, - ) { + ) => { this.negotiationManager.ongoingRenegotiation = true; const trackContext = new TrackContextImpl( @@ -441,9 +442,9 @@ export class StateManager { }); const mediaEvent = generateCustomEvent({ type: 'renegotiateTracks' }); this.webrtc.sendMediaEvent(mediaEvent); - } + }; - public removeTrackHandler(trackId: string) { + public removeTrackHandler = (trackId: string) => { const trackContext = this.localTrackIdToTrack.get(trackId)!; const sender = findSender(this.connection, trackContext.track!.id); @@ -454,13 +455,13 @@ export class StateManager { this.webrtc.sendMediaEvent(mediaEvent); this.localTrackIdToTrack.delete(trackId); this.localEndpoint.tracks.delete(trackId); - } + }; - public async replaceTrackHandler( + public replaceTrackHandler = async ( trackId: string, newTrack: MediaStreamTrack | null, newTrackMetadata?: TrackMetadata, - ) { + ): Promise => { // todo add validation to track.kind, you cannot replace video with audio const trackContext = this.localTrackIdToTrack.get(trackId)!; @@ -509,7 +510,7 @@ export class StateManager { this.ongoingTrackReplacement = false; // this.processNextCommand(); } - } + }; private addEndpoint = ( endpoint: EndpointWithTrackContext, diff --git a/packages/ts-client/src/webrtc/transciever.ts b/packages/ts-client/src/webrtc/transciever.ts index 3bbc05ab..dbcba34b 100644 --- a/packages/ts-client/src/webrtc/transciever.ts +++ b/packages/ts-client/src/webrtc/transciever.ts @@ -176,19 +176,16 @@ const getTransceiverMapping = ( connection .getTransceivers() .filter((transceiver) => transceiver.sender.track?.id && transceiver.mid) - .reduce( - (acc, transceiver) => { - const localTrackId = transceiver.sender.track!.id; - const mid = transceiver!.mid!; + .reduce((acc, transceiver) => { + const localTrackId = transceiver.sender.track!.id; + const mid = transceiver!.mid!; - const trackContext = getTrackContext(localTrackIdToTrack, localTrackId); + const trackContext = getTrackContext(localTrackIdToTrack, localTrackId); - acc[mid] = trackContext.trackId; + acc[mid] = trackContext.trackId; - return acc; - }, - {} as Record, - ); + return acc; + }, {} as MidToTrackId); const getAllNegotiatedLocalTracksMapping = ( midToTrackId: Map = new Map(), @@ -198,6 +195,6 @@ const getAllNegotiatedLocalTracksMapping = ( .filter(([_mid, trackId]) => localEndpoint.tracks.get(trackId)) .reduce( (acc, [mid, trackId]) => ({ ...acc, [mid]: trackId }), - {} as Record, + {} as MidToTrackId, ); }; diff --git a/packages/ts-client/src/webrtc/turn.ts b/packages/ts-client/src/webrtc/turn.ts index d7baf4cc..97c9b8d7 100644 --- a/packages/ts-client/src/webrtc/turn.ts +++ b/packages/ts-client/src/webrtc/turn.ts @@ -1,30 +1,33 @@ +export type TurnServer = { + transport: string; + password: string; + serverAddr: string; + serverPort: string; + username: string; +}; + +/** + * Configures TURN servers for WebRTC connections by adding them to the provided RTCConfiguration object. + */ export const setTurns = ( - turnServers: any[], + turnServers: TurnServer[], rtcConfig: RTCConfiguration, ): void => { - turnServers.forEach((turnServer: any) => { - let transport, uri; - if (turnServer.transport == 'tls') { - transport = 'tcp'; - uri = 'turns'; - } else { - transport = turnServer.transport; - uri = 'turn'; - } - - const rtcIceServer: RTCIceServer = { - credential: turnServer.password, - urls: uri.concat( - ':', - turnServer.serverAddr, - ':', - turnServer.serverPort, - '?transport=', - transport, - ), - username: turnServer.username, - }; + turnServers + .map((turnServer: TurnServer) => { + const transport = + turnServer.transport === 'tls' ? 'tcp' : turnServer.transport; + const uri = turnServer.transport === 'tls' ? 'turns' : 'turn'; + const address = turnServer.serverAddr; + const port = turnServer.serverPort; - rtcConfig.iceServers!.push(rtcIceServer); - }); + return { + credential: turnServer.password, + urls: uri.concat(':', address, ':', port, '?transport=', transport), + username: turnServer.username, + } satisfies RTCIceServer; + }) + .forEach((rtcIceServer) => { + rtcConfig.iceServers!.push(rtcIceServer); + }); }; From 65d36ea5ad04060fdab36b2dfbda2eb936bd53ac Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Tue, 9 Jul 2024 14:53:41 +0200 Subject: [PATCH 48/50] Add readonly to CommandQueue field --- packages/ts-client/src/webrtc/CommandsQueue.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ts-client/src/webrtc/CommandsQueue.ts b/packages/ts-client/src/webrtc/CommandsQueue.ts index 99446e9b..9ce87342 100644 --- a/packages/ts-client/src/webrtc/CommandsQueue.ts +++ b/packages/ts-client/src/webrtc/CommandsQueue.ts @@ -5,7 +5,7 @@ import type { NegotiationManager } from './NegotiationManager'; export class CommandsQueue { private readonly stateManager: StateManager; - private negotiationManager: NegotiationManager; + private readonly negotiationManager: NegotiationManager; private clearConnectionCallbacks: (() => void) | null = null; From adbab5a3af6844c48022dac6ca175d8c1448fd56 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Wed, 10 Jul 2024 11:20:53 +0200 Subject: [PATCH 49/50] Apply PR suggestions --- packages/ts-client/src/webrtc/StateManager.ts | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index 9b44e230..ef7a3d1e 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -206,21 +206,21 @@ export class StateManager { this.midToTrackId = new Map(Object.entries(data.midToTrackId)); for (const trackId of Object.values(data.midToTrackId)) { - const track = this.localTrackIdToTrack.get(trackId as string); + const trackContext = this.localTrackIdToTrack.get(trackId as string); - // if is local track - if (track) { - track.negotiationStatus = 'done'; + // if is local trackContext + if (trackContext) { + trackContext.negotiationStatus = 'done'; - if (track.pendingMetadataUpdate) { + if (trackContext.pendingMetadataUpdate) { const mediaEvent = generateMediaEvent('updateTrackMetadata', { trackId, - trackMetadata: track.metadata, + trackMetadata: trackContext.metadata, }); this.webrtc.sendMediaEvent(mediaEvent); } - track.pendingMetadataUpdate = false; + trackContext.pendingMetadataUpdate = false; } } @@ -263,9 +263,8 @@ export class StateManager { }; public onEndpointRemoved = (data: any) => { - const endpoint: EndpointWithTrackContext = - this.idToEndpoint.get(data.id)!; - if (endpoint === undefined) return; + const endpoint: EndpointWithTrackContext | undefined = this.idToEndpoint.get(data.id); + if (!endpoint) return; Array.from(endpoint.tracks.keys()).forEach((trackId) => { this.webrtc.emit('trackRemoved', this.trackIdToTrack.get(trackId)!); @@ -279,11 +278,10 @@ export class StateManager { public onTrackUpdated = (data: any) => { if (this.getEndpointId() === data.endpointId) return; - const endpoint: EndpointWithTrackContext = - this.idToEndpoint.get(data.endpointId)!; + const endpoint: EndpointWithTrackContext | undefined = + this.idToEndpoint.get(data.endpointId); - if (endpoint == null) - throw `Endpoint with id: ${data.endpointId} doesn't exist`; + if (!endpoint) throw `Endpoint with id: ${data.endpointId} doesn't exist`; const trackId = data.trackId; const trackMetadata = data.metadata; @@ -321,8 +319,7 @@ export class StateManager { const endpoint: EndpointWithTrackContext = this.idToEndpoint.get(data.endpointId)!; - if (endpoint == null) - throw `Endpoint with id: ${data.endpointId} doesn't exist`; + if (!endpoint) throw `Endpoint with id: ${data.endpointId} doesn't exist`; const trackId = data.trackId; const encoding = data.encoding; @@ -338,8 +335,7 @@ export class StateManager { const endpoint: EndpointWithTrackContext = this.idToEndpoint.get(data.endpointId)!; - if (endpoint == null) - throw `Endpoint with id: ${data.endpointId} doesn't exist`; + if (!endpoint) throw `Endpoint with id: ${data.endpointId} doesn't exist`; const trackId = data.trackId; const encoding = data.encoding; @@ -360,11 +356,12 @@ export class StateManager { public onVadNotification = (data: any) => { const trackId = data.trackId; - const ctx = this.trackIdToTrack.get(trackId)!; + const trackContext = this.trackIdToTrack.get(trackId)!; + const vadStatus = data.status; if (isVadStatus(vadStatus)) { - ctx.vadStatus = vadStatus; - ctx.emit('voiceActivityChanged', ctx); + trackContext.vadStatus = vadStatus; + trackContext.emit('voiceActivityChanged', trackContext); } else { console.warn('Received unknown vad status: ', vadStatus); } From 6f7d0201b203bf809d137366d248be895575a8d9 Mon Sep 17 00:00:00 2001 From: Kamil Stasiak Date: Wed, 10 Jul 2024 11:30:20 +0200 Subject: [PATCH 50/50] Fix lint and format --- packages/ts-client/src/webrtc/StateManager.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/ts-client/src/webrtc/StateManager.ts b/packages/ts-client/src/webrtc/StateManager.ts index ef7a3d1e..fffafe55 100644 --- a/packages/ts-client/src/webrtc/StateManager.ts +++ b/packages/ts-client/src/webrtc/StateManager.ts @@ -263,7 +263,9 @@ export class StateManager { }; public onEndpointRemoved = (data: any) => { - const endpoint: EndpointWithTrackContext | undefined = this.idToEndpoint.get(data.id); + const endpoint: + | EndpointWithTrackContext + | undefined = this.idToEndpoint.get(data.id); if (!endpoint) return; Array.from(endpoint.tracks.keys()).forEach((trackId) => { @@ -278,8 +280,9 @@ export class StateManager { public onTrackUpdated = (data: any) => { if (this.getEndpointId() === data.endpointId) return; - const endpoint: EndpointWithTrackContext | undefined = - this.idToEndpoint.get(data.endpointId); + const endpoint: + | EndpointWithTrackContext + | undefined = this.idToEndpoint.get(data.endpointId); if (!endpoint) throw `Endpoint with id: ${data.endpointId} doesn't exist`;