@arcanewizards/timecode-toolbox 0.0.3 → 0.1.1

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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/dist/components/frontend/index.d.mts +13 -0
  3. package/dist/components/frontend/index.d.ts +13 -0
  4. package/dist/components/frontend/index.js +18978 -0
  5. package/dist/components/frontend/index.mjs +19016 -0
  6. package/dist/entrypoint.css +2788 -0
  7. package/dist/entrypoint.js +42821 -0
  8. package/dist/entrypoint.js.map +7 -0
  9. package/dist/frontend.js +42818 -0
  10. package/dist/frontend.js.map +7 -0
  11. package/dist/index.d.mts +59 -0
  12. package/dist/index.d.ts +59 -0
  13. package/dist/index.js +14894 -0
  14. package/dist/index.mjs +14921 -0
  15. package/dist/start.d.mts +1 -0
  16. package/dist/start.d.ts +1 -0
  17. package/dist/start.js +14886 -0
  18. package/dist/start.mjs +14918 -0
  19. package/package.json +37 -28
  20. package/.turbo/turbo-build.log +0 -55
  21. package/CHANGELOG.md +0 -24
  22. package/eslint.config.mjs +0 -49
  23. package/src/app.tsx +0 -147
  24. package/src/components/backend/index.ts +0 -6
  25. package/src/components/backend/toolbox-root.ts +0 -119
  26. package/src/components/frontend/constants.ts +0 -81
  27. package/src/components/frontend/entrypoint.ts +0 -12
  28. package/src/components/frontend/frontend.css +0 -108
  29. package/src/components/frontend/index.tsx +0 -46
  30. package/src/components/frontend/toolbox/content.tsx +0 -45
  31. package/src/components/frontend/toolbox/context.tsx +0 -63
  32. package/src/components/frontend/toolbox/core/size-aware-div.tsx +0 -51
  33. package/src/components/frontend/toolbox/core/timecode-display.tsx +0 -592
  34. package/src/components/frontend/toolbox/generators.tsx +0 -318
  35. package/src/components/frontend/toolbox/inputs.tsx +0 -484
  36. package/src/components/frontend/toolbox/outputs.tsx +0 -581
  37. package/src/components/frontend/toolbox/preferences.ts +0 -25
  38. package/src/components/frontend/toolbox/root.tsx +0 -335
  39. package/src/components/frontend/toolbox/settings.tsx +0 -54
  40. package/src/components/frontend/toolbox/types.ts +0 -28
  41. package/src/components/frontend/toolbox/util.tsx +0 -61
  42. package/src/components/proto.ts +0 -420
  43. package/src/config.ts +0 -7
  44. package/src/generators/clock.tsx +0 -206
  45. package/src/generators/index.tsx +0 -15
  46. package/src/index.ts +0 -38
  47. package/src/inputs/artnet.tsx +0 -305
  48. package/src/inputs/index.tsx +0 -13
  49. package/src/inputs/tcnet.tsx +0 -272
  50. package/src/outputs/artnet.tsx +0 -170
  51. package/src/outputs/index.tsx +0 -11
  52. package/src/start.ts +0 -47
  53. package/src/tree.ts +0 -133
  54. package/src/types.ts +0 -12
  55. package/src/urls.ts +0 -49
  56. package/src/util.ts +0 -82
  57. package/tailwind.config.cjs +0 -7
  58. package/tsconfig.json +0 -10
  59. package/tsup.config.ts +0 -10
@@ -1,305 +0,0 @@
1
- import { useDataFileData } from '@arcanejs/react-toolkit/data';
2
- import { FC, ReactNode, useCallback, useEffect, useRef, useState } from 'react';
3
- import { ToolboxConfigData } from '../config';
4
- import {
5
- InputArtnetDefinition,
6
- InputConfig,
7
- InputState,
8
- isInputArtnetDefinition,
9
- TimecodeState,
10
- } from '../components/proto';
11
- import { useLogger } from '@arcanewizards/sigil';
12
- import {
13
- ArtNet,
14
- ArtNetTimecodeEvent,
15
- createArtnet,
16
- } from '@arcanewizards/artnet';
17
- import { TIMECODE_FPS, TimecodeMode } from '@arcanewizards/artnet/constants';
18
- import { StateSensitiveComponentProps } from '../types';
19
-
20
- /**
21
- * How much of a difference between the calculated timecode state,
22
- * and previous timecode state is required to trigger an update.
23
- */
24
- const MINIMUM_DRIFT_FOR_UPDATE_MS: Record<TimecodeMode, number> = {
25
- SMPTE: 1000 / TIMECODE_FPS.SMPTE / 2,
26
- FILM: 1000 / TIMECODE_FPS.FILM / 2,
27
- EBU: 1000 / TIMECODE_FPS.EBU / 2,
28
- DF: 1000 / TIMECODE_FPS.DF / 2,
29
- };
30
-
31
- /**
32
- * How many frames need to be missed before we consider a timecode to be lagging.
33
- */
34
- const LAGGING_FRAME_COUNT = 2;
35
-
36
- /**
37
- * If we haven't received a timecode update for this amount of time,
38
- * consider the timecode to be lagging
39
- */
40
- const LAGGING_TIMEOUT_MS: Record<TimecodeMode, number> = {
41
- SMPTE: (1000 / TIMECODE_FPS.SMPTE) * LAGGING_FRAME_COUNT,
42
- FILM: (1000 / TIMECODE_FPS.FILM) * LAGGING_FRAME_COUNT,
43
- EBU: (1000 / TIMECODE_FPS.EBU) * LAGGING_FRAME_COUNT,
44
- DF: (1000 / TIMECODE_FPS.DF) * LAGGING_FRAME_COUNT,
45
- };
46
-
47
- /**
48
- * How long should we wait not receiving any packets
49
- * before considering a timecode to be stopped.
50
- */
51
- const TIMEOUT_MS = 500;
52
-
53
- type ArtnetInputConnectionProps = StateSensitiveComponentProps & {
54
- uuid: string;
55
- config: InputConfig;
56
- connection: InputArtnetDefinition;
57
- };
58
-
59
- const ArtnetInputConnection: FC<ArtnetInputConnectionProps> = ({
60
- uuid,
61
- config: { name, delayMs },
62
- connection: { iface, port },
63
- setState,
64
- }) => {
65
- const log = useLogger();
66
-
67
- const [artnetInstance, setArtnetInstance] = useState<ArtNet | null>(null);
68
-
69
- // Use ref here to allow for updates without requiring re-init of node
70
- const delayRef = useRef(delayMs ?? 0);
71
-
72
- useEffect(() => {
73
- delayRef.current = delayMs ?? 0;
74
- }, [delayMs]);
75
-
76
- const setConnection = useCallback(
77
- (state: InputState) =>
78
- setState((current) => ({
79
- ...current,
80
- inputs: {
81
- ...current.inputs,
82
- [uuid]: state,
83
- },
84
- })),
85
- [setState, uuid],
86
- );
87
-
88
- useEffect(() => {
89
- const connectionConfig: Omit<InputState, 'status'> = {
90
- timecode: {
91
- name: null,
92
- state: {
93
- state: 'none',
94
- accuracyMillis: null,
95
- smpteMode: null,
96
- onAir: null,
97
- },
98
- metadata: null,
99
- },
100
- };
101
- let artnet: ArtNet | null = null;
102
- setConnection({ ...connectionConfig, status: 'connecting' });
103
- const created = createArtnet({
104
- type: 'interface',
105
- interface: iface,
106
- mode: 'receive',
107
- port,
108
- });
109
- created.on('error', (err) => {
110
- const error = new Error('ArtNet input connection error');
111
- error.cause = err instanceof Error ? err : new Error(String(err));
112
- log.error(error);
113
- setConnection({
114
- ...connectionConfig,
115
- status: 'error',
116
- errors: [`${err}`],
117
- });
118
- });
119
- created
120
- .connect()
121
- .then(() => {
122
- artnet = created;
123
- setArtnetInstance(created);
124
- log.info('ArtNet Timecode output initialized');
125
- setConnection({ ...connectionConfig, status: 'active' });
126
- })
127
- .catch((err) => {
128
- const error = new Error('Failed to start ArtNet Timecode output');
129
- error.cause = err instanceof Error ? err : new Error(String(err));
130
- log.error(error);
131
- setConnection({
132
- ...connectionConfig,
133
- status: 'error',
134
- errors: [`${err}`],
135
- });
136
- });
137
-
138
- return () => {
139
- if (artnet) {
140
- artnet.destroy();
141
- setArtnetInstance((current) => (artnet === current ? null : current));
142
- }
143
- };
144
- }, [setConnection, uuid, iface, port, log]);
145
-
146
- useEffect(() => {
147
- type ReceivedTimecode = {
148
- clockMillis: number;
149
- effectiveStartTimeMillis: number;
150
- mode: TimecodeMode;
151
- };
152
-
153
- let lastTimecode: ReceivedTimecode | null = null;
154
- let lastUsedTimecode: ReceivedTimecode | null = lastTimecode;
155
- let timecode: TimecodeState | null = null;
156
- let driftApproximation = 0;
157
- let laggingTimeout: NodeJS.Timeout | null = null;
158
-
159
- let isMounted = true;
160
-
161
- const updateTimecodeState = () => {
162
- if (!lastTimecode) {
163
- // No change
164
- return;
165
- }
166
- if (!isMounted) {
167
- // Update received after being unmounted, ignore
168
- return;
169
- }
170
- const now = Date.now();
171
- if (
172
- lastUsedTimecode === lastTimecode &&
173
- lastTimecode.clockMillis + TIMEOUT_MS < now
174
- ) {
175
- // Timecode has become stale
176
- timecode = {
177
- state: 'stopped',
178
- positionMillis:
179
- lastUsedTimecode.clockMillis -
180
- lastUsedTimecode.effectiveStartTimeMillis,
181
- accuracyMillis: driftApproximation,
182
- smpteMode: lastTimecode.mode,
183
- onAir: null,
184
- };
185
- setConnection({
186
- status: 'active',
187
- timecode: {
188
- name: null,
189
- state: timecode,
190
- metadata: null,
191
- },
192
- });
193
- lastTimecode = null;
194
- return;
195
- }
196
- const isLagging =
197
- lastTimecode.clockMillis + LAGGING_TIMEOUT_MS[lastTimecode.mode] < now;
198
- timecode = {
199
- state: isLagging ? 'lagging' : 'playing',
200
- effectiveStartTimeMillis: lastTimecode.effectiveStartTimeMillis,
201
- accuracyMillis: driftApproximation,
202
- smpteMode: lastTimecode.mode,
203
- speed: 1,
204
- onAir: null,
205
- };
206
- setConnection({
207
- status: 'active',
208
- timecode: {
209
- name: null,
210
- state: timecode,
211
- metadata: null,
212
- },
213
- });
214
- lastUsedTimecode = lastTimecode;
215
-
216
- if (!isLagging) {
217
- // Set up a timeout to mark the timecode as lagging,
218
- // if we don't receive an update quickly enough
219
- if (laggingTimeout) {
220
- clearTimeout(laggingTimeout);
221
- }
222
- // Set a bit later than lagging timeout to ensure that the
223
- // condition is met when the timeout triggers,
224
- laggingTimeout = setTimeout(
225
- updateTimecodeState,
226
- LAGGING_TIMEOUT_MS[lastTimecode.mode] * 1.1,
227
- );
228
- }
229
- };
230
-
231
- const interval = setInterval(updateTimecodeState, TIMEOUT_MS / 2);
232
-
233
- const onTimecode = (tc: ArtNetTimecodeEvent) => {
234
- const clockMillis = Date.now();
235
- const effectiveStartTimeMillis =
236
- clockMillis - tc.timeMillis + delayRef.current;
237
- lastTimecode = {
238
- clockMillis,
239
- effectiveStartTimeMillis,
240
- mode: tc.mode,
241
- };
242
- if (timecode?.state === 'playing') {
243
- const drift = Math.abs(
244
- effectiveStartTimeMillis - timecode.effectiveStartTimeMillis,
245
- );
246
- // Decay the drift approximation over time
247
- // so that temporary spikes don't last
248
- driftApproximation = Math.max(driftApproximation * 0.9, drift);
249
- if (drift < MINIMUM_DRIFT_FOR_UPDATE_MS[tc.mode]) {
250
- // Skip update, difference not significant enough
251
- // just rely on the interal to update the driftApproximation
252
- return;
253
- }
254
- }
255
-
256
- updateTimecodeState();
257
- };
258
-
259
- artnetInstance?.addListener('timecode', onTimecode);
260
-
261
- return () => {
262
- isMounted = false;
263
- clearInterval(interval);
264
- artnetInstance?.removeListener('timecode', onTimecode);
265
- };
266
- }, [artnetInstance, log, iface, name, setConnection]);
267
-
268
- useEffect(() => {
269
- return () => {
270
- // Remove the connection when it's no longer mounted / configured
271
- setState((current) => {
272
- const { [uuid]: _, ...rest } = current.inputs;
273
- return {
274
- ...current,
275
- inputs: rest,
276
- };
277
- });
278
- };
279
- }, [setState, uuid]);
280
-
281
- return null;
282
- };
283
-
284
- export const ArtnetInputConnections: FC<StateSensitiveComponentProps> = (
285
- props,
286
- ) => {
287
- const { inputs } = useDataFileData(ToolboxConfigData);
288
- return Object.entries(inputs)
289
- .filter(([_, { enabled }]) => enabled)
290
- .map<ReactNode>(([uuid, input]) => {
291
- const connection = input.definition;
292
- if (!isInputArtnetDefinition(connection)) {
293
- return null;
294
- }
295
- return (
296
- <ArtnetInputConnection
297
- key={uuid}
298
- uuid={uuid}
299
- config={input}
300
- connection={connection}
301
- {...props}
302
- />
303
- );
304
- });
305
- };
@@ -1,13 +0,0 @@
1
- import { FC } from 'react';
2
- import { ArtnetInputConnections } from './artnet';
3
- import { TcNetInputConnections } from './tcnet';
4
- import { StateSensitiveComponentProps } from '../types';
5
-
6
- export const InputConnections: FC<StateSensitiveComponentProps> = (props) => {
7
- return (
8
- <>
9
- <ArtnetInputConnections {...props} />
10
- <TcNetInputConnections {...props} />
11
- </>
12
- );
13
- };
@@ -1,272 +0,0 @@
1
- import { useDataFileData } from '@arcanejs/react-toolkit/data';
2
- import {
3
- FC,
4
- ReactNode,
5
- useCallback,
6
- useContext,
7
- useEffect,
8
- useMemo,
9
- useRef,
10
- } from 'react';
11
- import { ToolboxConfigData } from '../config';
12
- import {
13
- ConnectedClient,
14
- InputConfig,
15
- InputState,
16
- InputTcnetDefinition,
17
- isInputTcnetDefinition,
18
- TimecodeGroup,
19
- } from '../components/proto';
20
- import {
21
- AppInformationContext,
22
- useLogger,
23
- useShutdownHandler,
24
- } from '@arcanewizards/sigil';
25
-
26
- import { createTCNetNode } from '@arcanewizards/tcnet';
27
- import { createTCNetTimecodeMonitor } from '@arcanewizards/tcnet/monitor';
28
- import {
29
- TCNetConnectedNodes,
30
- TCNetPortUsage,
31
- } from '@arcanewizards/tcnet/types';
32
- import { NetworkPortStatus } from '@arcanewizards/net-utils';
33
- import { StateSensitiveComponentProps } from '../types';
34
-
35
- type TcnetInputConnectionProps = StateSensitiveComponentProps & {
36
- uuid: string;
37
- config: InputConfig;
38
- connection: InputTcnetDefinition;
39
- };
40
-
41
- const TcnetInputConnection: FC<TcnetInputConnectionProps> = ({
42
- uuid,
43
- config: { name, delayMs },
44
- connection: { iface, nodeName },
45
- setState,
46
- }) => {
47
- const logger = useLogger();
48
-
49
- const appInformation = useContext(AppInformationContext);
50
-
51
- const nodeRef = useRef<ReturnType<typeof createTCNetNode> | null>(null);
52
-
53
- /**
54
- * Variable that can be set to prevent further updates,
55
- * in particular when unmounting to prevent updates from this function
56
- * once it is unmounted,
57
- * since further updates may come from the TCNet node after it's been destroyed.
58
- */
59
- const isMountedRef = useRef(true);
60
-
61
- // Use ref here to allow for updates without requiring re-init of node
62
- const delayRef = useRef(delayMs ?? 0);
63
-
64
- useEffect(() => {
65
- delayRef.current = delayMs ?? 0;
66
- }, [delayMs]);
67
-
68
- const setConnection = useCallback(
69
- (state: InputState) =>
70
- setState((current) => ({
71
- ...current,
72
- inputs: {
73
- ...current.inputs,
74
- [uuid]: state,
75
- },
76
- })),
77
- [setState, uuid],
78
- );
79
-
80
- /**
81
- * TCNet has multiple connections / ports,
82
- * this convenience function ensures that all the required ports are kept up-to-date,
83
- * and have unique IDs
84
- */
85
- const updateState = useMemo(() => {
86
- return (
87
- connections: Record<TCNetPortUsage, NetworkPortStatus | null>,
88
- nodes: TCNetConnectedNodes,
89
- timecodeGroup: TimecodeGroup,
90
- ) => {
91
- if (!isMountedRef.current) {
92
- return;
93
- }
94
- const warnings =
95
- Object.values(nodes).length === 0
96
- ? ['No other TCNet nodes detected on the network']
97
- : [];
98
- const clients: ConnectedClient[] = Object.entries(nodes)
99
- .sort(([nodeIdA], [nodeIdB]) => nodeIdA.localeCompare(nodeIdB))
100
- .map(([_, nodeInfo]) => ({
101
- name: nodeInfo.nodeName,
102
- host: nodeInfo.host,
103
- port: nodeInfo.nodeListenerPort,
104
- protocolVersion: nodeInfo.protocolVersion,
105
- details: [`Type: ${nodeInfo.nodeType}`],
106
- }));
107
- const hasError = Object.values(connections).some(
108
- (port) => port?.status === 'error',
109
- );
110
- const isConnecting = Object.values(connections).some(
111
- (port) => port?.status === 'connecting',
112
- );
113
- setConnection({
114
- status: hasError ? 'error' : isConnecting ? 'connecting' : 'active',
115
- clients,
116
- warnings,
117
- timecode: timecodeGroup,
118
- });
119
- };
120
- }, [setConnection]);
121
-
122
- useEffect(() => {
123
- const node = createTCNetNode({
124
- logger,
125
- networkInterface: iface,
126
- nodeName: nodeName?.substring(0, 8) ?? 'TC-TLBOX',
127
- vendorName: 'Arcane Wizards',
128
- appName: appInformation.title.substring(0, 16),
129
- appVersion: appInformation.version.substring(0, 16),
130
- });
131
- nodeRef.current = node;
132
-
133
- let lastPortInformation = node.getPortInformation();
134
- let lastNodes: TCNetConnectedNodes = {};
135
- let timecodeGroup: TimecodeGroup = {
136
- name: null,
137
- color: null,
138
- timecodes: {},
139
- };
140
-
141
- const updateConnectionsState = () => {
142
- updateState(lastPortInformation, lastNodes, timecodeGroup);
143
- };
144
-
145
- updateConnectionsState();
146
-
147
- node.on('port-state-changed', (info) => {
148
- lastPortInformation = info;
149
- updateConnectionsState();
150
- });
151
-
152
- node.on('nodes-changed', (nodes) => {
153
- lastNodes = nodes;
154
- updateConnectionsState();
155
- });
156
-
157
- node.on('ready', () => {
158
- logger.info(`TCNet node ${uuid} is ready`);
159
- });
160
-
161
- const monitor = createTCNetTimecodeMonitor(node, logger);
162
-
163
- monitor.addListener(
164
- 'timecode-changed',
165
- ({ layerId, playState, ...timecodeState }) => {
166
- timecodeGroup = {
167
- ...timecodeGroup,
168
- timecodes: {
169
- ...timecodeGroup.timecodes,
170
- [layerId]: {
171
- name: timecodeState.layerName,
172
- metadata: {
173
- totalTime: timecodeState.totalTime,
174
- title: timecodeState?.info?.title ?? null,
175
- artist: timecodeState?.info?.artist ?? null,
176
- },
177
- state:
178
- playState.state === 'playing'
179
- ? {
180
- state: 'playing',
181
- effectiveStartTimeMillis:
182
- playState.effectiveStartTime + delayRef.current,
183
- speed: playState.speed,
184
- onAir: playState.onAir,
185
- accuracyMillis: null,
186
- smpteMode: null,
187
- }
188
- : {
189
- state: 'stopped',
190
- positionMillis:
191
- playState.currentTimeMillis - delayRef.current,
192
- onAir: playState.onAir,
193
- accuracyMillis: null,
194
- smpteMode: null,
195
- },
196
- },
197
- },
198
- };
199
- updateConnectionsState();
200
- },
201
- );
202
-
203
- monitor.addListener('layer-removed', ({ layerId }) => {
204
- logger.info(`Layer removed from node ${uuid} layer ${layerId}`);
205
- timecodeGroup = {
206
- ...timecodeGroup,
207
- timecodes: Object.fromEntries(
208
- Object.entries(timecodeGroup.timecodes).filter(
209
- ([layerIdKey]) => layerIdKey !== layerId,
210
- ),
211
- ),
212
- };
213
- updateConnectionsState();
214
- });
215
-
216
- node.connect();
217
-
218
- return () => {
219
- logger.info(`Destroying TCNet connection ${uuid}...`);
220
- node.destroy();
221
- if (nodeRef.current === node) {
222
- nodeRef.current = null;
223
- }
224
- };
225
- }, [uuid, iface, nodeName, logger, appInformation, updateState]);
226
-
227
- useShutdownHandler(async () => {
228
- if (nodeRef.current) {
229
- logger.info(`Shutting down TCNet node ${name ?? uuid}...`);
230
- await nodeRef.current.destroy();
231
- }
232
- });
233
-
234
- useEffect(() => {
235
- return () => {
236
- // Prevent the connection state being re-added with delayed updates from node
237
- isMountedRef.current = false;
238
- // Remove the connection when it's no longer mounted / configured
239
- setState((current) => {
240
- const { [uuid]: _, ...rest } = current.inputs;
241
- return {
242
- ...current,
243
- inputs: rest,
244
- };
245
- });
246
- };
247
- }, [setState, uuid]);
248
- return null;
249
- };
250
-
251
- export const TcNetInputConnections: FC<StateSensitiveComponentProps> = (
252
- props,
253
- ) => {
254
- const { inputs } = useDataFileData(ToolboxConfigData);
255
- return Object.entries(inputs)
256
- .filter(([_, { enabled }]) => enabled)
257
- .map<ReactNode>(([uuid, input]) => {
258
- const connection = input.definition;
259
- if (!isInputTcnetDefinition(connection)) {
260
- return null;
261
- }
262
- return (
263
- <TcnetInputConnection
264
- key={uuid}
265
- uuid={uuid}
266
- config={input}
267
- connection={connection}
268
- {...props}
269
- />
270
- );
271
- });
272
- };