stagelinq 1.0.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 (45) hide show
  1. package/.gitattributes +2 -0
  2. package/.prettierignore +7 -0
  3. package/.prettierrc.js +10 -0
  4. package/.vscode/launch.json +40 -0
  5. package/.vscode/tasks.json +24 -0
  6. package/Databases/Databases.ts +69 -0
  7. package/Databases/DbConnection.ts +42 -0
  8. package/Databases/index.ts +2 -0
  9. package/LICENSE +674 -0
  10. package/LogEmitter/index.ts +51 -0
  11. package/README.md +83 -0
  12. package/StageLinq/index.ts +58 -0
  13. package/TODO.md +5 -0
  14. package/albumArt/index.ts +2 -0
  15. package/albumArt/maybeDownloadFiles.ts +34 -0
  16. package/cli/index.ts +142 -0
  17. package/devices/Player.ts +182 -0
  18. package/devices/PlayerMessageQueue.ts +62 -0
  19. package/index.ts +5 -0
  20. package/network/NetworkDevice.ts +287 -0
  21. package/network/StageLinqDevices.ts +215 -0
  22. package/network/StageLinqListener.ts +50 -0
  23. package/network/announce.ts +133 -0
  24. package/network/index.ts +4 -0
  25. package/package.json +31 -0
  26. package/services/FileTransfer.ts +298 -0
  27. package/services/Service.ts +144 -0
  28. package/services/StateMap.ts +170 -0
  29. package/services/index.ts +3 -0
  30. package/stagelinq.code-workspace +16 -0
  31. package/tsconfig.json +13 -0
  32. package/types/common.ts +240 -0
  33. package/types/database.ts +50 -0
  34. package/types/index.ts +53 -0
  35. package/types/player.ts +41 -0
  36. package/types/tokens.ts +39 -0
  37. package/utils/Context.ts +45 -0
  38. package/utils/ReadContext.ts +114 -0
  39. package/utils/WriteContext.ts +105 -0
  40. package/utils/getTempFilePath.ts +23 -0
  41. package/utils/hex.ts +62 -0
  42. package/utils/index.ts +6 -0
  43. package/utils/log.ts +0 -0
  44. package/utils/sleep.ts +3 -0
  45. package/utils/tcp.ts +17 -0
@@ -0,0 +1,287 @@
1
+ import { Logger } from '../LogEmitter';
2
+ import { ReadContext } from '../utils/ReadContext';
3
+ import { ServicePorts, ConnectionInfo, LISTEN_TIMEOUT, MessageId, Tokens } from '../types';
4
+ import { sleep } from '../utils/sleep';
5
+ import { strict as assert } from 'assert';
6
+ import { WriteContext } from '../utils/WriteContext';
7
+ import * as FileType from 'file-type';
8
+ import * as fs from 'fs';
9
+ import * as services from '../services';
10
+ import * as tcp from '../utils/tcp';
11
+ import Database = require('better-sqlite3');
12
+
13
+
14
+ interface SourceAndTrackPath {
15
+ source: string;
16
+ trackPath: string;
17
+ }
18
+
19
+ export class NetworkDevice {
20
+ private connection: tcp.Connection = null;
21
+ //private source: string = null;
22
+ private serviceRequestAllowed = false;
23
+ private servicePorts: ServicePorts = {};
24
+ private services: Record<string, InstanceType<typeof services.Service>> = {};
25
+ private timeAlive: number = 0;
26
+ private connectedSources: {
27
+ [key: string]: {
28
+ db: Database.Database;
29
+ albumArt: {
30
+ path: string;
31
+ extensions: {
32
+ [key: string]: string;
33
+ };
34
+ };
35
+ };
36
+ } = {};
37
+
38
+ private connectionInfo: ConnectionInfo;
39
+
40
+ constructor(info: ConnectionInfo) {
41
+ this.connectionInfo = info;
42
+ }
43
+
44
+ private get address() {
45
+ return this.connectionInfo.address;
46
+ }
47
+
48
+ private get port() {
49
+ return this.connectionInfo.port;
50
+ }
51
+
52
+ ///////////////////////////////////////////////////////////////////////////
53
+ // Connect / Disconnect
54
+
55
+ async connect(): Promise<void> {
56
+ const info = this.connectionInfo;
57
+ Logger.debug(`Attempting to connect to ${info.address}:${info.port}`)
58
+ this.connection = await tcp.connect(info.address, info.port);
59
+ this.connection.socket.on('data', (p_message: Buffer) => {
60
+ this.messageHandler(p_message);
61
+ });
62
+ await this.requestAllServicePorts();
63
+ }
64
+
65
+ disconnect(): void {
66
+ // Disconnect all services
67
+ for (const [key, service] of Object.entries(this.services)) {
68
+ service.disconnect();
69
+ this.services[key] = null;
70
+ }
71
+
72
+ assert(this.connection);
73
+ this.connection.destroy();
74
+ this.connection = null;
75
+ }
76
+
77
+ ///////////////////////////////////////////////////////////////////////////
78
+ // Message Handler
79
+
80
+ messageHandler(p_message: Buffer): void {
81
+ const ctx = new ReadContext(p_message.buffer, false);
82
+ while (ctx.isEOF() === false) {
83
+ const id = ctx.readUInt32();
84
+ // FIXME: Verify token
85
+ ctx.seek(16); // Skip token; present in all messages
86
+ switch (id) {
87
+ case MessageId.TimeStamp:
88
+ ctx.seek(16); // Skip token; present in all messages
89
+ // Time Alive is in nanoseconds; convert back to seconds
90
+ this.timeAlive = Number(ctx.readUInt64() / (1000n * 1000n * 1000n));
91
+ break;
92
+ case MessageId.ServicesAnnouncement:
93
+ const service = ctx.readNetworkStringUTF16();
94
+ const port = ctx.readUInt16();
95
+ this.servicePorts[service] = port;
96
+ break;
97
+ case MessageId.ServicesRequest:
98
+ this.serviceRequestAllowed = true;
99
+ break;
100
+ default:
101
+ assert.fail(`NetworkDevice Unhandled message id '${id}'`);
102
+ break;
103
+ }
104
+ }
105
+ }
106
+
107
+ ///////////////////////////////////////////////////////////////////////////
108
+ // Public methods
109
+
110
+ getPort(): number {
111
+ return this.port;
112
+ }
113
+ getTimeAlive(): number {
114
+ return this.timeAlive;
115
+ }
116
+
117
+ // Factory function
118
+ async connectToService<T extends InstanceType<typeof services.Service>>(ctor: {
119
+ new (p_address: string, p_port: number, p_controller: NetworkDevice): T;
120
+ }): Promise<T> {
121
+ assert(this.connection);
122
+ // FIXME: find out why we need these waits before connecting to a service
123
+ await sleep(500);
124
+
125
+ const serviceName = ctor.name;
126
+
127
+ if (this.services[serviceName]) {
128
+ return this.services[serviceName] as T;
129
+ }
130
+
131
+ assert(this.servicePorts.hasOwnProperty(serviceName));
132
+ assert(this.servicePorts[serviceName] > 0);
133
+ const port = this.servicePorts[serviceName];
134
+
135
+ const service = new ctor(this.address, port, this);
136
+
137
+ await service.connect();
138
+ this.services[serviceName] = service;
139
+ return service;
140
+ }
141
+
142
+ // TODO: Refactor this out of here.
143
+ async addSource(p_sourceName: string, p_localDbPath: string, p_localAlbumArtPath: string) {
144
+ if (this.connectedSources[p_sourceName]) {
145
+ return;
146
+ }
147
+ const db = new Database(p_localDbPath);
148
+
149
+ // Get all album art extensions
150
+ const stmt = db.prepare('SELECT * FROM AlbumArt WHERE albumArt NOT NULL');
151
+ const result = stmt.all();
152
+ const albumArtExtensions: Record<string, string | null> = {};
153
+ for (const entry of result) {
154
+ const filetype = await FileType.fromBuffer(entry.albumArt);
155
+ albumArtExtensions[entry.id] = filetype ? filetype.ext : null;
156
+ }
157
+
158
+ this.connectedSources[p_sourceName] = {
159
+ db: db,
160
+ albumArt: {
161
+ path: p_localAlbumArtPath,
162
+ extensions: albumArtExtensions,
163
+ },
164
+ };
165
+ }
166
+
167
+ // TODO: Refactor this out of here.
168
+ async dumpAlbumArt(p_sourceName: string) {
169
+ if (!this.connectedSources[p_sourceName]) {
170
+ assert.fail(`Source '${p_sourceName}' not connected`);
171
+ return;
172
+ }
173
+ const path = this.connectedSources[p_sourceName].albumArt.path;
174
+ if (fs.existsSync(path) === false) {
175
+ fs.mkdirSync(path, { recursive: true });
176
+ }
177
+
178
+ const result = await this.querySource(p_sourceName, 'SELECT * FROM AlbumArt WHERE albumArt NOT NULL');
179
+ for (const entry of result) {
180
+ const filetype = await FileType.fromBuffer(entry.albumArt);
181
+ const ext = filetype ? '.' + filetype.ext : '';
182
+ const filepath = `${path}/${entry.id}${ext}`;
183
+ fs.writeFileSync(filepath, entry.albumArt);
184
+ }
185
+ Logger.info(`dumped ${result.length} albums arts in '${path}'`);
186
+ }
187
+
188
+ // Database helpers
189
+
190
+ querySource(p_sourceName: string, p_query: string, ...p_params: any[]): any[] {
191
+ if (!this.connectedSources[p_sourceName]) {
192
+ //assert.fail(`Source '${p_sourceName}' not connected`);
193
+ return [];
194
+ }
195
+ const db = this.connectedSources[p_sourceName].db;
196
+ const stmt = db.prepare(p_query);
197
+
198
+ return stmt.all(p_params);
199
+ }
200
+
201
+ getAlbumArtPath(p_networkPath: string): string {
202
+ const result = this.getSourceAndTrackFromNetworkPath(p_networkPath);
203
+ if (!result) {
204
+ return null;
205
+ }
206
+
207
+ const sql = 'SELECT * FROM Track WHERE path = ?';
208
+ const dbResult = this.querySource(result.source, sql, result.trackPath);
209
+ if (dbResult.length === 0) {
210
+ return null;
211
+ }
212
+
213
+ assert(dbResult.length === 1); // there can only be one path
214
+ const id = dbResult[0].idAlbumArt;
215
+ const ext = this.connectedSources[result.source].albumArt.extensions[id];
216
+ if (!ext) {
217
+ return null;
218
+ }
219
+
220
+ return `${this.connectedSources[result.source].albumArt.path}${id}.${ext}`;
221
+ }
222
+
223
+ ///////////////////////////////////////////////////////////////////////////
224
+ // Private methods
225
+
226
+ private getSourceAndTrackFromNetworkPath(p_path: string): SourceAndTrackPath {
227
+ if (!p_path || p_path.length === 0) {
228
+ return null;
229
+ }
230
+
231
+ const parts = p_path.split('/');
232
+ //assert(parts.length > )
233
+ assert(parts[0] === 'net:');
234
+ assert(parts[1] === '');
235
+ assert(parts[2].length === 36);
236
+ const source = parts[3];
237
+ let trackPath = parts.slice(5).join('/');
238
+ if (parts[4] !== 'Engine Library') {
239
+ // This probably occurs with RekordBox conversions; tracks are outside Engine Library folder
240
+ trackPath = `../${parts[4]}/${trackPath}`;
241
+ }
242
+ return {
243
+ source: source,
244
+ trackPath: trackPath,
245
+ };
246
+ }
247
+
248
+ private async requestAllServicePorts(): Promise<void> {
249
+ assert(this.connection);
250
+
251
+ return new Promise(async (resolve, reject) => {
252
+ setTimeout(() => {
253
+ reject(new Error(`Failed to requestServices for ` +
254
+ `${this.connectionInfo.source} ` +
255
+ `${this.connectionInfo.address}:${this.connectionInfo.port}`));
256
+ }, LISTEN_TIMEOUT);
257
+
258
+ // Wait for serviceRequestAllowed
259
+ while (true) {
260
+ if (this.serviceRequestAllowed) {
261
+ break;
262
+ }
263
+ await sleep(250);
264
+ }
265
+
266
+ // FIXME: Refactor into message writer helper class
267
+ const ctx = new WriteContext();
268
+ ctx.writeUInt32(MessageId.ServicesRequest);
269
+ ctx.write(Tokens.SoundSwitch);
270
+ const written = await this.connection.write(ctx.getBuffer());
271
+ assert(written === ctx.tell());
272
+
273
+ while (true) {
274
+ // FIXME: How to determine when all services have been announced?
275
+ if (Object.keys(this.servicePorts).length > 3) {
276
+ Logger.debug(`Discovered the following services on ${this.address}:${this.port}`);
277
+ for (const [name, port] of Object.entries(this.servicePorts)) {
278
+ Logger.debug(`\tport: ${port} => ${name}`);
279
+ }
280
+ resolve();
281
+ break;
282
+ }
283
+ await sleep(250);
284
+ }
285
+ });
286
+ }
287
+ }
@@ -0,0 +1,215 @@
1
+ import { ConnectionInfo, IpAddress, PlayerStatus, ServiceMessage, StageLinqOptions } from '../types';
2
+ import { EventEmitter } from 'events';
3
+ import { NetworkDevice } from '.';
4
+ import { Player } from '../devices/Player';
5
+ import { sleep } from '../utils';
6
+ import { FileTransfer, StateData, StateMap } from '../services';
7
+ import { Logger } from '../LogEmitter';
8
+ import { Databases } from '../Databases';
9
+
10
+ enum ConnectionStatus { CONNECTING, CONNECTED, FAILED };
11
+
12
+ interface StageLinqDevice {
13
+ networkDevice: NetworkDevice;
14
+ fileTransferService: FileTransfer;
15
+ };
16
+
17
+ export declare interface StageLinqDevices {
18
+ on(event: 'trackLoaded', listener: (status: PlayerStatus) => void): this;
19
+ on(event: 'stateChanged', listener: (status: PlayerStatus) => void): this;
20
+ on(event: 'nowPlaying', listener: (status: PlayerStatus) => void): this;
21
+ on(event: 'connected', listener: (connectionInfo: ConnectionInfo) => void): this;
22
+ on(event: 'message', listener: (connectionInfo: ConnectionInfo, message: ServiceMessage<StateData>) => void): this;
23
+ on(event: 'ready', listener: (connectionInfo: ConnectionInfo) => void): this;
24
+ }
25
+
26
+ //////////////////////////////////////////////////////////////////////////////
27
+
28
+ // TODO: Refactor device, listener, and player into something more nicer.
29
+
30
+ /**
31
+ * Handle connecting and disconnecting from discovered devices on the
32
+ * StageLinq network.
33
+ */
34
+ export class StageLinqDevices extends EventEmitter {
35
+
36
+ private _databases: Databases;
37
+ private devices: Map<IpAddress, StageLinqDevice> = new Map();
38
+ private discoveryStatus: Map<string, ConnectionStatus> = new Map();
39
+ private options: StageLinqOptions;
40
+
41
+ constructor(options: StageLinqOptions) {
42
+ super();
43
+ this.options = options;
44
+ this._databases = new Databases();
45
+ }
46
+
47
+ /**
48
+ * Attempt to connect to the player.
49
+ *
50
+ * @param connectionInfo Device discovered
51
+ * @returns Retries to connect 3 times. If successful return void if not throw exception.
52
+ */
53
+ async handleDevice(connectionInfo: ConnectionInfo) {
54
+ Logger.silly(this.showDiscoveryStatus(connectionInfo));
55
+
56
+ // Ignore this discovery message if connected, connecting, failed, or
57
+ // if it's blacklisted.
58
+ if (this.isConnected(connectionInfo)
59
+ || this.isConnecting(connectionInfo)
60
+ || this.isFailed(connectionInfo)
61
+ || this.isIgnored(connectionInfo)) return;
62
+
63
+ this.discoveryStatus.set(this.deviceId(connectionInfo), ConnectionStatus.CONNECTING);
64
+
65
+ // Retrying appears to be necessary because it seems the Denon hardware
66
+ // sometimes doesn't connect. Retrying after a little wait seems to
67
+ // solve the issue.
68
+
69
+ let attempt = 1;
70
+
71
+ while (attempt < this.options.maxRetries) {
72
+ try {
73
+ Logger.info(`Connecting to ${this.deviceId(connectionInfo)}. ` +
74
+ `Attempt ${attempt}/${this.options.maxRetries}`);
75
+ // If this fails, catch it, and maybe retry if necessary.
76
+ await this.connectToPlayer(connectionInfo);
77
+ this.discoveryStatus.set(this.deviceId(connectionInfo), ConnectionStatus.CONNECTED);
78
+ this.emit('ready', connectionInfo);
79
+ return; // Don't forget to return!
80
+ } catch(e) {
81
+
82
+ // Failed connection. Sleep then retry.
83
+ Logger.warn(`Could not connect to ${this.deviceId(connectionInfo)} ` +
84
+ `(${attempt}/${this.options.maxRetries}): ${e}`);
85
+ attempt += 1;
86
+ sleep(500);
87
+
88
+ }
89
+ }
90
+
91
+ // We failed 3 times. Throw exception.
92
+ this.discoveryStatus.set(this.deviceId(connectionInfo), ConnectionStatus.FAILED);
93
+ throw new Error(`Could not connect to ${this.deviceId(connectionInfo)}`);
94
+ }
95
+
96
+ /**
97
+ * Disconnect from all connected devices
98
+ */
99
+ disconnectAll() {
100
+ for (const device of this.devices.values()) {
101
+ device.networkDevice.disconnect();
102
+ }
103
+ }
104
+
105
+ get databases() {
106
+ return this._databases;
107
+ }
108
+
109
+ async downloadFile(ipAddress: string, path: string) {
110
+ const device = this.devices.get(ipAddress);
111
+ const file = await device.fileTransferService.getFile(path);
112
+ return file;
113
+ }
114
+
115
+ ////////////////////////////////////////////////////////////////////////////
116
+
117
+ /**
118
+ * Connect to the player.
119
+ * @param connectionInfo Device to connect to.
120
+ * @returns
121
+ */
122
+ private async connectToPlayer(connectionInfo: ConnectionInfo) {
123
+ const networkDevice = new NetworkDevice(connectionInfo);
124
+ await networkDevice.connect();
125
+
126
+ Logger.info(`Successfully connected to ${this.deviceId(connectionInfo)}`);
127
+ const fileTransfer = await networkDevice.connectToService(FileTransfer);
128
+
129
+ this.devices.set(connectionInfo.address, {
130
+ networkDevice: networkDevice,
131
+ fileTransferService: fileTransfer
132
+ });
133
+
134
+ if (this.options.downloadDbSources) {
135
+ const sources = await this.databases.downloadSourcesFromDevice(connectionInfo, networkDevice);
136
+ Logger.debug(`Database sources: ${sources.join(', ')}`);
137
+ }
138
+
139
+ // Setup StateMap
140
+ const stateMap = await networkDevice.connectToService(StateMap);
141
+ stateMap.on('message', (data) => {
142
+ this.emit('message', connectionInfo, data)
143
+ });
144
+
145
+ // Setup Player
146
+ const player = new Player({
147
+ stateMap: stateMap,
148
+ address: connectionInfo.address,
149
+ port: connectionInfo.port
150
+ });
151
+
152
+ player.on('trackLoaded', (status) => {
153
+ this.emit('trackLoaded', status);
154
+ });
155
+
156
+ player.on('stateChanged', (status) => {
157
+ this.emit('stateChanged', status);
158
+ });
159
+
160
+ player.on('nowPlaying', (status) => {
161
+ this.emit('nowPlaying', status);
162
+ });
163
+
164
+ this.emit('connected', connectionInfo);
165
+
166
+ }
167
+
168
+ private deviceId(device: ConnectionInfo) {
169
+ return `${device.address}:${device.port}:` +
170
+ `[${device.source}/${device.software.name}]`;
171
+ }
172
+
173
+ private isConnecting(device: ConnectionInfo) {
174
+ return this.discoveryStatus.get(this.deviceId(device))
175
+ === ConnectionStatus.CONNECTING;
176
+ }
177
+
178
+ private isConnected(device: ConnectionInfo) {
179
+ return this.discoveryStatus.get(this.deviceId(device))
180
+ === ConnectionStatus.CONNECTED;
181
+ }
182
+
183
+ private isFailed(device: ConnectionInfo) {
184
+ return this.discoveryStatus.get(this.deviceId(device))
185
+ === ConnectionStatus.FAILED;
186
+ }
187
+
188
+ private isIgnored(device: ConnectionInfo) {
189
+ return (
190
+ device.source === this.options.actingAs.source
191
+ || device.software.name === 'OfflineAnalyzer'
192
+ || /^SoundSwitch/i.test(device.software.name)
193
+ || /^Resolume/i.test(device.software.name)
194
+ || device.software.name === 'JM08' // Ignore X1800/X1850 mixers
195
+ || device.software.name === 'SSS0' // Ignore SoundSwitchEmbedded on players
196
+ )
197
+ }
198
+
199
+ private isDeviceSeen(device: ConnectionInfo) {
200
+ return this.discoveryStatus.has(device.address);
201
+ }
202
+
203
+ private showDiscoveryStatus(device: ConnectionInfo) {
204
+ let msg = `Discovery: ${this.deviceId(device)} `;
205
+
206
+ if (!this.isDeviceSeen) return msg += '(NEW)';
207
+ if (this.isIgnored(device)) return msg += '(IGNORED)';
208
+ return msg += (
209
+ this.isConnecting(device) ? '(CONNECTING)'
210
+ : this.isConnected(device) ? '(CONNECTED)'
211
+ : this.isFailed(device) ? '(FAILED)'
212
+ : '(NEW)');
213
+ }
214
+
215
+ }
@@ -0,0 +1,50 @@
1
+ import { ConnectionInfo } from '../types';
2
+ import { createSocket, RemoteInfo } from 'dgram';
3
+ import { LISTEN_PORT, DISCOVERY_MESSAGE_MARKER } from '../types/common';
4
+ import { ReadContext } from '../utils/ReadContext';
5
+ import { strict as assert } from 'assert';
6
+
7
+ type DeviceDiscoveryCallback = (info: ConnectionInfo) => void;
8
+
9
+ /**
10
+ * Continuously listens for devices to announce themselves. When they do,
11
+ * execute a callback.
12
+ */
13
+ export class StageLinqListener {
14
+
15
+ /**
16
+ * Listen for new devices on the network and callback when a new one is found.
17
+ * @param callback Callback when new device is discovered.
18
+ */
19
+ listenForDevices(callback: DeviceDiscoveryCallback) {
20
+ const client = createSocket('udp4');
21
+ client.on('message', (p_announcement: Uint8Array, p_remote: RemoteInfo) => {
22
+ const ctx = new ReadContext(p_announcement.buffer, false);
23
+ const result = this.readConnectionInfo(ctx, p_remote.address);
24
+ assert(ctx.tell() === p_remote.size);
25
+ callback(result);
26
+ });
27
+ client.bind(LISTEN_PORT);
28
+ }
29
+
30
+ private readConnectionInfo(p_ctx: ReadContext, p_address: string): ConnectionInfo {
31
+ const magic = p_ctx.getString(4);
32
+ if (magic !== DISCOVERY_MESSAGE_MARKER) {
33
+ return null;
34
+ }
35
+
36
+ const result: ConnectionInfo = {
37
+ token: p_ctx.read(16),
38
+ source: p_ctx.readNetworkStringUTF16(),
39
+ action: p_ctx.readNetworkStringUTF16(),
40
+ software: {
41
+ name: p_ctx.readNetworkStringUTF16(),
42
+ version: p_ctx.readNetworkStringUTF16(),
43
+ },
44
+ port: p_ctx.readUInt16(),
45
+ address: p_address,
46
+ };
47
+ assert(p_ctx.isEOF());
48
+ return result;
49
+ }
50
+ }
@@ -0,0 +1,133 @@
1
+ import {
2
+ ANNOUNCEMENT_INTERVAL,
3
+ CONNECT_TIMEOUT,
4
+ DISCOVERY_MESSAGE_MARKER,
5
+ LISTEN_PORT,
6
+ } from '../types';
7
+ import { createSocket, Socket as UDPSocket } from 'dgram';
8
+ import { Logger } from '../LogEmitter';
9
+ import { networkInterfaces } from 'os';
10
+ import { strict as assert } from 'assert';
11
+ import { subnet } from 'ip';
12
+ import { WriteContext } from '../utils/WriteContext';
13
+ import type { DiscoveryMessage } from '../types';
14
+
15
+ function findBroadcastIPs(): string[] {
16
+ const interfaces = Object.values(networkInterfaces());
17
+ assert(interfaces.length);
18
+ const ips = [];
19
+ for (const i of interfaces) {
20
+ assert(i && i.length);
21
+ for (const entry of i) {
22
+ if (entry.family === 'IPv4' && entry.internal === false) {
23
+ const info = subnet(entry.address, entry.netmask);
24
+ ips.push(info.broadcastAddress);
25
+ }
26
+ }
27
+ }
28
+ return ips;
29
+ }
30
+
31
+
32
+ let announceClient: UDPSocket | null = null;
33
+ let announceTimer: NodeJS.Timer | null = null;
34
+
35
+ function writeDiscoveryMessage(p_ctx: WriteContext, p_message: DiscoveryMessage): number {
36
+ let written = 0;
37
+ written += p_ctx.writeFixedSizedString(DISCOVERY_MESSAGE_MARKER);
38
+ written += p_ctx.write(p_message.token);
39
+ written += p_ctx.writeNetworkStringUTF16(p_message.source);
40
+ written += p_ctx.writeNetworkStringUTF16(p_message.action);
41
+ written += p_ctx.writeNetworkStringUTF16(p_message.software.name);
42
+ written += p_ctx.writeNetworkStringUTF16(p_message.software.version);
43
+ written += p_ctx.writeUInt16(p_message.port);
44
+ return written;
45
+ }
46
+
47
+ async function initUdpSocket(): Promise<UDPSocket> {
48
+ return new Promise<UDPSocket>((resolve, reject) => {
49
+ try {
50
+ const client = createSocket('udp4');
51
+ client.bind(); // we need to bind to a random port in order to enable broadcasting
52
+ client.on('listening', () => {
53
+ client.setBroadcast(true); // needs to be true in order to UDP multicast on MacOS
54
+ resolve(client);
55
+ });
56
+ } catch (err) {
57
+ Logger.error(`Failed to create UDP socket for announcing: ${err}`);
58
+ reject(err);
59
+ }
60
+ });
61
+ }
62
+
63
+ async function broadcastMessage(p_message: Uint8Array): Promise<void> {
64
+ const ips = findBroadcastIPs();
65
+ assert(ips.length > 0, 'No broadcast IPs have been found');
66
+
67
+ const send = async function (p_ip: string): Promise<void> {
68
+ return new Promise((resolve, reject) => {
69
+ setTimeout(() => {
70
+ reject(new Error('Failed to send announcement'));
71
+ }, CONNECT_TIMEOUT);
72
+
73
+ announceClient.send(p_message, LISTEN_PORT, p_ip, () => {
74
+ // Logger.log('UDP message sent to ' + p_ip);
75
+ resolve();
76
+ });
77
+ });
78
+ };
79
+
80
+ const promises = ips.map((ip) => send(ip));
81
+ await Promise.all(promises);
82
+ }
83
+
84
+ export async function unannounce(message: DiscoveryMessage): Promise<void> {
85
+ assert(announceTimer);
86
+ clearInterval(announceTimer);
87
+ announceTimer = null;
88
+ const ctx = new WriteContext();
89
+ writeDiscoveryMessage(ctx, message);
90
+ const msg = new Uint8Array(ctx.getBuffer());
91
+ await broadcastMessage(msg);
92
+ // Logger.info("Unannounced myself");
93
+ }
94
+
95
+ export async function announce(message: DiscoveryMessage): Promise<void> {
96
+ if (announceTimer) {
97
+ Logger.log('Already has an announce timer.')
98
+ return;
99
+ }
100
+
101
+ if (!announceClient) announceClient = await initUdpSocket();
102
+
103
+ const ctx = new WriteContext();
104
+ writeDiscoveryMessage(ctx, message);
105
+ const msg = new Uint8Array(ctx.getBuffer());
106
+
107
+ // Immediately announce myself
108
+ await broadcastMessage(msg);
109
+
110
+ announceTimer = setInterval(broadcastMessage, ANNOUNCEMENT_INTERVAL, msg);
111
+ Logger.info("Announced myself");
112
+ }
113
+
114
+ export interface DiscoveryMessageOptions {
115
+ name: string;
116
+ version: string;
117
+ source: string;
118
+ token: Uint8Array;
119
+ };
120
+
121
+ export function createDiscoveryMessage(action: string, discoveryMessageOptions: DiscoveryMessageOptions) {
122
+ const msg: DiscoveryMessage = {
123
+ action: action,
124
+ port: 0,
125
+ software: {
126
+ name: discoveryMessageOptions.name,
127
+ version: discoveryMessageOptions.version
128
+ },
129
+ source: discoveryMessageOptions.source,
130
+ token: discoveryMessageOptions.token
131
+ };
132
+ return msg;
133
+ }
@@ -0,0 +1,4 @@
1
+ export * from './announce';
2
+ export * from './NetworkDevice';
3
+ export * from './StageLinqDevices'
4
+ export * from './StageLinqListener';