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,51 @@
1
+ import { EventEmitter } from 'stream';
2
+
3
+ export declare interface Logger {
4
+ on(event: 'log', listener: (...args: any) => void): this;
5
+ on(event: 'error', listener: (...args: any) => void): this;
6
+ on(event: 'warn', listener: (...args: any) => void): this;
7
+ on(event: 'info', listener: (...args: any) => void): this;
8
+ on(event: 'debug', listener: (...args: any) => void): this;
9
+ on(event: 'silly', listener: (...args: any) => void): this;
10
+ on(event: 'any', listener: (...args: any) => void): this;
11
+ }
12
+
13
+ export class Logger extends EventEmitter {
14
+
15
+ private static _instance: Logger;
16
+
17
+ static get instance() {
18
+ return this._instance || (this._instance = new this());
19
+ }
20
+
21
+ static log(...args: any) {
22
+ Logger.instance.emit('log', ...args);
23
+ Logger.instance.emit('any', ...args);
24
+ }
25
+
26
+ static error(...args: any) {
27
+ Logger.instance.emit('error', ...args);
28
+ Logger.instance.emit('any', ...args);
29
+ }
30
+
31
+ static warn(...args: any) {
32
+ Logger.instance.emit('warn', ...args);
33
+ Logger.instance.emit('any', ...args);
34
+ }
35
+
36
+ static info(...args: any) {
37
+ Logger.instance.emit('info', ...args);
38
+ Logger.instance.emit('any', ...args);
39
+ }
40
+
41
+ static debug(...args: any) {
42
+ Logger.instance.emit('debug', ...args);
43
+ Logger.instance.emit('any', ...args);
44
+ }
45
+
46
+ static silly(...args: any) {
47
+ Logger.instance.emit('silly', ...args);
48
+ Logger.instance.emit('any', ...args);
49
+ }
50
+
51
+ }
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # StageLinq
2
+
3
+ NodeJS library implementation to access information through the Denon StageLinq protocol.
4
+
5
+ # Features
6
+
7
+ * Tested with Denon two SC6000s, X1850, Prime 4, Prime 2, and Prime Go.
8
+ * Event emitters for state changes, tracks getting loaded, and current playing track.
9
+ * Event emitter for debug logging.
10
+ * Downloads source databases for you.
11
+ * You can implement handling the database yourself or use this library's BetterSqlite3 dependency.
12
+
13
+ ---
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import { StageLinq } from '../StageLinq';
19
+
20
+ const options = { downloadDbSources: true };
21
+ const stageLinq = new StageLinq(options);
22
+
23
+ stageLinq.devices.on('ready', (connectionInfo) => {
24
+ console.log(`Device ${connectionInfo.software.name} on ` +
25
+ `${connectionInfo.address}:${connectionInfo.port} is ready.`);
26
+ });
27
+
28
+ stageLinq.devices.on('trackLoaded', (status) => {
29
+ console.log(`"${status.title}" - ${status.artist} loaded on player ` +
30
+ `${status.deck})`);
31
+ });
32
+
33
+ stageLinq.devices.on('nowPlaying', (status) => {
34
+ console.log(`Now Playing: "${status.title}" - ${status.artist})`);
35
+ });
36
+ ```
37
+
38
+ A [complete example](https://github.com/chrisle/StageLinq/blob/main/cli/index.ts) with all events and options can be found in the CLI.
39
+
40
+ ---
41
+
42
+ ## Overview
43
+
44
+ The idea behind this library is to have a structure something like this:
45
+
46
+ **StageLinq > Devices > Player > Deck**
47
+
48
+ A StageLinq sets up a device listener and a class that handles all the
49
+ devices (`StageLinqDevices`).
50
+
51
+ `StageLinqDevices` figures out if it wants to connect or not and handles
52
+ connections. There may be one or more device on the network. For each device it
53
+ will try to connect to it and subscribe to it's `StateMap`.
54
+
55
+ Currently there is only one type of device: `Player`. A `Player` may have up to
56
+ 4 decks A, B, C, D (aka "layers"). The `Player` handles incoming messages,
57
+ parses them, groups them, and emits events. These events bubble up to the
58
+ `Device`.
59
+
60
+ ## Database
61
+
62
+ You can use BetterSqlite3 bundled into this library or let this library
63
+ download the files for you, then choose your own Sqlite library to
64
+ query the database. See CLI example.
65
+
66
+ ## Logging
67
+
68
+ I needed the logging to be used outside of the library so I made them events
69
+ that you can listen to.
70
+
71
+ * `error`: When something bad happens.
72
+ * `warn`: When something happens but doesn't affect anything.
73
+ * `info`/`log`: When we have something to say
74
+ * `debug`: Spits out the parsed version of the packets.
75
+ * `silly`: Dumps all kinds of internal stuff
76
+
77
+ ## About
78
+
79
+ Forked from @MarByteBeep's code.
80
+
81
+ Additional reverse engineering work: https://github.com/chrisle/stagelinq-pcap
82
+
83
+ Used in my app Now Playing https://www.nowplaying2.com
@@ -0,0 +1,58 @@
1
+ import { announce, createDiscoveryMessage, StageLinqListener, unannounce } from '../network';
2
+ import { EventEmitter } from 'events';
3
+ import { StageLinqDevices } from '../network/StageLinqDevices';
4
+ import { Logger } from '../LogEmitter';
5
+ import { Action, ActingAsDevice, StageLinqOptions } from '../types';
6
+
7
+ const DEFAULT_OPTIONS: StageLinqOptions = {
8
+ maxRetries: 3,
9
+ actingAs: ActingAsDevice.NowPlaying,
10
+ downloadDbSources: true
11
+ };
12
+
13
+ /**
14
+ * Main StageLinq class.
15
+ */
16
+ export class StageLinq extends EventEmitter {
17
+
18
+ devices: StageLinqDevices;
19
+ logger: Logger = Logger.instance;
20
+ options: StageLinqOptions;
21
+
22
+ private listener: StageLinqListener = new StageLinqListener();
23
+
24
+ constructor(options?: StageLinqOptions) {
25
+ super();
26
+ this.options = { ...DEFAULT_OPTIONS, ...options };
27
+ this.devices = new StageLinqDevices(this.options);
28
+ }
29
+
30
+ /**
31
+ * Connect to the StageLinq network.
32
+ */
33
+ async connect() {
34
+ const msg = createDiscoveryMessage(Action.Login, this.options.actingAs);
35
+ await announce(msg);
36
+ this.listener.listenForDevices(async (connectionInfo) => {
37
+ await this.devices.handleDevice(connectionInfo);
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Disconnect from the StageLinq network.
43
+ */
44
+ async disconnect() {
45
+ try {
46
+ this.devices.disconnectAll();
47
+ const msg = createDiscoveryMessage(Action.Logout, this.options.actingAs)
48
+ await unannounce(msg);
49
+ } catch(e) {
50
+ throw new Error(e);
51
+ }
52
+ }
53
+
54
+ get databases() {
55
+ return this.devices.databases;
56
+ }
57
+
58
+ }
package/TODO.md ADDED
@@ -0,0 +1,5 @@
1
+ # TODO
2
+
3
+ - [ ] Figure out how fader start works.
4
+ - [ ] Refactor MarByteBeep's album art code.
5
+ - [ ] Get track metadata from DB.
@@ -0,0 +1,2 @@
1
+ export * from '../utils/getTempFilePath';
2
+ export * from './maybeDownloadFiles';
@@ -0,0 +1,34 @@
1
+ import { NetworkDevice } from '../network';
2
+ import { FileTransfer } from '../services';
3
+ import { getTempFilePath } from "../utils/getTempFilePath";
4
+ import { strict as assert } from 'assert';
5
+ import * as fs from 'fs';
6
+ import minimist = require('minimist');
7
+ import { Logger } from '../LogEmitter';
8
+
9
+ export async function maybeDownloadFiles(controller: NetworkDevice) {
10
+ const args = minimist(process.argv.slice(2));
11
+ if (!args.disableFileTransfer) {
12
+ const ftx = await controller.connectToService(FileTransfer);
13
+ assert(ftx);
14
+ const sources = await ftx.getSources();
15
+ {
16
+ const sync = !args.skipsync;
17
+ for (const source of sources) {
18
+ const dbPath = getTempFilePath(source.database.location);
19
+ // FIXME: Move all this away from main
20
+ if (sync) {
21
+ const file = await ftx.getFile(source.database.location);
22
+ fs.writeFileSync(dbPath, file);
23
+ Logger.info(`downloaded: '${source.database.location}' and stored in '${dbPath}'`);
24
+ }
25
+ await controller.addSource(source.name, dbPath, getTempFilePath(`${source.name}/Album Art/`));
26
+
27
+ if (sync) {
28
+ await controller.dumpAlbumArt(source.name);
29
+ }
30
+ }
31
+ ftx.disconnect();
32
+ }
33
+ }
34
+ }
package/cli/index.ts ADDED
@@ -0,0 +1,142 @@
1
+ import { ActingAsDevice } from '../types';
2
+ import { DbConnection } from "../Databases";
3
+ import { sleep } from '../utils/sleep';
4
+ import { StageLinq } from '../StageLinq';
5
+ require('console-stamp')(console, {
6
+ format: ':date(HH:MM:ss) :label',
7
+ });
8
+
9
+
10
+ (async () => {
11
+
12
+ console.log('Starting CLI');
13
+
14
+ const stageLinqOptions = {
15
+
16
+ // If set to true, download the source DBs in a temporary location.
17
+ // (default: true)
18
+ downloadDbSources: true,
19
+
20
+ // Max number of attempts to connect to a StageLinq device.
21
+ // (default: 3)
22
+ maxRetries: 3,
23
+
24
+ // What device to emulate on the network.
25
+ // (default: Now Playing)
26
+ actingAs: ActingAsDevice.NowPlaying
27
+ }
28
+
29
+ const stageLinq = new StageLinq(stageLinqOptions);
30
+
31
+ // Setup how you want to handle logs coming from StageLinq
32
+ stageLinq.logger.on('error', (...args: any) => {
33
+ console.error(...args);
34
+ });
35
+ stageLinq.logger.on('warn', (...args: any) => {
36
+ console.warn(...args);
37
+ });
38
+ stageLinq.logger.on('info', (...args: any) => {
39
+ console.info(...args);
40
+ });
41
+ stageLinq.logger.on('log', (...args: any) => {
42
+ console.log(...args);
43
+ });
44
+ stageLinq.logger.on('debug', (...args: any) => {
45
+ console.debug(...args);
46
+ });
47
+ // Note: Silly is very verbose!
48
+ // stageLinq.logger.on('silly', (...args: any) => {
49
+ // console.debug(...args);
50
+ // });
51
+
52
+ // Fires when we connect to any device
53
+ stageLinq.devices.on('connected', async (connectionInfo) => {
54
+ console.log(`Successfully connected to ${connectionInfo.software.name}`);
55
+
56
+ // Fires when the database source starts downloading.
57
+ stageLinq.databases.on('dbDownloading', (sourceName, dbPath) => {
58
+ console.log(`Downloading ${sourceName} to ${dbPath}`);
59
+ });
60
+
61
+ // Fires while the database source is being read
62
+ stageLinq.databases.on('dbProgress', (sourceName, total, bytes, percent) => {
63
+ console.debug(`Reading ${sourceName}: ${bytes}/${total} (${Math.ceil(percent)}%)`);
64
+ });
65
+
66
+ // Fires when the database source has been read and saved to a temporary path.
67
+ stageLinq.databases.on('dbDownloaded', (sourceName, dbPath) => {
68
+ console.log(`Database (${sourceName}) has been downloaded to ${dbPath}`);
69
+ });
70
+ });
71
+
72
+ // Fires when StageLinq has successfully connect to at least one device and is ready to use.
73
+ stageLinq.devices.on('ready', (connectionInfo) => {
74
+ console.log(`Device ${connectionInfo.software.name} is ready!`);
75
+ });
76
+
77
+ // Fires when a new track is loaded on to a player.
78
+ stageLinq.devices.on('trackLoaded', async (status) => {
79
+
80
+ // Example of how to connect to the database using this library's
81
+ // implementation of BetterSqlite3 to get additional information.
82
+ if (stageLinq.options.downloadDbSources && status.dbSourceName) {
83
+ try {
84
+ const connection = new DbConnection(stageLinq.databases.getDbPath(status.dbSourceName));
85
+ const result = connection.getTrackInfo(status.trackPath);
86
+ connection.close();
87
+ console.log('Database entry:', result);
88
+ } catch(e) {
89
+ console.error(e);
90
+ }
91
+ }
92
+ console.log('New track loaded:', status);
93
+ });
94
+
95
+ // Fires when a track has started playing.
96
+ stageLinq.devices.on('nowPlaying', (status) => {
97
+ console.log(`Now Playing on [${status.deck}]: ${status.title} - ${status.artist}`)
98
+ });
99
+
100
+ // Fires when StageLinq receives messages from a device.
101
+ stageLinq.devices.on('message', (connectionInfo, data) => {
102
+ const msg = data.message.json
103
+ ? JSON.stringify(data.message.json)
104
+ : data.message.interval;
105
+ console.debug(`${connectionInfo.address}:${connectionInfo.port} ` +
106
+ `${data.message.name} => ${msg}`);
107
+ });
108
+
109
+ // Fires when the state of a device has changed.
110
+ stageLinq.devices.on('stateChanged', (status) => {
111
+ console.log(`State changed on [${status.deck}]`, status)
112
+ });
113
+
114
+ let returnCode = 0;
115
+ try {
116
+ process.on('SIGINT', async function () {
117
+ console.info('... exiting');
118
+ // Ensure SIGINT won't be impeded by some error
119
+ try {
120
+ await stageLinq.disconnect();
121
+ } catch (err: any) {
122
+ const message = err.stack.toString();
123
+ console.error(message);
124
+ }
125
+ process.exit(returnCode);
126
+ });
127
+
128
+ await stageLinq.connect();
129
+
130
+ while (true) {
131
+ await sleep(250);
132
+ }
133
+
134
+ } catch (err: any) {
135
+ const message = err.stack.toString();
136
+ console.error(message);
137
+ returnCode = 1;
138
+ }
139
+
140
+ await stageLinq.disconnect();
141
+ process.exit(returnCode);
142
+ })();
@@ -0,0 +1,182 @@
1
+ import { EventEmitter } from 'events';
2
+ import { PlayerLayerState, PlayerStatus, ServiceMessage } from '../types';
3
+ import { PlayerMessageQueue } from './PlayerMessageQueue';
4
+ import { StateData, StateMap } from '../services';
5
+
6
+ export declare interface Player {
7
+ on(event: 'trackLoaded', listener: (status: PlayerStatus) => void): this;
8
+ on(event: 'stateChanged', listener: (status: PlayerStatus) => void): this;
9
+ on(event: 'nowPlaying', listener: (status: PlayerStatus) => void): this;
10
+ }
11
+
12
+ //////////////////////////////////////////////////////////////////////////////
13
+
14
+ interface PlayerOptions {
15
+ stateMap: StateMap;
16
+ address: string,
17
+ port: number;
18
+ }
19
+
20
+ interface SourceAndTrackPath {
21
+ source: string;
22
+ trackPath: string;
23
+ }
24
+
25
+ /**
26
+ * A player represents a device on the StageLinq network.
27
+ *
28
+ * A player on the network may have up to 4 decks (or "layers" as they're
29
+ * called on the harware). A player may also be given a player number.
30
+ *
31
+ * If you're using a Denon Prime Go/2/4 then you should only get one number.
32
+ * If you're using a Denon SC5000/SC6000 then you assign the numbers in the
33
+ * Denon's settings screen.
34
+ *
35
+ * Master tempo and master status only apply if you are using SC5000/SC6000
36
+ * and if they're both on the network.
37
+ *
38
+ * A queue is used to group all the incoming messages from StageLinq to give us
39
+ * a single updated PlayerStatus object.
40
+ */
41
+ export class Player extends EventEmitter {
42
+
43
+ private player: number; // Player number as reported by the device.
44
+ private address: string; // IP address
45
+ private port: number; // Port
46
+ private masterTempo: number; // Current master tempo BPM
47
+ private masterStatus: boolean; // If this device has the matser tempo
48
+ private decks: Map<string, PlayerLayerState> = new Map();
49
+ private queue: {[layer: string]: PlayerMessageQueue} = {};
50
+
51
+ /**
52
+ * Initialize a player device.
53
+ *
54
+ * @param networkDevice Network device
55
+ * @param stateMap Statemap service
56
+ */
57
+ constructor(options: PlayerOptions) {
58
+ super();
59
+ options.stateMap.on('message', this.messageHandler.bind(this));
60
+ this.address = options.address;
61
+ this.port = options.port;
62
+ this.queue = {
63
+ A: new PlayerMessageQueue('A').onDataReady(this.handleUpdate.bind(this)),
64
+ B: new PlayerMessageQueue('B').onDataReady(this.handleUpdate.bind(this)),
65
+ C: new PlayerMessageQueue('C').onDataReady(this.handleUpdate.bind(this)),
66
+ D: new PlayerMessageQueue('D').onDataReady(this.handleUpdate.bind(this)),
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Parse the state data and push it into the update queue.
72
+ *
73
+ * @param data State data from Denon.
74
+ * @returns
75
+ */
76
+ private messageHandler(data: ServiceMessage<StateData>) {
77
+ const message = data.message
78
+ if (!message.json) return;
79
+ const name = message.name;
80
+ const json = message.json as any;
81
+
82
+ if (/Client\/Preferences\/Player$/.test(name)) {
83
+ this.player = parseInt(json.string);
84
+ return;
85
+ }
86
+ if (/Engine\/Master\/MasterTempo/.test(name)) {
87
+ this.masterTempo = json.value;
88
+ return;
89
+ }
90
+ if (/Engine\/Sync\/Network\/MasterStatus/.test(name)) {
91
+ this.masterStatus = json.state;
92
+ return;
93
+ }
94
+
95
+ const split = message.name.split('/');
96
+
97
+ const deck =
98
+ (/PlayerJogColor[A-D]$/.test(name)) ? split[3].replace('PlayerJogColor', '')
99
+ : (/Engine\/Deck\d\//.test(name)) ? this.deckNumberToLayer(split[2])
100
+ : null;
101
+
102
+ const cueData =
103
+ (/PlayState$/.test(name)) ? { playState: json.state }
104
+ : (/Track\/TrackNetworkPath$/.test(name)) ? {
105
+ trackNetworkPath: json.string,
106
+ source: this.getSourceAndTrackPath(json.string).source,
107
+ trackPath: this.getSourceAndTrackPath(json.string).trackPath
108
+ }
109
+ : (/Track\/SongLoaded$/.test(name)) ? { songLoaded: json.state }
110
+ : (/Track\/SongName$/.test(name)) ? { title: json.string }
111
+ : (/Track\/ArtistName$/.test(name)) ? { artist: json.string }
112
+ : (/Track\/TrackData$/.test(name)) ? { hasTrackData: json.state }
113
+ : (/Track\/TrackName$/.test(name)) ? { fileLocation: json.string }
114
+ : (/CurrentBPM$/.test(name)) ? { currentBpm: json.value }
115
+ : (/ExternalMixerVolume$/.test(name)) ? { externalMixerVolume: json.value }
116
+ : (/Play$/.test(name)) ? { play: json.state }
117
+ : (/PlayerJogColor[A-D]$/.test(name)) ? { jogColor: json.color }
118
+ : null;
119
+
120
+ if (cueData) {
121
+ this.queue[deck].push({ layer: deck, ...cueData });
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Update current state and emit.
127
+ * @param data
128
+ */
129
+ private handleUpdate(data: PlayerLayerState) {
130
+ const layer = data.layer;
131
+ const songLoadedSignalPresent = data.hasOwnProperty('songLoaded');
132
+
133
+ // If a new song is loaded drop all the previous track data.
134
+ if (songLoadedSignalPresent) {
135
+ this.decks.set(layer, data);
136
+ } else {
137
+ this.decks.set(layer, { ...this.decks.get(layer), ...data });
138
+ }
139
+
140
+ const result = this.decks.get(layer);
141
+ const deck = `${this.player}${result.layer}`;
142
+
143
+ const output = {
144
+ deck: deck,
145
+ player: this.player,
146
+ layer: layer,
147
+ address: this.address,
148
+ port: this.port,
149
+ masterTempo: this.masterTempo,
150
+ masterStatus: this.masterStatus,
151
+ ...result
152
+ };
153
+
154
+ // We're casting it because we originally built it up piecemeal.
155
+ const currentState = output as PlayerStatus;
156
+ currentState.dbSourceName = currentState.source ? `${this.address}_${this.port}_${currentState.source}` : '';
157
+ if (songLoadedSignalPresent && currentState.trackNetworkPath) this.emit('trackLoaded', currentState);
158
+ if (result.playState) this.emit('nowPlaying', currentState);
159
+ this.emit('stateChanged', currentState);
160
+ }
161
+
162
+ private deckNumberToLayer(deck: string) {
163
+ const index = parseInt(deck.replace('Deck', '')) - 1;
164
+ return 'ABCD'[index];
165
+ }
166
+
167
+ private getSourceAndTrackPath(p_path: string): SourceAndTrackPath {
168
+ if (!p_path || p_path.length === 0) return { source: '', trackPath: '' };
169
+ const parts = p_path.split('/');
170
+ const source = parts[3];
171
+ let trackPath = parts.slice(5).join('/');
172
+ if (parts[4] !== 'Engine Library') {
173
+ // This probably occurs with RekordBox conversions; tracks are outside Engine Library folder
174
+ trackPath = `../${parts[4]}/${trackPath}`;
175
+ }
176
+ return {
177
+ source: source,
178
+ trackPath: trackPath,
179
+ };
180
+ }
181
+
182
+ }
@@ -0,0 +1,62 @@
1
+ import { PlayerLayerState } from '../types';
2
+
3
+ // How long to wait for all the messages to come in before firing the callback.
4
+ export const UPDATE_RATE_MS = 500;
5
+
6
+ export type DataQueueCallback = (data: PlayerLayerState) => void;
7
+
8
+ /**
9
+ * Collect all the messages from a player together and return it as one object.
10
+ *
11
+ * The Denon hardware will fire several messages in quick succession. This will
12
+ * take them all in, then after UPDATE_RATE_MS will merge all the data
13
+ * as a single update to the `onDataReady` callback.
14
+ *
15
+ * For example, when you move the fader up you get several ExternalMixerVolume
16
+ * messages where the value goes up from 0 to 1. Instead firing off loads
17
+ * of updates we're only interested in the last one.
18
+ */
19
+ export class PlayerMessageQueue {
20
+
21
+ private callback: DataQueueCallback;
22
+ private data: PlayerLayerState[] = [];
23
+ private timeout: NodeJS.Timer | null = null;
24
+ private layer: string;
25
+
26
+ constructor(layer: string) {
27
+ this.layer = layer;
28
+ }
29
+
30
+ /**
31
+ * Push data into the queue.
32
+ * @param data Parsed data from a player.
33
+ */
34
+ push(data: PlayerLayerState) {
35
+ this.data.push(data);
36
+ if (!this.timeout) {
37
+ this.timeout = setTimeout(this.emptyCue.bind(this), UPDATE_RATE_MS);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Merge data, empty the queue, clear the timeout, and fire the callback.
43
+ */
44
+ emptyCue() {
45
+ let output: any = { layer: this.layer };
46
+ this.data.map(d => { output = { ...output, ...d }; });
47
+ this.data = [];
48
+ clearTimeout(this.timeout);
49
+ this.timeout = null;
50
+ this.callback(output as PlayerLayerState);
51
+ }
52
+
53
+ /**
54
+ * Execute this callback when there is new data from the Denon hardware.
55
+ * @param callback User callback when we have an update.
56
+ * @returns
57
+ */
58
+ onDataReady(callback: DataQueueCallback) {
59
+ this.callback = callback;
60
+ return this;
61
+ }
62
+ }
package/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './network';
2
+ export * from './services';
3
+ export * from './StageLinq';
4
+ export * from './types';
5
+ export * from './utils';