@epicgames-ps/lib-pixelstreamingsignalling-ue5.8 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/.lintstagedrc.mjs +7 -0
  2. package/.prettierignore +0 -0
  3. package/CHANGELOG.md +21 -0
  4. package/README.md +32 -0
  5. package/dist/cjs/Logger.js +148 -0
  6. package/dist/cjs/Logger.js.map +1 -0
  7. package/dist/cjs/LoggingUtils.js +64 -0
  8. package/dist/cjs/LoggingUtils.js.map +1 -0
  9. package/dist/cjs/PlayerConnection.js +220 -0
  10. package/dist/cjs/PlayerConnection.js.map +1 -0
  11. package/dist/cjs/PlayerRegistry.js +77 -0
  12. package/dist/cjs/PlayerRegistry.js.map +1 -0
  13. package/dist/cjs/SFUConnection.js +264 -0
  14. package/dist/cjs/SFUConnection.js.map +1 -0
  15. package/dist/cjs/SignallingServer.js +149 -0
  16. package/dist/cjs/SignallingServer.js.map +1 -0
  17. package/dist/cjs/StreamerConnection.js +172 -0
  18. package/dist/cjs/StreamerConnection.js.map +1 -0
  19. package/dist/cjs/StreamerRegistry.js +149 -0
  20. package/dist/cjs/StreamerRegistry.js.map +1 -0
  21. package/dist/cjs/Utils.js +19 -0
  22. package/dist/cjs/Utils.js.map +1 -0
  23. package/dist/cjs/WebServer.js +124 -0
  24. package/dist/cjs/WebServer.js.map +1 -0
  25. package/dist/cjs/pixelstreamingsignalling.js +28 -0
  26. package/dist/cjs/pixelstreamingsignalling.js.map +1 -0
  27. package/dist/esm/Logger.js +141 -0
  28. package/dist/esm/Logger.js.map +1 -0
  29. package/dist/esm/LoggingUtils.js +58 -0
  30. package/dist/esm/LoggingUtils.js.map +1 -0
  31. package/dist/esm/PlayerConnection.js +183 -0
  32. package/dist/esm/PlayerConnection.js.map +1 -0
  33. package/dist/esm/PlayerRegistry.js +73 -0
  34. package/dist/esm/PlayerRegistry.js.map +1 -0
  35. package/dist/esm/SFUConnection.js +227 -0
  36. package/dist/esm/SFUConnection.js.map +1 -0
  37. package/dist/esm/SignallingServer.js +112 -0
  38. package/dist/esm/SignallingServer.js.map +1 -0
  39. package/dist/esm/StreamerConnection.js +135 -0
  40. package/dist/esm/StreamerConnection.js.map +1 -0
  41. package/dist/esm/StreamerRegistry.js +145 -0
  42. package/dist/esm/StreamerRegistry.js.map +1 -0
  43. package/dist/esm/Utils.js +15 -0
  44. package/dist/esm/Utils.js.map +1 -0
  45. package/dist/esm/WebServer.js +117 -0
  46. package/dist/esm/WebServer.js.map +1 -0
  47. package/dist/esm/pixelstreamingsignalling.js +12 -0
  48. package/dist/esm/pixelstreamingsignalling.js.map +1 -0
  49. package/dist/types/Logger.d.ts +20 -0
  50. package/dist/types/LoggingUtils.d.ts +44 -0
  51. package/dist/types/PlayerConnection.d.ts +66 -0
  52. package/dist/types/PlayerRegistry.d.ts +66 -0
  53. package/dist/types/SFUConnection.d.ts +85 -0
  54. package/dist/types/SignallingServer.d.ts +48 -0
  55. package/dist/types/StreamerConnection.d.ts +61 -0
  56. package/dist/types/StreamerRegistry.d.ts +96 -0
  57. package/dist/types/Utils.d.ts +8 -0
  58. package/dist/types/WebServer.d.ts +42 -0
  59. package/dist/types/pixelstreamingsignalling.d.ts +9 -0
  60. package/docs/Logger/README.md +19 -0
  61. package/docs/Logger/functions/InitLogging.md +26 -0
  62. package/docs/Logger/interfaces/IConfig.md +41 -0
  63. package/docs/Logger/variables/Logger.md +15 -0
  64. package/docs/LoggingUtils/README.md +19 -0
  65. package/docs/LoggingUtils/functions/createHandlerListener.md +42 -0
  66. package/docs/LoggingUtils/functions/logForward.md +36 -0
  67. package/docs/LoggingUtils/functions/logIncoming.md +30 -0
  68. package/docs/LoggingUtils/functions/logOutgoing.md +30 -0
  69. package/docs/LoggingUtils/interfaces/IMessageLogger.md +29 -0
  70. package/docs/LoggingUtils/interfaces/IProtoLogObj.md +57 -0
  71. package/docs/PlayerConnection/README.md +11 -0
  72. package/docs/PlayerConnection/classes/PlayerConnection.md +177 -0
  73. package/docs/PlayerRegistry/README.md +16 -0
  74. package/docs/PlayerRegistry/classes/PlayerRegistry.md +423 -0
  75. package/docs/PlayerRegistry/interfaces/IPlayer.md +86 -0
  76. package/docs/PlayerRegistry/interfaces/IPlayerInfo.md +43 -0
  77. package/docs/README.md +19 -0
  78. package/docs/SFUConnection/README.md +11 -0
  79. package/docs/SFUConnection/classes/SFUConnection.md +554 -0
  80. package/docs/SignallingServer/README.md +19 -0
  81. package/docs/SignallingServer/classes/SignallingServer.md +76 -0
  82. package/docs/SignallingServer/interfaces/IServerConfig.md +92 -0
  83. package/docs/SignallingServer/type-aliases/ProtocolConfig.md +15 -0
  84. package/docs/StreamerConnection/README.md +11 -0
  85. package/docs/StreamerConnection/classes/StreamerConnection.md +507 -0
  86. package/docs/StreamerRegistry/README.md +16 -0
  87. package/docs/StreamerRegistry/classes/StreamerRegistry.md +431 -0
  88. package/docs/StreamerRegistry/interfaces/IStreamer.md +375 -0
  89. package/docs/StreamerRegistry/interfaces/IStreamerInfo.md +51 -0
  90. package/docs/Utils/README.md +12 -0
  91. package/docs/Utils/functions/beautify.md +23 -0
  92. package/docs/Utils/functions/stringify.md +23 -0
  93. package/docs/WebServer/README.md +15 -0
  94. package/docs/WebServer/classes/WebServer.md +50 -0
  95. package/docs/WebServer/interfaces/IWebServerConfig.md +76 -0
  96. package/docs/pixelstreamingsignalling/README.md +145 -0
  97. package/eslint.config.mjs +39 -0
  98. package/package.json +58 -0
  99. package/src/Logger.ts +189 -0
  100. package/src/LoggingUtils.ts +80 -0
  101. package/src/PlayerConnection.ts +257 -0
  102. package/src/PlayerRegistry.ts +116 -0
  103. package/src/SFUConnection.ts +329 -0
  104. package/src/SignallingServer.ts +214 -0
  105. package/src/StreamerConnection.ts +194 -0
  106. package/src/StreamerRegistry.ts +252 -0
  107. package/src/Utils.ts +16 -0
  108. package/src/WebServer.ts +213 -0
  109. package/src/pixelstreamingsignalling.ts +11 -0
  110. package/tsconfig.cjs.json +8 -0
  111. package/tsconfig.esm.json +8 -0
  112. package/tsconfig.json +29 -0
@@ -0,0 +1,257 @@
1
+ // Copyright Epic Games, Inc. All Rights Reserved.
2
+ import type { IncomingMessage } from 'http';
3
+ import WebSocket from 'ws';
4
+ import {
5
+ ITransport,
6
+ WebSocketTransportNJS,
7
+ SignallingProtocol,
8
+ MessageHelpers,
9
+ Messages,
10
+ BaseMessage
11
+ } from '@epicgames-ps/lib-pixelstreamingcommon-ue5.8';
12
+ import { IPlayer, IPlayerInfo } from './PlayerRegistry';
13
+ import { IStreamer } from './StreamerRegistry';
14
+ import { Logger } from './Logger';
15
+ import * as LogUtils from './LoggingUtils';
16
+ import { SignallingServer } from './SignallingServer';
17
+
18
+ /**
19
+ * A connection between the signalling server and a player connection.
20
+ * This is where messages expected to be handled by the player come in
21
+ * and where messages are sent to the player.
22
+ *
23
+ * Interesting internals:
24
+ * playerId: The unique id string of this player.
25
+ * transport: The ITransport where transport events can be subscribed to
26
+ * protocol: The SignallingProtocol where signalling messages can be
27
+ * subscribed to.
28
+ */
29
+ export class PlayerConnection implements IPlayer, LogUtils.IMessageLogger {
30
+ // The unique id of this player connection.
31
+ playerId: string;
32
+ // The websocket transport used by this connection.
33
+ transport: ITransport;
34
+ // The protocol abstraction on this connection. Used for sending/receiving signalling messages.
35
+ protocol: SignallingProtocol;
36
+ // When the player is subscribed to a streamer this will be the streamer being subscribed to.
37
+ subscribedStreamer: IStreamer | null;
38
+ // A descriptive string describing the remote address of this connection.
39
+ remoteAddress?: string;
40
+ // The HTTP upgrade request that opened this connection, if available.
41
+ request?: IncomingMessage;
42
+
43
+ private server: SignallingServer;
44
+ private streamerIdChangeListener: (newId: string) => void;
45
+ private streamerDisconnectedListener: () => void;
46
+
47
+ /**
48
+ * Initializes a new connection with given and sane values. Adds listeners for the
49
+ * websocket close and error so it can react by unsubscribing and resetting itself.
50
+ * @param server - The signalling server object that spawned this player.
51
+ * @param ws - The websocket coupled to this player connection.
52
+ * @param remoteAddress - The remote address of this connection. Only used as display.
53
+ * @param request - The HTTP upgrade request that opened this connection, if available.
54
+ */
55
+ constructor(server: SignallingServer, ws: WebSocket, remoteAddress?: string, request?: IncomingMessage) {
56
+ this.server = server;
57
+ this.playerId = '';
58
+ this.subscribedStreamer = null;
59
+ this.transport = new WebSocketTransportNJS(ws);
60
+ this.protocol = new SignallingProtocol(this.transport);
61
+ this.remoteAddress = remoteAddress;
62
+ this.request = request;
63
+
64
+ this.transport.on('error', this.onTransportError.bind(this));
65
+ this.transport.on('close', this.onTransportClose.bind(this));
66
+
67
+ this.streamerIdChangeListener = this.onStreamerIdChanged.bind(this);
68
+ this.streamerDisconnectedListener = this.onStreamerDisconnected.bind(this);
69
+
70
+ this.registerMessageHandlers();
71
+ }
72
+
73
+ /**
74
+ * Returns an identifier that is displayed in logs.
75
+ * @returns A string describing this connection.
76
+ */
77
+ getReadableIdentifier(): string {
78
+ return this.playerId;
79
+ }
80
+
81
+ /**
82
+ * Sends a signalling message to the player.
83
+ * @param message - The message to send.
84
+ */
85
+ sendMessage(message: BaseMessage): void {
86
+ LogUtils.logOutgoing(this, message);
87
+ this.protocol.sendMessage(message);
88
+ }
89
+
90
+ /**
91
+ * Returns a descriptive object for the REST API inspection operations.
92
+ * @returns An IPlayerInfo object containing viewable information about this connection.
93
+ */
94
+ getPlayerInfo(): IPlayerInfo {
95
+ return {
96
+ playerId: this.playerId,
97
+ type: 'Player',
98
+ subscribedTo: this.subscribedStreamer?.streamerId,
99
+ remoteAddress: this.remoteAddress
100
+ };
101
+ }
102
+
103
+ private registerMessageHandlers(): void {
104
+ /* eslint-disable @typescript-eslint/unbound-method */
105
+ this.protocol.on(
106
+ Messages.subscribe.typeName,
107
+ LogUtils.createHandlerListener(this, this.onSubscribeMessage)
108
+ );
109
+ this.protocol.on(
110
+ Messages.unsubscribe.typeName,
111
+ LogUtils.createHandlerListener(this, this.onUnsubscribeMessage)
112
+ );
113
+ this.protocol.on(
114
+ Messages.listStreamers.typeName,
115
+ LogUtils.createHandlerListener(this, this.onListStreamers)
116
+ );
117
+ this.protocol.on(Messages.ping.typeName, LogUtils.createHandlerListener(this, this.onPingMessage));
118
+ /* eslint-enable @typescript-eslint/unbound-method */
119
+
120
+ this.protocol.on(Messages.offer.typeName, this.sendToStreamer.bind(this));
121
+ this.protocol.on(Messages.answer.typeName, this.sendToStreamer.bind(this));
122
+ this.protocol.on(Messages.iceCandidate.typeName, this.sendToStreamer.bind(this));
123
+ this.protocol.on(Messages.dataChannelRequest.typeName, this.sendToStreamer.bind(this));
124
+ this.protocol.on(Messages.peerDataChannelsReady.typeName, this.sendToStreamer.bind(this));
125
+ this.protocol.on(Messages.layerPreference.typeName, this.sendToStreamer.bind(this));
126
+
127
+ this.protocol.on('unhandled', (message: BaseMessage) => {
128
+ Logger.warn(`Unhandled player protocol message: ${JSON.stringify(message)}`);
129
+ });
130
+ }
131
+
132
+ private sendToStreamer(message: BaseMessage): void {
133
+ if (!this.subscribedStreamer) {
134
+ Logger.warn(
135
+ `Player ${this.playerId} tried to send to a streamer but they're not subscribed to any.`
136
+ );
137
+ const streamerId = this.server.streamerRegistry.getFirstStreamerId();
138
+ if (!streamerId) {
139
+ Logger.error('There are no streamers to force a subscription. Disconnecting.');
140
+ this.disconnect();
141
+ return;
142
+ } else {
143
+ Logger.warn(`Subscribing to ${streamerId}`);
144
+ this.subscribe(streamerId);
145
+ }
146
+ }
147
+
148
+ message.playerId = this.playerId;
149
+ LogUtils.logForward(this, this.subscribedStreamer!, message);
150
+ this.subscribedStreamer!.protocol.sendMessage(message);
151
+ }
152
+
153
+ private subscribe(streamerId: string) {
154
+ const streamer = this.server.streamerRegistry.find(streamerId);
155
+ if (!streamer) {
156
+ Logger.error(
157
+ `subscribe: Player ${this.playerId} tried to subscribe to a non-existent streamer ${streamerId}`
158
+ );
159
+ const failureMessage = MessageHelpers.createMessage(Messages.subscribeFailed, {
160
+ message: `Streamer ${streamerId} does not exist.`
161
+ });
162
+ this.protocol.sendMessage(failureMessage);
163
+ return;
164
+ }
165
+
166
+ if (this.subscribedStreamer) {
167
+ Logger.warn(
168
+ `subscribe: Player ${this.playerId} is resubscribing to a streamer but is already subscribed to ${this.subscribedStreamer.streamerId}`
169
+ );
170
+ this.unsubscribe();
171
+ }
172
+
173
+ if (streamer.maxSubscribers > 0 && streamer.subscribers.size >= streamer.maxSubscribers) {
174
+ Logger.error(
175
+ `subscribe: Player ${this.playerId} could not subscribe to ${streamerId}. Max players (${streamer.maxSubscribers}) reached.`
176
+ );
177
+ const failureMessage = MessageHelpers.createMessage(Messages.subscribeFailed, {
178
+ message: `Streamer ${streamerId} is full. Max players = ${streamer.maxSubscribers}.`
179
+ });
180
+ this.protocol.sendMessage(failureMessage);
181
+ return;
182
+ }
183
+
184
+ this.subscribedStreamer = streamer;
185
+ this.subscribedStreamer.subscribers.add(this.playerId);
186
+ this.subscribedStreamer.on('id_changed', this.streamerIdChangeListener);
187
+ this.subscribedStreamer.on('disconnect', this.streamerDisconnectedListener);
188
+
189
+ const connectedMessage = MessageHelpers.createMessage(Messages.playerConnected, {
190
+ playerId: this.playerId,
191
+ dataChannel: true,
192
+ sfu: false
193
+ });
194
+ this.sendToStreamer(connectedMessage);
195
+ }
196
+
197
+ private unsubscribe() {
198
+ if (!this.subscribedStreamer) {
199
+ return;
200
+ }
201
+
202
+ this.subscribedStreamer.subscribers.delete(this.playerId);
203
+
204
+ const disconnectedMessage = MessageHelpers.createMessage(Messages.playerDisconnected, {
205
+ playerId: this.playerId
206
+ });
207
+ this.sendToStreamer(disconnectedMessage);
208
+
209
+ this.subscribedStreamer.off('id_changed', this.streamerIdChangeListener);
210
+ this.subscribedStreamer.off('disconnect', this.streamerDisconnectedListener);
211
+ this.subscribedStreamer = null;
212
+ }
213
+
214
+ private disconnect() {
215
+ this.unsubscribe();
216
+ this.protocol.disconnect();
217
+ }
218
+
219
+ private onStreamerDisconnected(): void {
220
+ this.disconnect();
221
+ }
222
+
223
+ private onTransportError(error: ErrorEvent): void {
224
+ Logger.error(`Player (${this.playerId}) transport error ${error.message}`);
225
+ }
226
+
227
+ private onTransportClose(_event: CloseEvent): void {
228
+ Logger.debug('PlayerConnection transport close.');
229
+ this.disconnect();
230
+ }
231
+
232
+ private onSubscribeMessage(message: Messages.subscribe): void {
233
+ this.subscribe(message.streamerId);
234
+ }
235
+
236
+ private onUnsubscribeMessage(_message: Messages.unsubscribe): void {
237
+ this.unsubscribe();
238
+ }
239
+
240
+ private onListStreamers(_message: Messages.listStreamers): void {
241
+ const listMessage = MessageHelpers.createMessage(Messages.streamerList, {
242
+ ids: this.server.streamerRegistry.streamers
243
+ .filter((streamer) => streamer.streaming)
244
+ .map((streamer) => streamer.streamerId)
245
+ });
246
+ this.sendMessage(listMessage);
247
+ }
248
+
249
+ private onStreamerIdChanged(newId: string) {
250
+ const renameMessage = MessageHelpers.createMessage(Messages.streamerIdChanged, { newID: newId });
251
+ this.sendMessage(renameMessage);
252
+ }
253
+
254
+ private onPingMessage(message: Messages.ping): void {
255
+ this.sendMessage(MessageHelpers.createMessage(Messages.pong, { time: message.time }));
256
+ }
257
+ }
@@ -0,0 +1,116 @@
1
+ // Copyright Epic Games, Inc. All Rights Reserved.
2
+ import type { IncomingMessage } from 'http';
3
+ import { SignallingProtocol, BaseMessage, EventEmitter } from '@epicgames-ps/lib-pixelstreamingcommon-ue5.8';
4
+ import { Logger } from './Logger';
5
+ import { IMessageLogger } from './LoggingUtils';
6
+ import { IStreamer } from './StreamerRegistry';
7
+
8
+ /**
9
+ * An interface that describes a player that can be added to the
10
+ * player registry.
11
+ */
12
+ export interface IPlayer extends IMessageLogger {
13
+ playerId: string;
14
+ protocol: SignallingProtocol;
15
+ subscribedStreamer: IStreamer | null;
16
+ // The HTTP upgrade request that opened this connection, if available. Lets a consumer-supplied
17
+ // verifyClient (or other front door) attach an authenticated identity to the request and
18
+ // recover it here. The signalling server itself does not read this.
19
+ request?: IncomingMessage;
20
+
21
+ sendMessage(message: BaseMessage): void;
22
+ getPlayerInfo(): IPlayerInfo;
23
+ }
24
+
25
+ /**
26
+ * Used by the API to describe the current state of the player.
27
+ */
28
+ export interface IPlayerInfo {
29
+ playerId: string;
30
+ type: string;
31
+ subscribedTo: string | undefined;
32
+ remoteAddress: string | undefined;
33
+ }
34
+
35
+ /**
36
+ * Handles all the player connections of a signalling server and
37
+ * can be used to lookup connections by id etc.
38
+ * Fires events when players are added or removed.
39
+ * Events:
40
+ * 'added': (playerId: string) Player was added.
41
+ * 'removed': (playerId: string) Player was removed.
42
+ */
43
+ export class PlayerRegistry extends EventEmitter {
44
+ private players: Map<string, IPlayer> = new Map();
45
+ private nextPlayerId: number;
46
+
47
+ constructor() {
48
+ super();
49
+ this.players = new Map();
50
+ this.nextPlayerId = 0;
51
+ }
52
+
53
+ /**
54
+ * Assigns a unique id to the player and adds it to the registry
55
+ */
56
+ add(player: IPlayer): void {
57
+ player.playerId = this.getUniquePlayerId();
58
+ this.players.set(player.playerId, player);
59
+ this.emit('added', player.playerId);
60
+ Logger.info(`Registered new player: ${player.playerId}`);
61
+ }
62
+
63
+ /**
64
+ * Removes a player from the registry. Does nothing if the id
65
+ * does not exist.
66
+ */
67
+ remove(player: IPlayer): void {
68
+ if (!this.players.has(player.playerId)) {
69
+ return;
70
+ }
71
+
72
+ this.emit('removed', player.playerId);
73
+ this.players.delete(player.playerId);
74
+
75
+ Logger.info(`Unregistered player: ${player.playerId}`);
76
+ }
77
+
78
+ /**
79
+ * Tests if a player id exists in the registry.
80
+ */
81
+ has(playerId: string): boolean {
82
+ return this.players.has(playerId);
83
+ }
84
+
85
+ /**
86
+ * Gets a player from the registry using the player id.
87
+ * Returns undefined if the player doesn't exist.
88
+ */
89
+ get(playerId: string): IPlayer | undefined {
90
+ return this.players.get(playerId);
91
+ }
92
+
93
+ listPlayers(): IPlayer[] {
94
+ return Array.from(this.players.values());
95
+ }
96
+
97
+ /**
98
+ * Returns true when the registry is empty.
99
+ */
100
+ empty(): boolean {
101
+ return this.players.size === 0;
102
+ }
103
+
104
+ /**
105
+ * Gets the total number of connected players.
106
+ */
107
+ count(): number {
108
+ return this.players.size;
109
+ }
110
+
111
+ private getUniquePlayerId(): string {
112
+ const newPlayerId = `Player${this.nextPlayerId}`;
113
+ this.nextPlayerId++;
114
+ return newPlayerId;
115
+ }
116
+ }
@@ -0,0 +1,329 @@
1
+ // Copyright Epic Games, Inc. All Rights Reserved.
2
+ import type { IncomingMessage } from 'http';
3
+ import WebSocket from 'ws';
4
+ import {
5
+ ITransport,
6
+ WebSocketTransportNJS,
7
+ SignallingProtocol,
8
+ MessageHelpers,
9
+ Messages,
10
+ BaseMessage,
11
+ EventEmitter
12
+ } from '@epicgames-ps/lib-pixelstreamingcommon-ue5.8';
13
+ import { IPlayer, IPlayerInfo } from './PlayerRegistry';
14
+ import { IStreamer, IStreamerInfo } from './StreamerRegistry';
15
+ import { Logger } from './Logger';
16
+ import * as LogUtils from './LoggingUtils';
17
+ import { SignallingServer } from './SignallingServer';
18
+
19
+ /**
20
+ * A SFU connection to the signalling server.
21
+ * An SFU can act as both a streamer and a player. It can subscribe to
22
+ * streamers like a player, and other players can subscribe to the sfu.
23
+ * Therefore the SFU will have a streamer id and a player id and be
24
+ * registered in both streamer registries and player registries.
25
+ *
26
+ * Interesting internals:
27
+ * playerId: The player id of this connectiom.
28
+ * streamerId: The streamer id of this connection.
29
+ * transport: The ITransport where transport events can be subscribed to
30
+ * protocol: The SignallingProtocol where signalling messages can be
31
+ * subscribed to.
32
+ * streaming: True when the streamer is ready to accept subscriptions.
33
+ */
34
+ export class SFUConnection extends EventEmitter implements IPlayer, IStreamer, LogUtils.IMessageLogger {
35
+ // The player id related to this SFU connection.
36
+ playerId: string;
37
+ // The streamer id related to this SFU connection.
38
+ streamerId: string;
39
+ // The websocket transport used by this connection.
40
+ transport: ITransport;
41
+ // The protocol abstraction on this connection. Used for sending/receiving signalling messages.
42
+ protocol: SignallingProtocol;
43
+ // True when this SFU is subscribed to another streamer and is ready to be subscribed to itself.
44
+ streaming: boolean;
45
+ // When the player is subscribed to a streamer this will be the streamer being subscribed to.
46
+ subscribedStreamer: IStreamer | null;
47
+ // A descriptive string describing the remote address of this connection.
48
+ remoteAddress?: string;
49
+ // The HTTP upgrade request that opened this connection, if available.
50
+ request?: IncomingMessage;
51
+ // The max number of subscribed players at a time. A value of 0 means there is no limit (this the default).
52
+ maxSubscribers: number;
53
+ // A list of all the current subscribed players.
54
+ subscribers: Set<string>;
55
+
56
+ private server: SignallingServer;
57
+ private layerPreferenceListener: (message: Messages.layerPreference) => void;
58
+ private streamerIdChangeListener: (newId: string) => void;
59
+ private streamerDisconnectedListener: () => void;
60
+
61
+ /**
62
+ * Construct a new SFU connection.
63
+ * @param server - The signalling server object that spawned this sfu.
64
+ * @param ws - The websocket coupled to this sfu connection.
65
+ * @param remoteAddress - The remote address of this connection. Only used as display.
66
+ * @param request - The HTTP upgrade request that opened this connection, if available.
67
+ */
68
+ constructor(server: SignallingServer, ws: WebSocket, remoteAddress?: string, request?: IncomingMessage) {
69
+ super();
70
+
71
+ this.server = server;
72
+ this.transport = new WebSocketTransportNJS(ws);
73
+ this.protocol = new SignallingProtocol(this.transport);
74
+ this.playerId = '';
75
+ this.streamerId = '';
76
+ this.streaming = false;
77
+ this.remoteAddress = remoteAddress;
78
+ this.request = request;
79
+ this.subscribedStreamer = null;
80
+ this.maxSubscribers = 0;
81
+ this.subscribers = new Set();
82
+
83
+ this.transport.on('error', this.onTransportError.bind(this));
84
+ this.transport.on('close', this.onTransportClose.bind(this));
85
+
86
+ this.layerPreferenceListener = this.onLayerPreference.bind(this);
87
+ this.streamerIdChangeListener = this.onStreamerIdChanged.bind(this);
88
+ this.streamerDisconnectedListener = this.onStreamerDisconnected.bind(this);
89
+
90
+ this.registerMessageHandlers();
91
+ }
92
+
93
+ /**
94
+ * Returns an identifier that is displayed in logs.
95
+ * @returns A string describing this connection.
96
+ */
97
+ getReadableIdentifier(): string {
98
+ return `(${this.streamerId}:${this.playerId})`;
99
+ }
100
+
101
+ /**
102
+ * Sends a signalling message to the SFU.
103
+ * @param message - The message to send.
104
+ */
105
+ sendMessage(message: BaseMessage): void {
106
+ LogUtils.logOutgoing(this, message);
107
+ this.protocol.sendMessage(message);
108
+ }
109
+
110
+ /**
111
+ * Returns a descriptive object for the REST API inspection operations.
112
+ * @returns An IStreamerInfo object containing viewable information about this connection.
113
+ */
114
+ getStreamerInfo(): IStreamerInfo {
115
+ return {
116
+ streamerId: this.streamerId,
117
+ type: 'SFU',
118
+ streaming: this.streaming,
119
+ remoteAddress: this.remoteAddress,
120
+ subscribers: this.server.playerRegistry
121
+ .listPlayers()
122
+ .filter((player) => player.subscribedStreamer == this)
123
+ .map((player) => player.getPlayerInfo())
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Returns a descriptive object for the REST API inspection operations.
129
+ * @returns An IPlayerInfo object containing viewable information about this connection.
130
+ */
131
+ getPlayerInfo(): IPlayerInfo {
132
+ return {
133
+ playerId: this.playerId,
134
+ type: 'SFU',
135
+ remoteAddress: this.remoteAddress,
136
+ subscribedTo: this.subscribedStreamer?.streamerId
137
+ };
138
+ }
139
+
140
+ private registerMessageHandlers(): void {
141
+ /* eslint-disable @typescript-eslint/unbound-method */
142
+ this.protocol.on(
143
+ Messages.subscribe.typeName,
144
+ LogUtils.createHandlerListener(this, this.onSubscribeMessage)
145
+ );
146
+ this.protocol.on(
147
+ Messages.unsubscribe.typeName,
148
+ LogUtils.createHandlerListener(this, this.onUnsubscribeMessage)
149
+ );
150
+ this.protocol.on(
151
+ Messages.listStreamers.typeName,
152
+ LogUtils.createHandlerListener(this, this.onListStreamers)
153
+ );
154
+ this.protocol.on(
155
+ Messages.endpointId.typeName,
156
+ LogUtils.createHandlerListener(this, this.onEndpointId)
157
+ );
158
+ this.protocol.on(
159
+ Messages.streamerDataChannels.typeName,
160
+ LogUtils.createHandlerListener(this, this.onStreamerDataChannels)
161
+ );
162
+ this.protocol.on(
163
+ Messages.startStreaming.typeName,
164
+ LogUtils.createHandlerListener(this, this.onStartStreaming)
165
+ );
166
+ this.protocol.on(
167
+ Messages.stopStreaming.typeName,
168
+ LogUtils.createHandlerListener(this, this.onStopStreaming)
169
+ );
170
+ this.protocol.on(Messages.ping.typeName, LogUtils.createHandlerListener(this, this.onPingMessage));
171
+ /* eslint-enable @typescript-eslint/unbound-method */
172
+
173
+ this.protocol.on(Messages.offer.typeName, this.sendToPlayer.bind(this));
174
+ this.protocol.on(Messages.answer.typeName, this.sendToStreamer.bind(this));
175
+ this.protocol.on(Messages.peerDataChannels.typeName, this.sendToPlayer.bind(this));
176
+ }
177
+
178
+ private subscribe(streamerId: string) {
179
+ const streamer = this.server.streamerRegistry.find(streamerId);
180
+ if (!streamer) {
181
+ Logger.error(
182
+ `subscribe: SFU ${this.playerId} tried to subscribe to a non-existent streamer ${streamerId}`
183
+ );
184
+ return;
185
+ }
186
+
187
+ this.subscribedStreamer = streamer;
188
+ // Register as a subscriber of the upstream streamer so it will forward signalling messages
189
+ // back to this SFU (the streamer's forwardMessage only sends to subscribed players).
190
+ this.subscribedStreamer.subscribers.add(this.playerId);
191
+ this.subscribedStreamer.on('layer_preference', this.layerPreferenceListener);
192
+ this.subscribedStreamer.on('id_changed', this.streamerIdChangeListener);
193
+ this.subscribedStreamer.on('disconnect', this.streamerDisconnectedListener);
194
+
195
+ const connectedMessage = MessageHelpers.createMessage(Messages.playerConnected, {
196
+ playerId: this.playerId,
197
+ dataChannel: true,
198
+ sfu: true
199
+ });
200
+ this.sendToStreamer(connectedMessage);
201
+ }
202
+
203
+ private unsubscribe() {
204
+ if (!this.subscribedStreamer) {
205
+ return;
206
+ }
207
+
208
+ const disconnectedMessage = MessageHelpers.createMessage(Messages.playerDisconnected, {
209
+ playerId: this.playerId
210
+ });
211
+ this.sendToStreamer(disconnectedMessage);
212
+
213
+ this.subscribedStreamer.subscribers.delete(this.playerId);
214
+ this.subscribedStreamer.off('layer_preference', this.layerPreferenceListener);
215
+ this.subscribedStreamer.off('id_changed', this.streamerIdChangeListener);
216
+ this.subscribedStreamer.off('disconnect', this.streamerDisconnectedListener);
217
+ this.subscribedStreamer = null;
218
+ }
219
+
220
+ private sendToStreamer(message: BaseMessage): void {
221
+ if (!this.subscribedStreamer) {
222
+ Logger.error(
223
+ `SFU ${this.playerId} tried to send to a streamer but they're not subscribed to any.`
224
+ );
225
+ return;
226
+ }
227
+
228
+ LogUtils.logForward(this, this.subscribedStreamer, message);
229
+
230
+ // normally we want to indicate what player this message came from
231
+ // but in some instances we might already have set this (streamerDataChannels) due to poor choices
232
+ message.playerId = message.playerId || this.playerId;
233
+
234
+ this.subscribedStreamer.protocol.sendMessage(message);
235
+ }
236
+
237
+ private sendToPlayer(message: BaseMessage): void {
238
+ if (!message.playerId) {
239
+ Logger.error(
240
+ `SFU ${this.streamerId} trying to send a message to a player with no playerId. Ignored.`
241
+ );
242
+ return;
243
+ }
244
+ if (!this.subscribers.has(message.playerId)) {
245
+ // Only send to players subscribed to this SFU, otherwise the SFU could target any player
246
+ // on the server via the global player registry (cross-tenant signalling).
247
+ Logger.error(
248
+ `SFU ${this.streamerId} tried to send a message to player ${message.playerId} which is not subscribed to it. Ignored.`
249
+ );
250
+ return;
251
+ }
252
+ const player = this.server.playerRegistry.get(message.playerId);
253
+ if (player) {
254
+ delete message.playerId;
255
+ LogUtils.logForward(this, player, message);
256
+ player.protocol.sendMessage(message);
257
+ } else {
258
+ Logger.error(`SFU attempted to forward to player ${message.playerId} which does not exist.`);
259
+ }
260
+ }
261
+
262
+ private disconnect() {
263
+ this.unsubscribe();
264
+ this.protocol.disconnect();
265
+ this.emit('disconnect');
266
+ }
267
+
268
+ private onLayerPreference(message: Messages.layerPreference): void {
269
+ this.sendMessage(message);
270
+ }
271
+
272
+ private onStreamerIdChanged(newId: string): void {
273
+ const renameMessage = MessageHelpers.createMessage(Messages.streamerIdChanged, { newID: newId });
274
+ this.sendMessage(renameMessage);
275
+ }
276
+
277
+ private onStreamerDisconnected(): void {
278
+ this.unsubscribe();
279
+ this.sendMessage(MessageHelpers.createMessage(Messages.streamerDisconnected));
280
+ this.streaming = false;
281
+ this.emit('disconnect');
282
+ }
283
+
284
+ private onTransportError(error: ErrorEvent): void {
285
+ Logger.error(`SFU (${this.playerId}) transport error ${error.message}`);
286
+ }
287
+
288
+ private onTransportClose(_event: CloseEvent): void {
289
+ Logger.debug('SFUConnection transport close.');
290
+ this.disconnect();
291
+ }
292
+
293
+ private onSubscribeMessage(message: Messages.subscribe): void {
294
+ this.subscribe(message.streamerId);
295
+ }
296
+
297
+ private onUnsubscribeMessage(_message: Messages.unsubscribe): void {
298
+ this.unsubscribe();
299
+ }
300
+
301
+ private onListStreamers(_message: Messages.listStreamers): void {
302
+ const listMessage = MessageHelpers.createMessage(Messages.streamerList, {
303
+ ids: this.server.streamerRegistry.streamers.map((streamer) => streamer.streamerId)
304
+ });
305
+ this.sendMessage(listMessage);
306
+ }
307
+
308
+ private onStreamerDataChannels(message: Messages.streamerDataChannels): void {
309
+ message.sfuId = this.playerId!;
310
+ this.sendToStreamer(message);
311
+ }
312
+
313
+ private onEndpointId(_message: Messages.endpointId): void {
314
+ this.streaming = true;
315
+ }
316
+
317
+ private onStartStreaming(_message: Messages.startStreaming): void {
318
+ this.streaming = true;
319
+ }
320
+
321
+ private onStopStreaming(_message: Messages.stopStreaming): void {
322
+ this.streaming = false;
323
+ this.emit('disconnect');
324
+ }
325
+
326
+ private onPingMessage(message: Messages.ping): void {
327
+ this.sendMessage(MessageHelpers.createMessage(Messages.pong, { time: message.time }));
328
+ }
329
+ }