lox-slimproto 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.
@@ -0,0 +1,104 @@
1
+ import dgram from 'dgram';
2
+ function parseTlvDiscoveryRequest(payload) {
3
+ const data = payload.toString('utf-8', 1); // drop leading 'e'
4
+ const result = [];
5
+ let idx = 0;
6
+ while (idx <= data.length - 5) {
7
+ const key = data.slice(idx, idx + 4);
8
+ const len = data.charCodeAt(idx + 4);
9
+ idx += 5;
10
+ const value = len > 0 ? data.slice(idx, idx + len) : null;
11
+ idx += len;
12
+ result.push([key, value]);
13
+ }
14
+ return result;
15
+ }
16
+ function buildTlvResponse(requestData, opts) {
17
+ const response = [];
18
+ for (const [key, val] of requestData) {
19
+ switch (key) {
20
+ case 'NAME':
21
+ response.push([key, opts.name]);
22
+ break;
23
+ case 'IPAD':
24
+ response.push([key, opts.ipAddress]);
25
+ break;
26
+ case 'JSON':
27
+ if (opts.cliPortJson != null)
28
+ response.push([key, String(opts.cliPortJson)]);
29
+ break;
30
+ case 'CLIP':
31
+ if (opts.cliPort != null)
32
+ response.push([key, String(opts.cliPort)]);
33
+ break;
34
+ case 'VERS':
35
+ response.push([key, '7.999.999']);
36
+ break;
37
+ case 'UUID':
38
+ response.push([key, opts.uuid]);
39
+ break;
40
+ default:
41
+ response.push([key, val]);
42
+ }
43
+ }
44
+ return response;
45
+ }
46
+ function encodeTlvResponse(responseData) {
47
+ const parts = ['E']; // response prefix
48
+ for (const [key, value] of responseData) {
49
+ const val = value ?? '';
50
+ const truncated = val.length > 255 ? val.slice(0, 255) : val;
51
+ parts.push(key, String.fromCharCode(truncated.length), truncated);
52
+ }
53
+ return Buffer.from(parts.join(''), 'utf-8');
54
+ }
55
+ function encodeLegacyDiscovery(ipAddress) {
56
+ const hostname = ipAddress.slice(0, 16).padEnd(16, '\u0000');
57
+ const buf = Buffer.alloc(17);
58
+ buf.write('D', 0, 'ascii');
59
+ buf.write(hostname, 1, 'binary');
60
+ return buf;
61
+ }
62
+ export function startDiscovery(opts) {
63
+ const socket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
64
+ const name = opts.name ?? 'Slimproto';
65
+ const uuid = opts.uuid ?? 'slimproto';
66
+ socket.on('listening', () => {
67
+ try {
68
+ socket.addMembership('239.255.255.250');
69
+ }
70
+ catch (err) {
71
+ // eslint-disable-next-line no-console
72
+ console.warn('Failed to join discovery multicast group', err);
73
+ }
74
+ });
75
+ socket.on('message', (msg, rinfo) => {
76
+ try {
77
+ if (msg.length === 0)
78
+ return;
79
+ if (msg[0] === 0x65) {
80
+ const requestData = parseTlvDiscoveryRequest(msg);
81
+ const responseData = buildTlvResponse(requestData, {
82
+ name,
83
+ ipAddress: opts.ipAddress,
84
+ cliPort: opts.cliPort,
85
+ cliPortJson: opts.cliPortJson,
86
+ uuid,
87
+ });
88
+ const payload = encodeTlvResponse(responseData);
89
+ socket.send(payload, rinfo.port, rinfo.address);
90
+ return;
91
+ }
92
+ if (msg[0] === 0x64) {
93
+ const payload = encodeLegacyDiscovery(opts.ipAddress);
94
+ socket.send(payload, rinfo.port, rinfo.address);
95
+ }
96
+ }
97
+ catch (err) {
98
+ // eslint-disable-next-line no-console
99
+ console.error('Error handling discovery message from', rinfo.address, err);
100
+ }
101
+ });
102
+ socket.bind(opts.controlPort, '0.0.0.0');
103
+ return socket;
104
+ }
@@ -0,0 +1,3 @@
1
+ export declare class UnsupportedContentType extends Error {
2
+ constructor(message: string);
3
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,6 @@
1
+ export class UnsupportedContentType extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'UnsupportedContentType';
5
+ }
6
+ }
@@ -0,0 +1,8 @@
1
+ export * from './constants.js';
2
+ export * from './models.js';
3
+ export * from './util.js';
4
+ export * from './volume.js';
5
+ export * from './discovery.js';
6
+ export * from './errors.js';
7
+ export * from './client.js';
8
+ export * from './server.js';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from './constants.js';
2
+ export * from './models.js';
3
+ export * from './util.js';
4
+ export * from './volume.js';
5
+ export * from './discovery.js';
6
+ export * from './errors.js';
7
+ export * from './client.js';
8
+ export * from './server.js';
@@ -0,0 +1,119 @@
1
+ export declare enum EventType {
2
+ PLAYER_UPDATED = "player_updated",
3
+ PLAYER_HEARTBEAT = "player_heartbeat",
4
+ PLAYER_CONNECTED = "player_connected",
5
+ PLAYER_DISCONNECTED = "player_disconnected",
6
+ PLAYER_NAME_RECEIVED = "player_name_received",
7
+ PLAYER_DISPLAY_RESOLUTION = "player_display_resolution",
8
+ PLAYER_DECODER_READY = "player_decoder_ready",
9
+ PLAYER_DECODER_ERROR = "player_decoder_error",
10
+ PLAYER_OUTPUT_UNDERRUN = "player_output_underrun",
11
+ PLAYER_BUFFER_READY = "player_buffer_ready",
12
+ PLAYER_CLI_EVENT = "player_cli_event",
13
+ PLAYER_BTN_EVENT = "player_btn_event",
14
+ PLAYER_PRESETS_UPDATED = "player_presets_updated"
15
+ }
16
+ export interface SlimEvent<T = unknown> {
17
+ type: EventType;
18
+ playerId: string;
19
+ data?: T;
20
+ }
21
+ export declare const DEVICE_TYPE: Record<number, string>;
22
+ export declare enum PlayerState {
23
+ PLAYING = "playing",
24
+ STOPPED = "stopped",
25
+ PAUSED = "paused",
26
+ BUFFERING = "buffering",
27
+ BUFFER_READY = "buffer_ready"
28
+ }
29
+ export declare enum TransitionType {
30
+ NONE = "0",
31
+ CROSSFADE = "1",
32
+ FADE_IN = "2",
33
+ FADE_OUT = "3",
34
+ FADE_IN_OUT = "4"
35
+ }
36
+ export declare enum VisualisationType {
37
+ NONE = "none",
38
+ SPECTRUM_ANALYZER = "spectrum_analyzer",
39
+ VU_METER_ANALOG = "vu_meter_analog",
40
+ VU_METER_DIGITAL = "vu_meter_digital",
41
+ WAVEFORM = "waveform"
42
+ }
43
+ export declare enum RemoteCode {
44
+ SLEEP = 1988737095,
45
+ POWER = 1988706495,
46
+ REWIND = 1988739135,
47
+ PAUSE = 1988698335,
48
+ FORWARD = 1988730975,
49
+ ADD = 1988714655,
50
+ PLAY = 1988694255,
51
+ UP = 1988747295,
52
+ DOWN = 1988735055,
53
+ LEFT = 1988726895,
54
+ RIGHT = 1988743215,
55
+ VOLUME_UP = 1988722815,
56
+ VOLUME_DOWN = 1988690175,
57
+ NUM_1 = 1988751375,
58
+ NUM_2 = 1988692215,
59
+ NUM_3 = 1988724855,
60
+ NUM_4 = 1988708535,
61
+ NUM_5 = 1988741175,
62
+ NUM_6 = 1988700375,
63
+ NUM_7 = 1988733015,
64
+ NUM_8 = 1988716695,
65
+ NUM_9 = 1988749335,
66
+ NUM_0 = 1988728935,
67
+ FAVORITES = 1988696295,
68
+ SEARCH = 1988712615,
69
+ BROWSE = 1988718735,
70
+ SHUFFLE = 1988745255,
71
+ REPEAT = 1988704455,
72
+ NOW_PLAYING = 1988720775,
73
+ SIZE = 1988753415,
74
+ BRIGHTNESS = 1988691195
75
+ }
76
+ export declare enum ButtonCode {
77
+ POWER = 65546,
78
+ PRESET_1 = 131104,
79
+ PRESET_2 = 131105,
80
+ PRESET_3 = 131106,
81
+ PRESET_4 = 131107,
82
+ PRESET_5 = 131108,
83
+ PRESET_6 = 131109,
84
+ BACK = 131085,
85
+ PLAY = 131090,
86
+ ADD = 131091,
87
+ UP = 131083,
88
+ OK = 131086,
89
+ REWIND = 131088,
90
+ PAUSE = 131095,
91
+ FORWARD = 131101,
92
+ VOLUME_DOWN = 131081,
93
+ POWER_RELEASE = 131082
94
+ }
95
+ export declare const PCM_SAMPLE_SIZE: Record<number, Buffer>;
96
+ export declare const PCM_SAMPLE_RATE: Record<number, Buffer>;
97
+ export declare const CODEC_MAPPING: Record<string, string>;
98
+ export declare const FORMAT_BYTE: Record<string, Buffer>;
99
+ export declare const PLAYMODE_MAP: Record<PlayerState, string>;
100
+ export interface MediaMetadata {
101
+ item_id?: string;
102
+ artist?: string;
103
+ album?: string;
104
+ title?: string;
105
+ image_url?: string;
106
+ duration?: number;
107
+ }
108
+ export interface MediaDetails {
109
+ url: string;
110
+ mimeType?: string;
111
+ metadata?: MediaMetadata;
112
+ transition?: TransitionType;
113
+ transitionDuration?: number;
114
+ }
115
+ export interface Preset {
116
+ uri: string;
117
+ text: string;
118
+ icon: string;
119
+ }
package/dist/models.js ADDED
@@ -0,0 +1,168 @@
1
+ export var EventType;
2
+ (function (EventType) {
3
+ EventType["PLAYER_UPDATED"] = "player_updated";
4
+ EventType["PLAYER_HEARTBEAT"] = "player_heartbeat";
5
+ EventType["PLAYER_CONNECTED"] = "player_connected";
6
+ EventType["PLAYER_DISCONNECTED"] = "player_disconnected";
7
+ EventType["PLAYER_NAME_RECEIVED"] = "player_name_received";
8
+ EventType["PLAYER_DISPLAY_RESOLUTION"] = "player_display_resolution";
9
+ EventType["PLAYER_DECODER_READY"] = "player_decoder_ready";
10
+ EventType["PLAYER_DECODER_ERROR"] = "player_decoder_error";
11
+ EventType["PLAYER_OUTPUT_UNDERRUN"] = "player_output_underrun";
12
+ EventType["PLAYER_BUFFER_READY"] = "player_buffer_ready";
13
+ EventType["PLAYER_CLI_EVENT"] = "player_cli_event";
14
+ EventType["PLAYER_BTN_EVENT"] = "player_btn_event";
15
+ EventType["PLAYER_PRESETS_UPDATED"] = "player_presets_updated";
16
+ })(EventType || (EventType = {}));
17
+ export const DEVICE_TYPE = {
18
+ 2: 'squeezebox',
19
+ 3: 'softsqueeze',
20
+ 4: 'squeezebox2',
21
+ 5: 'transporter',
22
+ 6: 'softsqueeze3',
23
+ 7: 'receiver',
24
+ 8: 'squeezeslave',
25
+ 9: 'controller',
26
+ 10: 'boom',
27
+ 11: 'softboom',
28
+ 12: 'squeezeplay',
29
+ 100: 'squeezeesp32',
30
+ };
31
+ export var PlayerState;
32
+ (function (PlayerState) {
33
+ PlayerState["PLAYING"] = "playing";
34
+ PlayerState["STOPPED"] = "stopped";
35
+ PlayerState["PAUSED"] = "paused";
36
+ PlayerState["BUFFERING"] = "buffering";
37
+ PlayerState["BUFFER_READY"] = "buffer_ready";
38
+ })(PlayerState || (PlayerState = {}));
39
+ export var TransitionType;
40
+ (function (TransitionType) {
41
+ TransitionType["NONE"] = "0";
42
+ TransitionType["CROSSFADE"] = "1";
43
+ TransitionType["FADE_IN"] = "2";
44
+ TransitionType["FADE_OUT"] = "3";
45
+ TransitionType["FADE_IN_OUT"] = "4";
46
+ })(TransitionType || (TransitionType = {}));
47
+ export var VisualisationType;
48
+ (function (VisualisationType) {
49
+ VisualisationType["NONE"] = "none";
50
+ VisualisationType["SPECTRUM_ANALYZER"] = "spectrum_analyzer";
51
+ VisualisationType["VU_METER_ANALOG"] = "vu_meter_analog";
52
+ VisualisationType["VU_METER_DIGITAL"] = "vu_meter_digital";
53
+ VisualisationType["WAVEFORM"] = "waveform";
54
+ })(VisualisationType || (VisualisationType = {}));
55
+ export var RemoteCode;
56
+ (function (RemoteCode) {
57
+ RemoteCode[RemoteCode["SLEEP"] = 1988737095] = "SLEEP";
58
+ RemoteCode[RemoteCode["POWER"] = 1988706495] = "POWER";
59
+ RemoteCode[RemoteCode["REWIND"] = 1988739135] = "REWIND";
60
+ RemoteCode[RemoteCode["PAUSE"] = 1988698335] = "PAUSE";
61
+ RemoteCode[RemoteCode["FORWARD"] = 1988730975] = "FORWARD";
62
+ RemoteCode[RemoteCode["ADD"] = 1988714655] = "ADD";
63
+ RemoteCode[RemoteCode["PLAY"] = 1988694255] = "PLAY";
64
+ RemoteCode[RemoteCode["UP"] = 1988747295] = "UP";
65
+ RemoteCode[RemoteCode["DOWN"] = 1988735055] = "DOWN";
66
+ RemoteCode[RemoteCode["LEFT"] = 1988726895] = "LEFT";
67
+ RemoteCode[RemoteCode["RIGHT"] = 1988743215] = "RIGHT";
68
+ RemoteCode[RemoteCode["VOLUME_UP"] = 1988722815] = "VOLUME_UP";
69
+ RemoteCode[RemoteCode["VOLUME_DOWN"] = 1988690175] = "VOLUME_DOWN";
70
+ RemoteCode[RemoteCode["NUM_1"] = 1988751375] = "NUM_1";
71
+ RemoteCode[RemoteCode["NUM_2"] = 1988692215] = "NUM_2";
72
+ RemoteCode[RemoteCode["NUM_3"] = 1988724855] = "NUM_3";
73
+ RemoteCode[RemoteCode["NUM_4"] = 1988708535] = "NUM_4";
74
+ RemoteCode[RemoteCode["NUM_5"] = 1988741175] = "NUM_5";
75
+ RemoteCode[RemoteCode["NUM_6"] = 1988700375] = "NUM_6";
76
+ RemoteCode[RemoteCode["NUM_7"] = 1988733015] = "NUM_7";
77
+ RemoteCode[RemoteCode["NUM_8"] = 1988716695] = "NUM_8";
78
+ RemoteCode[RemoteCode["NUM_9"] = 1988749335] = "NUM_9";
79
+ RemoteCode[RemoteCode["NUM_0"] = 1988728935] = "NUM_0";
80
+ RemoteCode[RemoteCode["FAVORITES"] = 1988696295] = "FAVORITES";
81
+ RemoteCode[RemoteCode["SEARCH"] = 1988712615] = "SEARCH";
82
+ RemoteCode[RemoteCode["BROWSE"] = 1988718735] = "BROWSE";
83
+ RemoteCode[RemoteCode["SHUFFLE"] = 1988745255] = "SHUFFLE";
84
+ RemoteCode[RemoteCode["REPEAT"] = 1988704455] = "REPEAT";
85
+ RemoteCode[RemoteCode["NOW_PLAYING"] = 1988720775] = "NOW_PLAYING";
86
+ RemoteCode[RemoteCode["SIZE"] = 1988753415] = "SIZE";
87
+ RemoteCode[RemoteCode["BRIGHTNESS"] = 1988691195] = "BRIGHTNESS";
88
+ })(RemoteCode || (RemoteCode = {}));
89
+ export var ButtonCode;
90
+ (function (ButtonCode) {
91
+ ButtonCode[ButtonCode["POWER"] = 65546] = "POWER";
92
+ ButtonCode[ButtonCode["PRESET_1"] = 131104] = "PRESET_1";
93
+ ButtonCode[ButtonCode["PRESET_2"] = 131105] = "PRESET_2";
94
+ ButtonCode[ButtonCode["PRESET_3"] = 131106] = "PRESET_3";
95
+ ButtonCode[ButtonCode["PRESET_4"] = 131107] = "PRESET_4";
96
+ ButtonCode[ButtonCode["PRESET_5"] = 131108] = "PRESET_5";
97
+ ButtonCode[ButtonCode["PRESET_6"] = 131109] = "PRESET_6";
98
+ ButtonCode[ButtonCode["BACK"] = 131085] = "BACK";
99
+ ButtonCode[ButtonCode["PLAY"] = 131090] = "PLAY";
100
+ ButtonCode[ButtonCode["ADD"] = 131091] = "ADD";
101
+ ButtonCode[ButtonCode["UP"] = 131083] = "UP";
102
+ ButtonCode[ButtonCode["OK"] = 131086] = "OK";
103
+ ButtonCode[ButtonCode["REWIND"] = 131088] = "REWIND";
104
+ ButtonCode[ButtonCode["PAUSE"] = 131095] = "PAUSE";
105
+ ButtonCode[ButtonCode["FORWARD"] = 131101] = "FORWARD";
106
+ ButtonCode[ButtonCode["VOLUME_DOWN"] = 131081] = "VOLUME_DOWN";
107
+ ButtonCode[ButtonCode["POWER_RELEASE"] = 131082] = "POWER_RELEASE";
108
+ })(ButtonCode || (ButtonCode = {}));
109
+ export const PCM_SAMPLE_SIZE = {
110
+ 8: Buffer.from('0'),
111
+ 16: Buffer.from('1'),
112
+ 20: Buffer.from('2'),
113
+ 32: Buffer.from('3'),
114
+ 24: Buffer.from('4'),
115
+ 0: Buffer.from('?'),
116
+ };
117
+ export const PCM_SAMPLE_RATE = {
118
+ 11000: Buffer.from('0'),
119
+ 22000: Buffer.from('1'),
120
+ 44100: Buffer.from('3'),
121
+ 48000: Buffer.from('4'),
122
+ 8000: Buffer.from('5'),
123
+ 12000: Buffer.from('6'),
124
+ 16000: Buffer.from('7'),
125
+ 24000: Buffer.from('8'),
126
+ 88200: Buffer.from(':'),
127
+ 96000: Buffer.from('9'),
128
+ 176400: Buffer.from(';'),
129
+ 192000: Buffer.from('<'),
130
+ 352800: Buffer.from('='),
131
+ 384000: Buffer.from('>'),
132
+ 0: Buffer.from('?'),
133
+ };
134
+ export const CODEC_MAPPING = {
135
+ 'audio/mp3': 'mp3',
136
+ 'audio/mpeg': 'mp3',
137
+ 'audio/flac': 'flc',
138
+ 'audio/x-flac': 'flc',
139
+ 'audio/wma': 'wma',
140
+ 'audio/ogg': 'ogg',
141
+ 'audio/oga': 'ogg',
142
+ 'audio/aac': 'aac',
143
+ 'audio/aacp': 'aac',
144
+ 'audio/alac': 'alc',
145
+ 'audio/wav': 'pcm',
146
+ 'audio/x-wav': 'pcm',
147
+ 'audio/dsf': 'dsf',
148
+ 'audio/pcm,': 'pcm',
149
+ };
150
+ export const FORMAT_BYTE = {
151
+ pcm: Buffer.from('p'),
152
+ mp3: Buffer.from('m'),
153
+ flc: Buffer.from('f'),
154
+ wma: Buffer.from('w'),
155
+ ogg: Buffer.from('o'),
156
+ aac: Buffer.from('a'),
157
+ alc: Buffer.from('l'),
158
+ dsf: Buffer.from('p'),
159
+ dff: Buffer.from('p'),
160
+ aif: Buffer.from('p'),
161
+ };
162
+ export const PLAYMODE_MAP = {
163
+ [PlayerState.STOPPED]: 'stop',
164
+ [PlayerState.PLAYING]: 'play',
165
+ [PlayerState.BUFFER_READY]: 'play',
166
+ [PlayerState.BUFFERING]: 'play',
167
+ [PlayerState.PAUSED]: 'pause',
168
+ };
@@ -0,0 +1,27 @@
1
+ import { EventEmitter } from 'events';
2
+ import { EventType, SlimEvent } from './models.js';
3
+ import { SlimClient } from './client.js';
4
+ export interface SlimServerOptions {
5
+ cliPort?: number | null;
6
+ cliPortJson?: number | null;
7
+ ipAddress?: string | null;
8
+ name?: string | null;
9
+ controlPort?: number;
10
+ }
11
+ export declare class SlimServer extends EventEmitter {
12
+ readonly options: SlimServerOptions;
13
+ private server?;
14
+ private discovery?;
15
+ private playersMap;
16
+ private subscriptions;
17
+ constructor(options?: SlimServerOptions);
18
+ get players(): SlimClient[];
19
+ getPlayer(playerId: string): SlimClient | undefined;
20
+ start(): Promise<void>;
21
+ stop(): Promise<void>;
22
+ subscribe(cb: (event: SlimEvent) => void | Promise<void>, eventFilter?: EventType | EventType[] | null, playerFilter?: string | string[] | null): () => void;
23
+ private registerPlayer;
24
+ private handleDisconnect;
25
+ private handleClientEvent;
26
+ private forwardEvent;
27
+ }
package/dist/server.js ADDED
@@ -0,0 +1,109 @@
1
+ import { EventEmitter } from 'events';
2
+ import net from 'net';
3
+ import { startDiscovery } from './discovery.js';
4
+ import { SLIMPROTO_PORT } from './constants.js';
5
+ import { EventType } from './models.js';
6
+ import { getHostname, getIp } from './util.js';
7
+ import { SlimClient } from './client.js';
8
+ export class SlimServer extends EventEmitter {
9
+ options;
10
+ server;
11
+ discovery;
12
+ playersMap = new Map();
13
+ subscriptions = [];
14
+ constructor(options = {}) {
15
+ super();
16
+ this.options = options;
17
+ }
18
+ get players() {
19
+ return Array.from(this.playersMap.values());
20
+ }
21
+ getPlayer(playerId) {
22
+ return this.playersMap.get(playerId);
23
+ }
24
+ async start() {
25
+ const ipAddress = this.options.ipAddress ?? (await getIp());
26
+ const name = this.options.name ?? getHostname();
27
+ const controlPort = this.options.controlPort ?? SLIMPROTO_PORT;
28
+ this.server = net.createServer((socket) => this.registerPlayer(socket));
29
+ await new Promise((resolve, reject) => {
30
+ this.server?.once('error', reject);
31
+ this.server?.listen(controlPort, '0.0.0.0', () => resolve());
32
+ });
33
+ this.discovery = startDiscovery({
34
+ ipAddress,
35
+ controlPort,
36
+ cliPort: this.options.cliPort ?? null,
37
+ cliPortJson: this.options.cliPortJson ?? null,
38
+ name,
39
+ });
40
+ }
41
+ async stop() {
42
+ for (const client of this.playersMap.values()) {
43
+ client.disconnect();
44
+ }
45
+ this.playersMap.clear();
46
+ if (this.server) {
47
+ await new Promise((resolve) => this.server?.close(() => resolve()));
48
+ this.server = undefined;
49
+ }
50
+ this.discovery?.close();
51
+ this.discovery = undefined;
52
+ }
53
+ subscribe(cb, eventFilter, playerFilter) {
54
+ const eventList = eventFilter == null ? null : Array.isArray(eventFilter) ? eventFilter : [eventFilter];
55
+ const playerList = playerFilter == null ? null : Array.isArray(playerFilter) ? playerFilter : [playerFilter];
56
+ const subscription = { callback: cb, eventFilter: eventList, playerFilter: playerList };
57
+ this.subscriptions.push(subscription);
58
+ return () => {
59
+ const idx = this.subscriptions.indexOf(subscription);
60
+ if (idx >= 0)
61
+ this.subscriptions.splice(idx, 1);
62
+ };
63
+ }
64
+ registerPlayer(socket) {
65
+ const client = new SlimClient(socket, (player, eventType, data) => {
66
+ this.handleClientEvent(player, eventType, data);
67
+ });
68
+ socket.on('close', () => this.handleDisconnect(client));
69
+ socket.on('error', () => this.handleDisconnect(client));
70
+ }
71
+ handleDisconnect(client) {
72
+ if (client.playerId) {
73
+ this.playersMap.delete(client.playerId);
74
+ }
75
+ }
76
+ handleClientEvent(player, eventType, data) {
77
+ const playerId = player.playerId;
78
+ if (eventType === EventType.PLAYER_CONNECTED) {
79
+ const existing = this.playersMap.get(playerId);
80
+ if (existing && existing.connected) {
81
+ player.disconnect();
82
+ return;
83
+ }
84
+ if (existing) {
85
+ existing.disconnect();
86
+ }
87
+ this.playersMap.set(playerId, player);
88
+ }
89
+ if (eventType === EventType.PLAYER_DISCONNECTED) {
90
+ if (playerId) {
91
+ this.playersMap.delete(playerId);
92
+ }
93
+ }
94
+ this.forwardEvent(playerId, eventType, data);
95
+ }
96
+ forwardEvent(playerId, eventType, data) {
97
+ const event = { type: eventType, playerId, data };
98
+ this.emit(eventType, event);
99
+ for (const sub of this.subscriptions) {
100
+ if (sub.playerFilter && playerId && !sub.playerFilter.includes(playerId))
101
+ continue;
102
+ if (sub.eventFilter && !sub.eventFilter.includes(eventType))
103
+ continue;
104
+ Promise.resolve(sub.callback(event)).catch((err) =>
105
+ // eslint-disable-next-line no-console
106
+ console.error('Error in subscriber', err));
107
+ }
108
+ }
109
+ }
package/dist/util.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ export declare function getIp(): Promise<string>;
2
+ export declare function getHostname(): string;
3
+ export declare function selectFreePort(rangeStart: number, rangeEnd: number): Promise<number>;
4
+ export declare function parseCapabilities(heloData: Buffer): Record<string, unknown>;
5
+ export declare function parseHeaders(respData: Buffer): Record<string, string>;
6
+ export declare function parseStatus(respData: Buffer): {
7
+ version: string;
8
+ statusCode: number;
9
+ statusText: string;
10
+ };
11
+ export declare function lookupHost(host: string): Promise<string>;
12
+ export declare function ipToInt(ipAddress: string): number;
package/dist/util.js ADDED
@@ -0,0 +1,98 @@
1
+ import dgram from 'dgram';
2
+ import dns from 'dns/promises';
3
+ import net from 'net';
4
+ import os from 'os';
5
+ import { FALLBACK_CODECS } from './constants.js';
6
+ export async function getIp() {
7
+ const socket = dgram.createSocket('udp4');
8
+ return new Promise((resolve) => {
9
+ socket.connect(1, '10.255.255.255', () => {
10
+ const address = socket.address();
11
+ socket.close();
12
+ if (typeof address === 'object') {
13
+ resolve(address.address);
14
+ }
15
+ else {
16
+ resolve('127.0.0.1');
17
+ }
18
+ });
19
+ socket.on('error', () => {
20
+ socket.close();
21
+ resolve('127.0.0.1');
22
+ });
23
+ });
24
+ }
25
+ export function getHostname() {
26
+ return os.hostname();
27
+ }
28
+ export async function selectFreePort(rangeStart, rangeEnd) {
29
+ const isPortInUse = (port) => new Promise((resolve) => {
30
+ const tester = net.createServer();
31
+ tester.once('error', () => resolve(true));
32
+ tester.once('listening', () => tester.close(() => resolve(false)));
33
+ tester.listen(port, '0.0.0.0');
34
+ });
35
+ for (let port = rangeStart; port < rangeEnd; port += 1) {
36
+ // eslint-disable-next-line no-await-in-loop
37
+ const inUse = await isPortInUse(port);
38
+ if (!inUse) {
39
+ return port;
40
+ }
41
+ }
42
+ throw new Error('No free port available');
43
+ }
44
+ export function parseCapabilities(heloData) {
45
+ const params = {};
46
+ try {
47
+ const info = heloData.subarray(36).toString();
48
+ const pairs = info.replace(/,/g, '&').split('&');
49
+ for (const pair of pairs) {
50
+ if (!pair)
51
+ continue;
52
+ const [key, value] = pair.split('=');
53
+ if (key) {
54
+ params[key] = value ?? '';
55
+ }
56
+ }
57
+ params.SupportedCodecs =
58
+ ['alc', 'aac', 'ogg', 'ogf', 'flc', 'aif', 'pcm', 'mp3'].filter((codec) => info.includes(codec)) ||
59
+ FALLBACK_CODECS;
60
+ }
61
+ catch (err) {
62
+ // eslint-disable-next-line no-console
63
+ console.error('Failed to parse capabilities', err);
64
+ }
65
+ return params;
66
+ }
67
+ export function parseHeaders(respData) {
68
+ const result = {};
69
+ const lines = respData.toString().split('\r\n').slice(1);
70
+ for (const line of lines) {
71
+ const [key, ...rest] = line.split(': ');
72
+ if (!key || rest.length === 0)
73
+ continue;
74
+ result[key.toLowerCase()] = rest.join(': ');
75
+ }
76
+ return result;
77
+ }
78
+ export function parseStatus(respData) {
79
+ const [statusLine] = respData.toString().split('\r\n');
80
+ if (!statusLine) {
81
+ return { version: 'HTTP/1.0', statusCode: 200, statusText: '' };
82
+ }
83
+ const [version, code, ...rest] = statusLine.split(' ');
84
+ return {
85
+ version,
86
+ statusCode: Number(code ?? 200),
87
+ statusText: rest.join(' '),
88
+ };
89
+ }
90
+ export async function lookupHost(host) {
91
+ const result = await dns.lookup(host);
92
+ return result.address;
93
+ }
94
+ export function ipToInt(ipAddress) {
95
+ return ipAddress
96
+ .split('.')
97
+ .reduce((acc, octet) => (acc << 8) + Number(octet), 0) >>> 0;
98
+ }
@@ -0,0 +1,15 @@
1
+ export declare class SlimProtoVolume {
2
+ minimum: number;
3
+ maximum: number;
4
+ step: number;
5
+ private static readonly oldMap;
6
+ private readonly totalVolumeRange;
7
+ private readonly stepPoint;
8
+ private readonly stepFraction;
9
+ volume: number;
10
+ increment(): void;
11
+ decrement(): void;
12
+ oldGain(): number;
13
+ decibels(): number;
14
+ newGain(): number;
15
+ }