@arcanewizards/artnet 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,20 @@
1
+ // src/constants.ts
2
+ var ARTNET_PORT = 6454;
3
+ var TIMECODE_MODES = {
4
+ FILM: 0,
5
+ EBU: 1,
6
+ DF: 2,
7
+ SMPTE: 3
8
+ };
9
+ var TIMECODE_FPS = {
10
+ FILM: 24,
11
+ EBU: 25,
12
+ DF: 29.97,
13
+ SMPTE: 30
14
+ };
15
+
16
+ export {
17
+ ARTNET_PORT,
18
+ TIMECODE_MODES,
19
+ TIMECODE_FPS
20
+ };
@@ -0,0 +1,20 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});// src/constants.ts
2
+ var ARTNET_PORT = 6454;
3
+ var TIMECODE_MODES = {
4
+ FILM: 0,
5
+ EBU: 1,
6
+ DF: 2,
7
+ SMPTE: 3
8
+ };
9
+ var TIMECODE_FPS = {
10
+ FILM: 24,
11
+ EBU: 25,
12
+ DF: 29.97,
13
+ SMPTE: 30
14
+ };
15
+
16
+
17
+
18
+
19
+
20
+ exports.ARTNET_PORT = ARTNET_PORT; exports.TIMECODE_MODES = TIMECODE_MODES; exports.TIMECODE_FPS = TIMECODE_FPS;
@@ -0,0 +1,10 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
+
3
+
4
+
5
+ var _chunkXUTZK65Bcjs = require('./chunk-XUTZK65B.cjs');
6
+
7
+
8
+
9
+
10
+ exports.ARTNET_PORT = _chunkXUTZK65Bcjs.ARTNET_PORT; exports.TIMECODE_FPS = _chunkXUTZK65Bcjs.TIMECODE_FPS; exports.TIMECODE_MODES = _chunkXUTZK65Bcjs.TIMECODE_MODES;
@@ -0,0 +1,11 @@
1
+ declare const ARTNET_PORT = 6454;
2
+ declare const TIMECODE_MODES: {
3
+ FILM: number;
4
+ EBU: number;
5
+ DF: number;
6
+ SMPTE: number;
7
+ };
8
+ type TimecodeMode = keyof typeof TIMECODE_MODES;
9
+ declare const TIMECODE_FPS: Record<TimecodeMode, number>;
10
+
11
+ export { ARTNET_PORT, TIMECODE_FPS, TIMECODE_MODES, type TimecodeMode };
@@ -0,0 +1,11 @@
1
+ declare const ARTNET_PORT = 6454;
2
+ declare const TIMECODE_MODES: {
3
+ FILM: number;
4
+ EBU: number;
5
+ DF: number;
6
+ SMPTE: number;
7
+ };
8
+ type TimecodeMode = keyof typeof TIMECODE_MODES;
9
+ declare const TIMECODE_FPS: Record<TimecodeMode, number>;
10
+
11
+ export { ARTNET_PORT, TIMECODE_FPS, TIMECODE_MODES, type TimecodeMode };
@@ -0,0 +1,10 @@
1
+ import {
2
+ ARTNET_PORT,
3
+ TIMECODE_FPS,
4
+ TIMECODE_MODES
5
+ } from "./chunk-J2HDMITA.js";
6
+ export {
7
+ ARTNET_PORT,
8
+ TIMECODE_FPS,
9
+ TIMECODE_MODES
10
+ };
package/dist/index.cjs ADDED
@@ -0,0 +1,301 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
+
3
+
4
+
5
+ var _chunkXUTZK65Bcjs = require('./chunk-XUTZK65B.cjs');
6
+
7
+ // src/index.ts
8
+ var _dgram = require('dgram');
9
+ var _events = require('events'); var _events2 = _interopRequireDefault(_events);
10
+
11
+
12
+ var _netutils = require('@arcanewizards/net-utils');
13
+ var ARTNET_HEADER = "Art-Net\0";
14
+ var ARTNET_VERSION = 14;
15
+ var OP_TIME_CODE = 38656;
16
+ var DROP_FRAME_NUMERATOR = 3e4;
17
+ var DROP_FRAME_DENOMINATOR = 1001;
18
+ var DROP_FRAME_COUNT = 2;
19
+ var DROP_FRAME_FRAMES_PER_SECOND = 30;
20
+ var DROP_FRAME_FRAMES_PER_MINUTE = DROP_FRAME_FRAMES_PER_SECOND * 60 - DROP_FRAME_COUNT;
21
+ var DROP_FRAME_FRAMES_PER_10_MINUTES = DROP_FRAME_FRAMES_PER_MINUTE * 9 + DROP_FRAME_FRAMES_PER_SECOND * 60;
22
+ var DROP_FRAME_FRAMES_PER_HOUR = DROP_FRAME_FRAMES_PER_10_MINUTES * 6;
23
+ var DROP_FRAME_FRAMES_PER_24_HOURS = DROP_FRAME_FRAMES_PER_HOUR * 24;
24
+ var TIMECODE_MODE_IDS = Object.fromEntries(
25
+ Object.entries(_chunkXUTZK65Bcjs.TIMECODE_MODES).map(([mode, id]) => [id, mode])
26
+ );
27
+ var bindSocket = (socket, port, address) => {
28
+ return new Promise((resolve, reject) => {
29
+ const onError = (error) => {
30
+ socket.removeListener("error", onError);
31
+ reject(error);
32
+ };
33
+ socket.once("error", onError);
34
+ const onBound = () => {
35
+ socket.removeListener("error", onError);
36
+ resolve();
37
+ };
38
+ if (address) {
39
+ socket.bind(port, address, onBound);
40
+ } else {
41
+ socket.bind(port, onBound);
42
+ }
43
+ });
44
+ };
45
+ var getDropFrameTimecode = (timeMillis) => {
46
+ const totalFrames = Math.floor(
47
+ timeMillis * DROP_FRAME_NUMERATOR / (1e3 * DROP_FRAME_DENOMINATOR)
48
+ );
49
+ const wrappedFrames = (totalFrames % DROP_FRAME_FRAMES_PER_24_HOURS + DROP_FRAME_FRAMES_PER_24_HOURS) % DROP_FRAME_FRAMES_PER_24_HOURS;
50
+ const tenMinuteChunks = Math.floor(
51
+ wrappedFrames / DROP_FRAME_FRAMES_PER_10_MINUTES
52
+ );
53
+ const remainingFrames = wrappedFrames % DROP_FRAME_FRAMES_PER_10_MINUTES;
54
+ const skippedFrames = DROP_FRAME_COUNT * 9 * tenMinuteChunks + (remainingFrames > DROP_FRAME_COUNT ? DROP_FRAME_COUNT * Math.floor(
55
+ (remainingFrames - DROP_FRAME_COUNT) / DROP_FRAME_FRAMES_PER_MINUTE
56
+ ) : 0);
57
+ const displayFrameNumber = wrappedFrames + skippedFrames;
58
+ return {
59
+ hours: Math.floor(
60
+ displayFrameNumber / (DROP_FRAME_FRAMES_PER_SECOND * 60 * 60)
61
+ ),
62
+ minutes: Math.floor(displayFrameNumber / (DROP_FRAME_FRAMES_PER_SECOND * 60)) % 60,
63
+ seconds: Math.floor(displayFrameNumber / DROP_FRAME_FRAMES_PER_SECOND) % 60,
64
+ frame: displayFrameNumber % DROP_FRAME_FRAMES_PER_SECOND,
65
+ mode: "DF",
66
+ timeMillis
67
+ };
68
+ };
69
+ var getTimecodeFromMillis = (mode, timeMillis) => {
70
+ if (mode === "DF") {
71
+ return getDropFrameTimecode(timeMillis);
72
+ }
73
+ return {
74
+ hours: Math.floor(timeMillis / 36e5),
75
+ minutes: Math.floor(timeMillis % 36e5 / 6e4),
76
+ seconds: Math.floor(timeMillis % 6e4 / 1e3),
77
+ frame: Math.floor(timeMillis % 1e3 / 1e3 * _chunkXUTZK65Bcjs.TIMECODE_FPS[mode]),
78
+ mode,
79
+ timeMillis
80
+ };
81
+ };
82
+ var getTimeMillisFromTimecode = (timecode) => {
83
+ const { hours, minutes, seconds, frame, mode } = timecode;
84
+ if (mode === "DF") {
85
+ const totalMinutes = hours * 60 + minutes;
86
+ const droppedFrames = DROP_FRAME_COUNT * (totalMinutes - Math.floor(totalMinutes / 10));
87
+ const displayFrameNumber = (hours * 60 * 60 + minutes * 60 + seconds) * DROP_FRAME_FRAMES_PER_SECOND + frame;
88
+ const totalFrames = displayFrameNumber - droppedFrames;
89
+ return totalFrames * 1e3 * DROP_FRAME_DENOMINATOR / DROP_FRAME_NUMERATOR;
90
+ }
91
+ return (hours * 60 * 60 + minutes * 60 + seconds) * 1e3 + frame * 1e3 / _chunkXUTZK65Bcjs.TIMECODE_FPS[mode];
92
+ };
93
+ var parseTimecodePacket = (packet, source) => {
94
+ if (packet.length < 19) {
95
+ return null;
96
+ }
97
+ if (packet.subarray(0, 8).toString("ascii") !== ARTNET_HEADER) {
98
+ return null;
99
+ }
100
+ if (packet.readUInt16LE(8) !== OP_TIME_CODE) {
101
+ return null;
102
+ }
103
+ const mode = TIMECODE_MODE_IDS[packet.readUInt8(18)];
104
+ if (!mode) {
105
+ return null;
106
+ }
107
+ const hours = packet.readUInt8(17);
108
+ const minutes = packet.readUInt8(16);
109
+ const seconds = packet.readUInt8(15);
110
+ const frame = packet.readUInt8(14);
111
+ return {
112
+ hours,
113
+ minutes,
114
+ seconds,
115
+ frame,
116
+ mode,
117
+ timeMillis: getTimeMillisFromTimecode({
118
+ hours,
119
+ minutes,
120
+ seconds,
121
+ frame,
122
+ mode
123
+ }),
124
+ host: source.address,
125
+ port: source.port
126
+ };
127
+ };
128
+ var createArtnet = (config) => {
129
+ const events = new (0, _events2.default)();
130
+ let sendSocket = null;
131
+ let receiveSocket = null;
132
+ let destroyed = false;
133
+ let connectPromise = null;
134
+ let interfacePromise = null;
135
+ const on = events.on.bind(events);
136
+ const addListener = events.addListener.bind(events);
137
+ const removeListener = events.removeListener.bind(
138
+ events
139
+ );
140
+ const getInterface = async () => {
141
+ if (config.type === "host") {
142
+ throw new Error(
143
+ "Network interface must be specified when listening for ArtNet packets"
144
+ );
145
+ }
146
+ if (!interfacePromise) {
147
+ interfacePromise = _netutils.getNetworkInterfaces.call(void 0, ).then((interfaces) => {
148
+ const iface = interfaces[config.interface];
149
+ if (!iface) {
150
+ throw new Error(`Network interface ${config.interface} not found`);
151
+ }
152
+ return iface;
153
+ });
154
+ }
155
+ return interfacePromise;
156
+ };
157
+ const cleanupSockets = () => {
158
+ _optionalChain([sendSocket, 'optionalAccess', _ => _.socket, 'access', _2 => _2.close, 'call', _3 => _3()]);
159
+ _optionalChain([receiveSocket, 'optionalAccess', _4 => _4.close, 'call', _5 => _5()]);
160
+ sendSocket = null;
161
+ receiveSocket = null;
162
+ connectPromise = null;
163
+ };
164
+ const initializeSendSocket = async () => {
165
+ if (sendSocket) {
166
+ return;
167
+ }
168
+ const socket = _dgram.createSocket.call(void 0, { type: "udp4", reuseAddr: true });
169
+ let sendHost;
170
+ if (config.type === "interface") {
171
+ const iface = await getInterface();
172
+ sendHost = iface.broadcastAddress;
173
+ await bindSocket(socket, 0);
174
+ socket.setBroadcast(true);
175
+ } else {
176
+ sendHost = config.host;
177
+ }
178
+ sendSocket = { socket, sendHost };
179
+ };
180
+ const initializeReceiveSocket = async () => {
181
+ if (receiveSocket) {
182
+ return;
183
+ }
184
+ const iface = await getInterface();
185
+ const bindAddress = iface.internal ? iface.address : iface.broadcastAddress;
186
+ const socket = _dgram.createSocket.call(void 0, { type: "udp4", reuseAddr: true });
187
+ receiveSocket = socket;
188
+ socket.on("message", (packet, source) => {
189
+ const timecode = parseTimecodePacket(packet, source);
190
+ if (!timecode) {
191
+ return;
192
+ }
193
+ events.emit("timecode", timecode);
194
+ });
195
+ try {
196
+ await bindSocket(socket, _nullishCoalesce(config.port, () => ( _chunkXUTZK65Bcjs.ARTNET_PORT)), bindAddress);
197
+ } catch (error) {
198
+ if (receiveSocket === socket) {
199
+ receiveSocket = null;
200
+ }
201
+ socket.close();
202
+ throw error;
203
+ }
204
+ socket.on("error", (error) => {
205
+ events.emit("error", error);
206
+ });
207
+ };
208
+ const connect = async () => {
209
+ if (destroyed) {
210
+ throw new Error("Cannot connect destroyed ArtNet instance");
211
+ }
212
+ if (connectPromise) {
213
+ return connectPromise;
214
+ }
215
+ connectPromise = (async () => {
216
+ try {
217
+ if (config.mode !== "receive") {
218
+ await initializeSendSocket();
219
+ }
220
+ if (config.mode !== "send") {
221
+ if (config.type === "interface") {
222
+ await initializeReceiveSocket();
223
+ } else if (events.listenerCount("timecode") > 0) {
224
+ throw new Error(
225
+ "Network interface must be specified when listening for ArtNet packets"
226
+ );
227
+ }
228
+ }
229
+ } catch (error) {
230
+ cleanupSockets();
231
+ throw error instanceof Error ? error : new Error(String(error));
232
+ }
233
+ })();
234
+ return connectPromise;
235
+ };
236
+ const sendTimecode = (mode, timeMillis) => {
237
+ if (timeMillis < 0) {
238
+ return Promise.resolve();
239
+ }
240
+ if (!sendSocket) {
241
+ return Promise.reject(new Error("ArtNet connection has not been opened"));
242
+ }
243
+ if (destroyed) {
244
+ return Promise.reject(
245
+ new Error("Cannot send timecode with destroyed ArtNet instance")
246
+ );
247
+ }
248
+ const { socket, sendHost } = sendSocket;
249
+ const { hours, minutes, seconds, frame } = getTimecodeFromMillis(
250
+ mode,
251
+ timeMillis
252
+ );
253
+ const packet = Buffer.alloc(19);
254
+ packet.write(ARTNET_HEADER, 0, "ascii");
255
+ packet.writeUInt16LE(OP_TIME_CODE, 8);
256
+ packet.writeUint16BE(ARTNET_VERSION, 10);
257
+ packet.writeUInt8(0, 12);
258
+ packet.writeUInt8(0, 13);
259
+ packet.writeUInt8(frame, 14);
260
+ packet.writeUInt8(seconds, 15);
261
+ packet.writeUInt8(minutes, 16);
262
+ packet.writeUInt8(hours, 17);
263
+ packet.writeUInt8(_chunkXUTZK65Bcjs.TIMECODE_MODES[mode], 18);
264
+ return new Promise(
265
+ (resolve, reject) => socket.send(
266
+ packet,
267
+ 0,
268
+ packet.length,
269
+ _nullishCoalesce(config.port, () => ( _chunkXUTZK65Bcjs.ARTNET_PORT)),
270
+ sendHost,
271
+ (err) => {
272
+ if (err) {
273
+ const error = new Error("Failed to send ArtNet timecode packet", {
274
+ cause: err instanceof Error ? err : new Error(String(err))
275
+ });
276
+ events.emit("error", error);
277
+ reject(error);
278
+ } else {
279
+ resolve();
280
+ }
281
+ }
282
+ )
283
+ );
284
+ };
285
+ const destroy = () => {
286
+ destroyed = true;
287
+ events.emit("destroy");
288
+ cleanupSockets();
289
+ };
290
+ return {
291
+ connect,
292
+ sendTimecode,
293
+ on,
294
+ addListener,
295
+ removeListener,
296
+ destroy
297
+ };
298
+ };
299
+
300
+
301
+ exports.createArtnet = createArtnet;
@@ -0,0 +1,34 @@
1
+ import { TimecodeMode } from './constants.cjs';
2
+ import { ConnectionConfig } from '@arcanewizards/net-utils';
3
+
4
+ type ArtNetTimecode = {
5
+ hours: number;
6
+ minutes: number;
7
+ seconds: number;
8
+ frame: number;
9
+ mode: TimecodeMode;
10
+ timeMillis: number;
11
+ };
12
+ type ArtNetTimecodeEvent = ArtNetTimecode & {
13
+ host: string;
14
+ port: number;
15
+ };
16
+ type ArtNetEventMap = {
17
+ destroy: [];
18
+ timecode: [ArtNetTimecodeEvent];
19
+ error: [Error];
20
+ };
21
+ type ArtNet = {
22
+ connect: () => Promise<void>;
23
+ sendTimecode: (mode: TimecodeMode, timeMillis: number) => Promise<void>;
24
+ on<K extends keyof ArtNetEventMap>(event: K, callback: (...args: ArtNetEventMap[K]) => void): void;
25
+ addListener<K extends keyof ArtNetEventMap>(event: K, callback: (...args: ArtNetEventMap[K]) => void): void;
26
+ removeListener<K extends keyof ArtNetEventMap>(event: K, callback: (...args: ArtNetEventMap[K]) => void): void;
27
+ destroy: () => void;
28
+ };
29
+ type ArtNetConnectionConfig = ConnectionConfig & {
30
+ mode: 'send' | 'receive' | 'both';
31
+ };
32
+ declare const createArtnet: (config: ArtNetConnectionConfig) => ArtNet;
33
+
34
+ export { type ArtNet, type ArtNetConnectionConfig, type ArtNetEventMap, type ArtNetTimecode, type ArtNetTimecodeEvent, createArtnet };
@@ -0,0 +1,34 @@
1
+ import { TimecodeMode } from './constants.js';
2
+ import { ConnectionConfig } from '@arcanewizards/net-utils';
3
+
4
+ type ArtNetTimecode = {
5
+ hours: number;
6
+ minutes: number;
7
+ seconds: number;
8
+ frame: number;
9
+ mode: TimecodeMode;
10
+ timeMillis: number;
11
+ };
12
+ type ArtNetTimecodeEvent = ArtNetTimecode & {
13
+ host: string;
14
+ port: number;
15
+ };
16
+ type ArtNetEventMap = {
17
+ destroy: [];
18
+ timecode: [ArtNetTimecodeEvent];
19
+ error: [Error];
20
+ };
21
+ type ArtNet = {
22
+ connect: () => Promise<void>;
23
+ sendTimecode: (mode: TimecodeMode, timeMillis: number) => Promise<void>;
24
+ on<K extends keyof ArtNetEventMap>(event: K, callback: (...args: ArtNetEventMap[K]) => void): void;
25
+ addListener<K extends keyof ArtNetEventMap>(event: K, callback: (...args: ArtNetEventMap[K]) => void): void;
26
+ removeListener<K extends keyof ArtNetEventMap>(event: K, callback: (...args: ArtNetEventMap[K]) => void): void;
27
+ destroy: () => void;
28
+ };
29
+ type ArtNetConnectionConfig = ConnectionConfig & {
30
+ mode: 'send' | 'receive' | 'both';
31
+ };
32
+ declare const createArtnet: (config: ArtNetConnectionConfig) => ArtNet;
33
+
34
+ export { type ArtNet, type ArtNetConnectionConfig, type ArtNetEventMap, type ArtNetTimecode, type ArtNetTimecodeEvent, createArtnet };
package/dist/index.js ADDED
@@ -0,0 +1,301 @@
1
+ import {
2
+ ARTNET_PORT,
3
+ TIMECODE_FPS,
4
+ TIMECODE_MODES
5
+ } from "./chunk-J2HDMITA.js";
6
+
7
+ // src/index.ts
8
+ import { createSocket } from "dgram";
9
+ import EventEmitter from "events";
10
+ import {
11
+ getNetworkInterfaces
12
+ } from "@arcanewizards/net-utils";
13
+ var ARTNET_HEADER = "Art-Net\0";
14
+ var ARTNET_VERSION = 14;
15
+ var OP_TIME_CODE = 38656;
16
+ var DROP_FRAME_NUMERATOR = 3e4;
17
+ var DROP_FRAME_DENOMINATOR = 1001;
18
+ var DROP_FRAME_COUNT = 2;
19
+ var DROP_FRAME_FRAMES_PER_SECOND = 30;
20
+ var DROP_FRAME_FRAMES_PER_MINUTE = DROP_FRAME_FRAMES_PER_SECOND * 60 - DROP_FRAME_COUNT;
21
+ var DROP_FRAME_FRAMES_PER_10_MINUTES = DROP_FRAME_FRAMES_PER_MINUTE * 9 + DROP_FRAME_FRAMES_PER_SECOND * 60;
22
+ var DROP_FRAME_FRAMES_PER_HOUR = DROP_FRAME_FRAMES_PER_10_MINUTES * 6;
23
+ var DROP_FRAME_FRAMES_PER_24_HOURS = DROP_FRAME_FRAMES_PER_HOUR * 24;
24
+ var TIMECODE_MODE_IDS = Object.fromEntries(
25
+ Object.entries(TIMECODE_MODES).map(([mode, id]) => [id, mode])
26
+ );
27
+ var bindSocket = (socket, port, address) => {
28
+ return new Promise((resolve, reject) => {
29
+ const onError = (error) => {
30
+ socket.removeListener("error", onError);
31
+ reject(error);
32
+ };
33
+ socket.once("error", onError);
34
+ const onBound = () => {
35
+ socket.removeListener("error", onError);
36
+ resolve();
37
+ };
38
+ if (address) {
39
+ socket.bind(port, address, onBound);
40
+ } else {
41
+ socket.bind(port, onBound);
42
+ }
43
+ });
44
+ };
45
+ var getDropFrameTimecode = (timeMillis) => {
46
+ const totalFrames = Math.floor(
47
+ timeMillis * DROP_FRAME_NUMERATOR / (1e3 * DROP_FRAME_DENOMINATOR)
48
+ );
49
+ const wrappedFrames = (totalFrames % DROP_FRAME_FRAMES_PER_24_HOURS + DROP_FRAME_FRAMES_PER_24_HOURS) % DROP_FRAME_FRAMES_PER_24_HOURS;
50
+ const tenMinuteChunks = Math.floor(
51
+ wrappedFrames / DROP_FRAME_FRAMES_PER_10_MINUTES
52
+ );
53
+ const remainingFrames = wrappedFrames % DROP_FRAME_FRAMES_PER_10_MINUTES;
54
+ const skippedFrames = DROP_FRAME_COUNT * 9 * tenMinuteChunks + (remainingFrames > DROP_FRAME_COUNT ? DROP_FRAME_COUNT * Math.floor(
55
+ (remainingFrames - DROP_FRAME_COUNT) / DROP_FRAME_FRAMES_PER_MINUTE
56
+ ) : 0);
57
+ const displayFrameNumber = wrappedFrames + skippedFrames;
58
+ return {
59
+ hours: Math.floor(
60
+ displayFrameNumber / (DROP_FRAME_FRAMES_PER_SECOND * 60 * 60)
61
+ ),
62
+ minutes: Math.floor(displayFrameNumber / (DROP_FRAME_FRAMES_PER_SECOND * 60)) % 60,
63
+ seconds: Math.floor(displayFrameNumber / DROP_FRAME_FRAMES_PER_SECOND) % 60,
64
+ frame: displayFrameNumber % DROP_FRAME_FRAMES_PER_SECOND,
65
+ mode: "DF",
66
+ timeMillis
67
+ };
68
+ };
69
+ var getTimecodeFromMillis = (mode, timeMillis) => {
70
+ if (mode === "DF") {
71
+ return getDropFrameTimecode(timeMillis);
72
+ }
73
+ return {
74
+ hours: Math.floor(timeMillis / 36e5),
75
+ minutes: Math.floor(timeMillis % 36e5 / 6e4),
76
+ seconds: Math.floor(timeMillis % 6e4 / 1e3),
77
+ frame: Math.floor(timeMillis % 1e3 / 1e3 * TIMECODE_FPS[mode]),
78
+ mode,
79
+ timeMillis
80
+ };
81
+ };
82
+ var getTimeMillisFromTimecode = (timecode) => {
83
+ const { hours, minutes, seconds, frame, mode } = timecode;
84
+ if (mode === "DF") {
85
+ const totalMinutes = hours * 60 + minutes;
86
+ const droppedFrames = DROP_FRAME_COUNT * (totalMinutes - Math.floor(totalMinutes / 10));
87
+ const displayFrameNumber = (hours * 60 * 60 + minutes * 60 + seconds) * DROP_FRAME_FRAMES_PER_SECOND + frame;
88
+ const totalFrames = displayFrameNumber - droppedFrames;
89
+ return totalFrames * 1e3 * DROP_FRAME_DENOMINATOR / DROP_FRAME_NUMERATOR;
90
+ }
91
+ return (hours * 60 * 60 + minutes * 60 + seconds) * 1e3 + frame * 1e3 / TIMECODE_FPS[mode];
92
+ };
93
+ var parseTimecodePacket = (packet, source) => {
94
+ if (packet.length < 19) {
95
+ return null;
96
+ }
97
+ if (packet.subarray(0, 8).toString("ascii") !== ARTNET_HEADER) {
98
+ return null;
99
+ }
100
+ if (packet.readUInt16LE(8) !== OP_TIME_CODE) {
101
+ return null;
102
+ }
103
+ const mode = TIMECODE_MODE_IDS[packet.readUInt8(18)];
104
+ if (!mode) {
105
+ return null;
106
+ }
107
+ const hours = packet.readUInt8(17);
108
+ const minutes = packet.readUInt8(16);
109
+ const seconds = packet.readUInt8(15);
110
+ const frame = packet.readUInt8(14);
111
+ return {
112
+ hours,
113
+ minutes,
114
+ seconds,
115
+ frame,
116
+ mode,
117
+ timeMillis: getTimeMillisFromTimecode({
118
+ hours,
119
+ minutes,
120
+ seconds,
121
+ frame,
122
+ mode
123
+ }),
124
+ host: source.address,
125
+ port: source.port
126
+ };
127
+ };
128
+ var createArtnet = (config) => {
129
+ const events = new EventEmitter();
130
+ let sendSocket = null;
131
+ let receiveSocket = null;
132
+ let destroyed = false;
133
+ let connectPromise = null;
134
+ let interfacePromise = null;
135
+ const on = events.on.bind(events);
136
+ const addListener = events.addListener.bind(events);
137
+ const removeListener = events.removeListener.bind(
138
+ events
139
+ );
140
+ const getInterface = async () => {
141
+ if (config.type === "host") {
142
+ throw new Error(
143
+ "Network interface must be specified when listening for ArtNet packets"
144
+ );
145
+ }
146
+ if (!interfacePromise) {
147
+ interfacePromise = getNetworkInterfaces().then((interfaces) => {
148
+ const iface = interfaces[config.interface];
149
+ if (!iface) {
150
+ throw new Error(`Network interface ${config.interface} not found`);
151
+ }
152
+ return iface;
153
+ });
154
+ }
155
+ return interfacePromise;
156
+ };
157
+ const cleanupSockets = () => {
158
+ sendSocket?.socket.close();
159
+ receiveSocket?.close();
160
+ sendSocket = null;
161
+ receiveSocket = null;
162
+ connectPromise = null;
163
+ };
164
+ const initializeSendSocket = async () => {
165
+ if (sendSocket) {
166
+ return;
167
+ }
168
+ const socket = createSocket({ type: "udp4", reuseAddr: true });
169
+ let sendHost;
170
+ if (config.type === "interface") {
171
+ const iface = await getInterface();
172
+ sendHost = iface.broadcastAddress;
173
+ await bindSocket(socket, 0);
174
+ socket.setBroadcast(true);
175
+ } else {
176
+ sendHost = config.host;
177
+ }
178
+ sendSocket = { socket, sendHost };
179
+ };
180
+ const initializeReceiveSocket = async () => {
181
+ if (receiveSocket) {
182
+ return;
183
+ }
184
+ const iface = await getInterface();
185
+ const bindAddress = iface.internal ? iface.address : iface.broadcastAddress;
186
+ const socket = createSocket({ type: "udp4", reuseAddr: true });
187
+ receiveSocket = socket;
188
+ socket.on("message", (packet, source) => {
189
+ const timecode = parseTimecodePacket(packet, source);
190
+ if (!timecode) {
191
+ return;
192
+ }
193
+ events.emit("timecode", timecode);
194
+ });
195
+ try {
196
+ await bindSocket(socket, config.port ?? ARTNET_PORT, bindAddress);
197
+ } catch (error) {
198
+ if (receiveSocket === socket) {
199
+ receiveSocket = null;
200
+ }
201
+ socket.close();
202
+ throw error;
203
+ }
204
+ socket.on("error", (error) => {
205
+ events.emit("error", error);
206
+ });
207
+ };
208
+ const connect = async () => {
209
+ if (destroyed) {
210
+ throw new Error("Cannot connect destroyed ArtNet instance");
211
+ }
212
+ if (connectPromise) {
213
+ return connectPromise;
214
+ }
215
+ connectPromise = (async () => {
216
+ try {
217
+ if (config.mode !== "receive") {
218
+ await initializeSendSocket();
219
+ }
220
+ if (config.mode !== "send") {
221
+ if (config.type === "interface") {
222
+ await initializeReceiveSocket();
223
+ } else if (events.listenerCount("timecode") > 0) {
224
+ throw new Error(
225
+ "Network interface must be specified when listening for ArtNet packets"
226
+ );
227
+ }
228
+ }
229
+ } catch (error) {
230
+ cleanupSockets();
231
+ throw error instanceof Error ? error : new Error(String(error));
232
+ }
233
+ })();
234
+ return connectPromise;
235
+ };
236
+ const sendTimecode = (mode, timeMillis) => {
237
+ if (timeMillis < 0) {
238
+ return Promise.resolve();
239
+ }
240
+ if (!sendSocket) {
241
+ return Promise.reject(new Error("ArtNet connection has not been opened"));
242
+ }
243
+ if (destroyed) {
244
+ return Promise.reject(
245
+ new Error("Cannot send timecode with destroyed ArtNet instance")
246
+ );
247
+ }
248
+ const { socket, sendHost } = sendSocket;
249
+ const { hours, minutes, seconds, frame } = getTimecodeFromMillis(
250
+ mode,
251
+ timeMillis
252
+ );
253
+ const packet = Buffer.alloc(19);
254
+ packet.write(ARTNET_HEADER, 0, "ascii");
255
+ packet.writeUInt16LE(OP_TIME_CODE, 8);
256
+ packet.writeUint16BE(ARTNET_VERSION, 10);
257
+ packet.writeUInt8(0, 12);
258
+ packet.writeUInt8(0, 13);
259
+ packet.writeUInt8(frame, 14);
260
+ packet.writeUInt8(seconds, 15);
261
+ packet.writeUInt8(minutes, 16);
262
+ packet.writeUInt8(hours, 17);
263
+ packet.writeUInt8(TIMECODE_MODES[mode], 18);
264
+ return new Promise(
265
+ (resolve, reject) => socket.send(
266
+ packet,
267
+ 0,
268
+ packet.length,
269
+ config.port ?? ARTNET_PORT,
270
+ sendHost,
271
+ (err) => {
272
+ if (err) {
273
+ const error = new Error("Failed to send ArtNet timecode packet", {
274
+ cause: err instanceof Error ? err : new Error(String(err))
275
+ });
276
+ events.emit("error", error);
277
+ reject(error);
278
+ } else {
279
+ resolve();
280
+ }
281
+ }
282
+ )
283
+ );
284
+ };
285
+ const destroy = () => {
286
+ destroyed = true;
287
+ events.emit("destroy");
288
+ cleanupSockets();
289
+ };
290
+ return {
291
+ connect,
292
+ sendTimecode,
293
+ on,
294
+ addListener,
295
+ removeListener,
296
+ destroy
297
+ };
298
+ };
299
+ export {
300
+ createArtnet
301
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@arcanewizards/artnet",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "@arcanewizards/source": "./src/index.ts",
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.cjs"
12
+ },
13
+ "./constants": {
14
+ "@arcanewizards/source": "./src/constants.ts",
15
+ "types": "./dist/constants.d.ts",
16
+ "import": "./dist/constants.js",
17
+ "require": "./dist/constants.cjs"
18
+ },
19
+ "./package.json": "./package.json"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "rm -rf dist && tsup && check-export-map",
26
+ "check:types": "tsc --noEmit",
27
+ "format:fix": "cd .. && pnpm format:fix",
28
+ "lint": "eslint . --max-warnings 0",
29
+ "lint:fix": "eslint --fix ."
30
+ },
31
+ "devDependencies": {
32
+ "@arcanewizards/eslint-config": "workspace:^",
33
+ "@arcanewizards/typescript-config": "workspace:^",
34
+ "@types/node": "^25.0.3",
35
+ "check-export-map": "^1.3.1",
36
+ "eslint": "^9",
37
+ "tsup": "^8.1.0",
38
+ "typescript": "^5.7.3"
39
+ },
40
+ "dependencies": {
41
+ "@arcanewizards/net-utils": "workspace:^"
42
+ }
43
+ }