@arcanewizards/timecode-toolbox 0.0.3
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.
- package/.turbo/turbo-build.log +55 -0
- package/CHANGELOG.md +24 -0
- package/eslint.config.mjs +49 -0
- package/package.json +74 -0
- package/src/app.tsx +147 -0
- package/src/components/backend/index.ts +6 -0
- package/src/components/backend/toolbox-root.ts +119 -0
- package/src/components/frontend/constants.ts +81 -0
- package/src/components/frontend/entrypoint.ts +12 -0
- package/src/components/frontend/frontend.css +108 -0
- package/src/components/frontend/index.tsx +46 -0
- package/src/components/frontend/toolbox/content.tsx +45 -0
- package/src/components/frontend/toolbox/context.tsx +63 -0
- package/src/components/frontend/toolbox/core/size-aware-div.tsx +51 -0
- package/src/components/frontend/toolbox/core/timecode-display.tsx +592 -0
- package/src/components/frontend/toolbox/generators.tsx +318 -0
- package/src/components/frontend/toolbox/inputs.tsx +484 -0
- package/src/components/frontend/toolbox/outputs.tsx +581 -0
- package/src/components/frontend/toolbox/preferences.ts +25 -0
- package/src/components/frontend/toolbox/root.tsx +335 -0
- package/src/components/frontend/toolbox/settings.tsx +54 -0
- package/src/components/frontend/toolbox/types.ts +28 -0
- package/src/components/frontend/toolbox/util.tsx +61 -0
- package/src/components/proto.ts +420 -0
- package/src/config.ts +7 -0
- package/src/generators/clock.tsx +206 -0
- package/src/generators/index.tsx +15 -0
- package/src/index.ts +38 -0
- package/src/inputs/artnet.tsx +305 -0
- package/src/inputs/index.tsx +13 -0
- package/src/inputs/tcnet.tsx +272 -0
- package/src/outputs/artnet.tsx +170 -0
- package/src/outputs/index.tsx +11 -0
- package/src/start.ts +47 -0
- package/src/tree.ts +133 -0
- package/src/types.ts +12 -0
- package/src/urls.ts +49 -0
- package/src/util.ts +82 -0
- package/tailwind.config.cjs +7 -0
- package/tsconfig.json +10 -0
- package/tsup.config.ts +10 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { useDataFileData } from '@arcanejs/react-toolkit/data';
|
|
2
|
+
import {
|
|
3
|
+
FC,
|
|
4
|
+
ReactNode,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { ToolboxConfigData } from '../config';
|
|
11
|
+
import {
|
|
12
|
+
ApplicationState,
|
|
13
|
+
OutputArtnetDefinition,
|
|
14
|
+
OutputConfig,
|
|
15
|
+
OutputState,
|
|
16
|
+
isOutputArtnetDefinition,
|
|
17
|
+
} from '../components/proto';
|
|
18
|
+
import { adjustTimecodeForDelay, getTimecodeInstance } from '../util';
|
|
19
|
+
import { useLogger } from '@arcanewizards/sigil';
|
|
20
|
+
import { ArtNet, createArtnet } from '@arcanewizards/artnet';
|
|
21
|
+
import { TIMECODE_FPS } from '@arcanewizards/artnet/constants';
|
|
22
|
+
import { StateSensitiveComponentProps } from '../types';
|
|
23
|
+
|
|
24
|
+
type ArtnetOutputConnectionProps = StateSensitiveComponentProps & {
|
|
25
|
+
uuid: string;
|
|
26
|
+
config: OutputConfig;
|
|
27
|
+
connection: OutputArtnetDefinition;
|
|
28
|
+
state: ApplicationState;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const ArtnetOutputConnection: FC<ArtnetOutputConnectionProps> = ({
|
|
32
|
+
uuid,
|
|
33
|
+
config,
|
|
34
|
+
connection: { target, mode },
|
|
35
|
+
setState,
|
|
36
|
+
state,
|
|
37
|
+
}) => {
|
|
38
|
+
const log = useLogger();
|
|
39
|
+
|
|
40
|
+
const [artnetInstance, setArtnetInstance] = useState<ArtNet | null>(null);
|
|
41
|
+
|
|
42
|
+
const setConnection = useCallback(
|
|
43
|
+
(state: OutputState) =>
|
|
44
|
+
setState((current) => ({
|
|
45
|
+
...current,
|
|
46
|
+
outputs: {
|
|
47
|
+
...current.outputs,
|
|
48
|
+
[uuid]: state,
|
|
49
|
+
},
|
|
50
|
+
})),
|
|
51
|
+
[setState, uuid],
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
let artnet: ArtNet | null = null;
|
|
56
|
+
setConnection({ status: 'connecting' });
|
|
57
|
+
const created = createArtnet({
|
|
58
|
+
mode: 'send',
|
|
59
|
+
...target,
|
|
60
|
+
});
|
|
61
|
+
created.on('error', (err) => {
|
|
62
|
+
const error = new Error('ArtNet output connection error');
|
|
63
|
+
error.cause = err instanceof Error ? err : new Error(String(err));
|
|
64
|
+
log.error(error);
|
|
65
|
+
setConnection({
|
|
66
|
+
status: 'error',
|
|
67
|
+
errors: [`${err}`],
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
created
|
|
71
|
+
.connect()
|
|
72
|
+
.then(() => {
|
|
73
|
+
artnet = created;
|
|
74
|
+
setArtnetInstance(created);
|
|
75
|
+
log.info(`ArtNet Timecode output initialized`);
|
|
76
|
+
setConnection({ status: 'active' });
|
|
77
|
+
})
|
|
78
|
+
.catch((err) => {
|
|
79
|
+
const error = new Error('Failed to start ArtNet Timecode output');
|
|
80
|
+
error.cause = err instanceof Error ? err : new Error(String(err));
|
|
81
|
+
log.error(error);
|
|
82
|
+
setConnection({
|
|
83
|
+
status: 'error',
|
|
84
|
+
errors: [`${err}`],
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return () => {
|
|
89
|
+
if (artnet) {
|
|
90
|
+
artnet.destroy();
|
|
91
|
+
setArtnetInstance((current) => (artnet === current ? null : current));
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}, [setConnection, uuid, target, log]);
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
return () => {
|
|
98
|
+
// Remove the connection when it's no longer mounted / configured
|
|
99
|
+
setState((current) => {
|
|
100
|
+
const { [uuid]: _, ...rest } = current.outputs;
|
|
101
|
+
return {
|
|
102
|
+
...current,
|
|
103
|
+
outputs: rest,
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
}, [setState, uuid]);
|
|
108
|
+
|
|
109
|
+
const tcInstance = useMemo(
|
|
110
|
+
() => config.link && getTimecodeInstance(state, config.link),
|
|
111
|
+
[state, config.link],
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const timecodeState = useMemo(
|
|
115
|
+
() =>
|
|
116
|
+
tcInstance?.state
|
|
117
|
+
? adjustTimecodeForDelay(tcInstance.state, config.delayMs ?? 0)
|
|
118
|
+
: null,
|
|
119
|
+
[tcInstance?.state, config.delayMs],
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!artnetInstance) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
timecodeState?.state === 'playing' ||
|
|
129
|
+
timecodeState?.state === 'lagging'
|
|
130
|
+
) {
|
|
131
|
+
const tcState = timecodeState;
|
|
132
|
+
const interval = setInterval(() => {
|
|
133
|
+
const time =
|
|
134
|
+
(Date.now() - tcState.effectiveStartTimeMillis) * tcState.speed;
|
|
135
|
+
artnetInstance.sendTimecode(mode, time);
|
|
136
|
+
}, 1000 / TIMECODE_FPS[mode]);
|
|
137
|
+
return () => {
|
|
138
|
+
clearInterval(interval);
|
|
139
|
+
};
|
|
140
|
+
} else if (timecodeState?.state === 'stopped') {
|
|
141
|
+
artnetInstance.sendTimecode(mode, timecodeState?.positionMillis ?? 0);
|
|
142
|
+
}
|
|
143
|
+
}, [artnetInstance, mode, timecodeState]);
|
|
144
|
+
|
|
145
|
+
return null;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const ArtnetOutputConnections: FC<StateSensitiveComponentProps> = (
|
|
149
|
+
props,
|
|
150
|
+
) => {
|
|
151
|
+
const { outputs } = useDataFileData(ToolboxConfigData);
|
|
152
|
+
|
|
153
|
+
return Object.entries(outputs)
|
|
154
|
+
.filter(([_, { enabled }]) => enabled)
|
|
155
|
+
.map<ReactNode>(([uuid, output]) => {
|
|
156
|
+
const definition = output.definition;
|
|
157
|
+
if (!isOutputArtnetDefinition(definition)) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return (
|
|
161
|
+
<ArtnetOutputConnection
|
|
162
|
+
key={uuid}
|
|
163
|
+
uuid={uuid}
|
|
164
|
+
config={output}
|
|
165
|
+
connection={definition}
|
|
166
|
+
{...props}
|
|
167
|
+
/>
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
import { ArtnetOutputConnections } from './artnet';
|
|
3
|
+
import { StateSensitiveComponentProps } from '../types';
|
|
4
|
+
|
|
5
|
+
export const OutputConnections: FC<StateSensitiveComponentProps> = (props) => {
|
|
6
|
+
return (
|
|
7
|
+
<>
|
|
8
|
+
<ArtnetOutputConnections {...props} />
|
|
9
|
+
</>
|
|
10
|
+
);
|
|
11
|
+
};
|
package/src/start.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import pino from 'pino';
|
|
3
|
+
import { runTimecodeToolboxServer } from '.';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
|
|
6
|
+
const DATA_DIR = path.join(homedir(), 'timecode-toolbox');
|
|
7
|
+
|
|
8
|
+
const logger = pino({
|
|
9
|
+
level: 'info',
|
|
10
|
+
transport: {
|
|
11
|
+
target: 'pino-pretty',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const server = runTimecodeToolboxServer({
|
|
16
|
+
logger,
|
|
17
|
+
appProps: {
|
|
18
|
+
dataDirectory: DATA_DIR,
|
|
19
|
+
},
|
|
20
|
+
toolkitOptions: {
|
|
21
|
+
entrypointJsFile: path.join(path.dirname(__dirname), 'dist/entrypoint.js'),
|
|
22
|
+
},
|
|
23
|
+
title: 'Timecode Toolbox Server',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const shutdown = () =>
|
|
27
|
+
server
|
|
28
|
+
.shutdown()
|
|
29
|
+
.catch((err) => {
|
|
30
|
+
logger.error({ err }, 'Error during shutdown');
|
|
31
|
+
})
|
|
32
|
+
.finally(() => {
|
|
33
|
+
process.exit(0);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Catch SIGINT and SIGTERM to allow for graceful shutdown
|
|
37
|
+
process.on('SIGINT', async () => {
|
|
38
|
+
logger.info('Received SIGINT, shutting down...');
|
|
39
|
+
shutdown();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
process.on('SIGTERM', () => {
|
|
43
|
+
logger.info('Received SIGTERM, shutting down...');
|
|
44
|
+
shutdown();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export {};
|
package/src/tree.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
export type TreeLeaf<T> = {
|
|
2
|
+
value: T;
|
|
3
|
+
children?: undefined;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type TreeNode<T> = {
|
|
7
|
+
value?: T;
|
|
8
|
+
children: Record<string, Tree<T>>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type Tree<T> = TreeLeaf<T> | TreeNode<T>;
|
|
12
|
+
|
|
13
|
+
export const isTreeNode = <T>(node: Tree<T>): node is TreeNode<T> =>
|
|
14
|
+
'children' in node &&
|
|
15
|
+
typeof node.children === 'object' &&
|
|
16
|
+
node.children !== null;
|
|
17
|
+
|
|
18
|
+
export const updateTreeState = <T>(
|
|
19
|
+
current: Tree<T>,
|
|
20
|
+
path: string[],
|
|
21
|
+
value: T,
|
|
22
|
+
): Tree<T> => {
|
|
23
|
+
if (path.length === 0) {
|
|
24
|
+
return {
|
|
25
|
+
...current,
|
|
26
|
+
value,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const [nextKey, ...remainingPath] = path;
|
|
30
|
+
if (!nextKey) {
|
|
31
|
+
throw new Error('Invalid path');
|
|
32
|
+
}
|
|
33
|
+
if (remainingPath.length === 0) {
|
|
34
|
+
// Update the child directly
|
|
35
|
+
return {
|
|
36
|
+
...current,
|
|
37
|
+
children: {
|
|
38
|
+
...current.children,
|
|
39
|
+
[nextKey]: {
|
|
40
|
+
value,
|
|
41
|
+
children: current.children?.[nextKey]?.children,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Path is not empty, need to recurse
|
|
47
|
+
const children = isTreeNode(current) ? current.children : {};
|
|
48
|
+
return {
|
|
49
|
+
...current,
|
|
50
|
+
children: {
|
|
51
|
+
...children,
|
|
52
|
+
[nextKey]: updateTreeState(
|
|
53
|
+
children[nextKey] ?? { children: {} },
|
|
54
|
+
remainingPath,
|
|
55
|
+
value,
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const getTreeValue = <T>(current: Tree<T>, path: string[]): T | null => {
|
|
62
|
+
if (path.length === 0) {
|
|
63
|
+
return current.value ?? null;
|
|
64
|
+
}
|
|
65
|
+
const [nextKey, ...remainingPath] = path;
|
|
66
|
+
if (!nextKey) {
|
|
67
|
+
throw new Error('Invalid path');
|
|
68
|
+
}
|
|
69
|
+
if (isTreeNode(current)) {
|
|
70
|
+
return getTreeValue(
|
|
71
|
+
current.children[nextKey] ?? { children: {} },
|
|
72
|
+
remainingPath,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const deleteTreePath = <T>(
|
|
79
|
+
current: Tree<T>,
|
|
80
|
+
path: string[],
|
|
81
|
+
): Tree<T> => {
|
|
82
|
+
if (path.length === 0) {
|
|
83
|
+
throw new Error('Cannot delete root of the tree');
|
|
84
|
+
}
|
|
85
|
+
const [nextKey, ...remainingPath] = path;
|
|
86
|
+
if (!nextKey) {
|
|
87
|
+
throw new Error('Invalid path');
|
|
88
|
+
}
|
|
89
|
+
if (isTreeNode(current)) {
|
|
90
|
+
if (remainingPath.length === 0) {
|
|
91
|
+
// Delete the child directly
|
|
92
|
+
const { [nextKey]: _, ...remainingChildren } = current.children;
|
|
93
|
+
return {
|
|
94
|
+
...current,
|
|
95
|
+
children: remainingChildren,
|
|
96
|
+
};
|
|
97
|
+
} else {
|
|
98
|
+
// Recurse into the child
|
|
99
|
+
return {
|
|
100
|
+
...current,
|
|
101
|
+
children: {
|
|
102
|
+
...current.children,
|
|
103
|
+
[nextKey]: deleteTreePath(
|
|
104
|
+
current.children[nextKey] ?? { children: {} },
|
|
105
|
+
remainingPath,
|
|
106
|
+
),
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return current;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const mapTree = <T, U>(
|
|
115
|
+
current: Tree<T>,
|
|
116
|
+
fn: (value: T) => U,
|
|
117
|
+
): Tree<U> => {
|
|
118
|
+
if (isTreeNode(current)) {
|
|
119
|
+
return {
|
|
120
|
+
value: current.value ? fn(current.value) : undefined,
|
|
121
|
+
children: Object.fromEntries(
|
|
122
|
+
Object.entries(current.children).map(([key, child]) => [
|
|
123
|
+
key,
|
|
124
|
+
mapTree(child, fn),
|
|
125
|
+
]),
|
|
126
|
+
),
|
|
127
|
+
};
|
|
128
|
+
} else {
|
|
129
|
+
return {
|
|
130
|
+
value: fn(current.value),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Dispatch, SetStateAction } from 'react';
|
|
2
|
+
import { ApplicationState, TimecodeHandlerMethods } from './components/proto';
|
|
3
|
+
import { Tree } from './tree';
|
|
4
|
+
|
|
5
|
+
export type TimecodeHandlers = Tree<TimecodeHandlerMethods>;
|
|
6
|
+
|
|
7
|
+
export type StateSensitiveComponentProps = {
|
|
8
|
+
state: ApplicationState;
|
|
9
|
+
setState: Dispatch<SetStateAction<ApplicationState>>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type HandlersUpdater = Dispatch<SetStateAction<TimecodeHandlers>>;
|
package/src/urls.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import { TimecodeInstanceId } from './components/proto';
|
|
3
|
+
|
|
4
|
+
export const TIMECODE_PATH_FRAGMENT = 'tc';
|
|
5
|
+
|
|
6
|
+
export const WINDOW_MODE_TIMECODE = 'timecode';
|
|
7
|
+
|
|
8
|
+
type FragmentValues = {
|
|
9
|
+
tc?: TimecodeInstanceId;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type WithUrlFragmentArgs = {
|
|
13
|
+
location?: Location | URL;
|
|
14
|
+
values: FragmentValues;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const withUrlFragment = ({ location, values }: WithUrlFragmentArgs) => {
|
|
18
|
+
const url = new URL(location ? location.href : window.location.href);
|
|
19
|
+
const fragmentParams = new URLSearchParams();
|
|
20
|
+
|
|
21
|
+
for (const [key, value] of Object.entries(values)) {
|
|
22
|
+
if (value === null) {
|
|
23
|
+
fragmentParams.delete(key);
|
|
24
|
+
} else {
|
|
25
|
+
fragmentParams.set(key, JSON.stringify(value));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
url.hash = `#${fragmentParams.toString()}`;
|
|
29
|
+
return url;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const getFragmentValue = <K extends keyof FragmentValues>(
|
|
33
|
+
key: K,
|
|
34
|
+
zodParser: z.ZodType<NonNullable<FragmentValues[K]>>,
|
|
35
|
+
): NonNullable<FragmentValues[K]> | null => {
|
|
36
|
+
const url = new URL(window.location.href);
|
|
37
|
+
const fragmentParams = new URLSearchParams(url.hash.slice(1));
|
|
38
|
+
const value = fragmentParams.get(key);
|
|
39
|
+
if (!value) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
return zodParser.parse(JSON.parse(value));
|
|
44
|
+
} catch (e) {
|
|
45
|
+
// eslint-disable-next-line no-console
|
|
46
|
+
console.error(`Error parsing URL fragment value for key ${key}:`, e);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
};
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ApplicationState,
|
|
3
|
+
InputOrGenInstance,
|
|
4
|
+
isTimecodeGroup,
|
|
5
|
+
isTimecodeInstance,
|
|
6
|
+
OutputConfig,
|
|
7
|
+
TimecodeInstance,
|
|
8
|
+
TimecodeState,
|
|
9
|
+
} from './components/proto';
|
|
10
|
+
|
|
11
|
+
export const isKeyedEntry =
|
|
12
|
+
<K, I, O extends I>(typeGuard: (value: I) => value is O) =>
|
|
13
|
+
(entry: [K, I]): entry is [K, O] =>
|
|
14
|
+
entry.length === 2 && typeGuard(entry[1]);
|
|
15
|
+
|
|
16
|
+
export const getTimecodeInstance = (
|
|
17
|
+
state: ApplicationState,
|
|
18
|
+
id: InputOrGenInstance,
|
|
19
|
+
): TimecodeInstance | null => {
|
|
20
|
+
const [type, firstId, ...remainingPath] = id;
|
|
21
|
+
if (!firstId) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
let current =
|
|
25
|
+
state[type === 'input' ? 'inputs' : 'generators'][firstId]?.timecode ??
|
|
26
|
+
null;
|
|
27
|
+
for (const idPart of remainingPath) {
|
|
28
|
+
if (!current || !isTimecodeGroup(current)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
current = current.timecodes[idPart] ?? null;
|
|
32
|
+
}
|
|
33
|
+
if (isTimecodeInstance(current)) {
|
|
34
|
+
return current;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const adjustTimecodeForDelay = (
|
|
40
|
+
state: TimecodeState,
|
|
41
|
+
delayMillis: number,
|
|
42
|
+
): TimecodeState => {
|
|
43
|
+
if (state.state === 'playing' || state.state === 'lagging') {
|
|
44
|
+
return {
|
|
45
|
+
...state,
|
|
46
|
+
effectiveStartTimeMillis: state.effectiveStartTimeMillis + delayMillis,
|
|
47
|
+
};
|
|
48
|
+
} else if (state.state === 'stopped') {
|
|
49
|
+
return {
|
|
50
|
+
...state,
|
|
51
|
+
positionMillis: state.positionMillis - delayMillis,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return state;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const augmentUpstreamTimecodeWithOutputMetadata = (
|
|
58
|
+
tc: TimecodeInstance | null,
|
|
59
|
+
config: OutputConfig,
|
|
60
|
+
): TimecodeInstance => {
|
|
61
|
+
if (!tc) {
|
|
62
|
+
return {
|
|
63
|
+
name: null,
|
|
64
|
+
metadata: null,
|
|
65
|
+
state: {
|
|
66
|
+
state: 'none',
|
|
67
|
+
accuracyMillis: null,
|
|
68
|
+
smpteMode: config.definition.mode,
|
|
69
|
+
onAir: null,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Adjust the timecode instance with output-specific metadata
|
|
74
|
+
return {
|
|
75
|
+
name: null,
|
|
76
|
+
metadata: tc.metadata,
|
|
77
|
+
state: {
|
|
78
|
+
...adjustTimecodeForDelay(tc.state, config.delayMs ?? 0),
|
|
79
|
+
smpteMode: config.definition.mode,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
};
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED