@nonstrict/recordkit 0.87.1 → 0.97.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 (54) hide show
  1. package/bin/README.md +1 -1
  2. package/bin/recordkit-rpc +0 -0
  3. package/out/Errors.d.ts +91 -0
  4. package/out/Errors.js +63 -0
  5. package/out/Errors.js.map +1 -0
  6. package/out/InputEvents.d.ts +661 -0
  7. package/out/InputEvents.js +8 -0
  8. package/out/InputEvents.js.map +1 -0
  9. package/out/IpcRecordKit.js +13 -0
  10. package/out/IpcRecordKit.js.map +1 -1
  11. package/out/NonstrictRPC.d.ts +9 -0
  12. package/out/NonstrictRPC.js +27 -4
  13. package/out/NonstrictRPC.js.map +1 -1
  14. package/out/RecordKit.d.ts +267 -5
  15. package/out/RecordKit.js +243 -3
  16. package/out/RecordKit.js.map +1 -1
  17. package/out/Recorder.d.ts +451 -53
  18. package/out/Recorder.js +80 -2
  19. package/out/Recorder.js.map +1 -1
  20. package/out/RecordingMetadata.d.ts +96 -0
  21. package/out/RecordingMetadata.js +12 -0
  22. package/out/RecordingMetadata.js.map +1 -0
  23. package/out/WebAudioUtils.d.ts +35 -0
  24. package/out/WebAudioUtils.js +37 -0
  25. package/out/WebAudioUtils.js.map +1 -1
  26. package/out/WindowLevels.d.ts +46 -0
  27. package/out/WindowLevels.js +41 -0
  28. package/out/WindowLevels.js.map +1 -0
  29. package/out/browser.d.ts +8 -1
  30. package/out/browser.js +3 -1
  31. package/out/browser.js.map +1 -1
  32. package/out/index.cjs +603 -9
  33. package/out/index.cjs.map +1 -1
  34. package/out/index.d.ts +8 -0
  35. package/out/index.js +3 -0
  36. package/out/index.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/Errors.test.ts +38 -0
  39. package/src/Errors.ts +151 -0
  40. package/src/InputEvents.ts +695 -0
  41. package/src/IpcRecordKit.ts +12 -0
  42. package/src/NonstrictRPC.test.ts +24 -1
  43. package/src/NonstrictRPC.ts +29 -5
  44. package/src/RecordKit.ts +347 -11
  45. package/src/Recorder.schema.test.ts +167 -0
  46. package/src/Recorder.ts +558 -80
  47. package/src/RecordingMetadata.ts +125 -0
  48. package/src/WebAudioUtils.test.ts +78 -0
  49. package/src/WebAudioUtils.ts +57 -1
  50. package/src/WindowLevels.test.ts +34 -0
  51. package/src/WindowLevels.ts +47 -0
  52. package/src/browser.ts +12 -2
  53. package/src/index.ts +8 -0
  54. package/src/__snapshots__/NonstrictRPC.test.ts.snap +0 -24
@@ -0,0 +1,125 @@
1
+ // TypeScript mirrors of the Swift `Codable` types whose JSON is written to sidecar files inside a
2
+ // RecordKit recording bundle. Consumers parse those JSON files and rely on these types to describe
3
+ // their shape: property names are verbatim camelCase, Swift optionals become optional properties,
4
+ // and Swift tagged-union enums encode as flat objects discriminated by a `type` field at the same
5
+ // level as the payload.
6
+ //
7
+ // Keep this file in sync with the corresponding Swift sources:
8
+ // - `RKWindowPresence.swift`
9
+ // - `RKVideoDimensionChange.swift`
10
+ // (The `RKBundleInfo` mirror, `BundleInfo`, lives in `Recorder.ts`.)
11
+
12
+ import type { EventTime } from './InputEvents.js';
13
+
14
+ /**
15
+ * Presence of a window at a point in time within a recording.
16
+ *
17
+ * Encoded as a flat object discriminated by `type`. Mirrors the Swift
18
+ * `RKWindowPresence` enum.
19
+ *
20
+ * @remarks
21
+ * The `present` member ({@link WindowInfo}) carries the window's position,
22
+ * size and identifying metadata at that moment; the `absent` member
23
+ * ({@link WindowAbsence}) marks a time at which the tracked window was not
24
+ * present (e.g. closed, minimized, or moved to another Space).
25
+ *
26
+ * @group Recording
27
+ */
28
+ export type WindowPresence = WindowInfo | WindowAbsence;
29
+
30
+ /**
31
+ * Information about a window that is present at a point in time within a recording.
32
+ *
33
+ * Mirrors the Swift `RKWindowInfo` struct (the `present` case of
34
+ * `RKWindowPresence`).
35
+ *
36
+ * @group Recording
37
+ */
38
+ export interface WindowInfo {
39
+ type: 'present';
40
+
41
+ /** Time the event occurred in the recording. */
42
+ time: EventTime;
43
+
44
+ /** ID of the window. */
45
+ windowID: number;
46
+
47
+ /** Title of the window. */
48
+ windowTitle?: string;
49
+
50
+ /**
51
+ * ID of the macOS Space where this window lives (can be used to detect space changes).
52
+ *
53
+ * @remarks Mirrors a Swift `UInt64`; for very large Space IDs this can exceed JavaScript's
54
+ * `Number.MAX_SAFE_INTEGER`. ({@link WindowInfo.windowID} and {@link WindowInfo.applicationID}
55
+ * are 32-bit and not affected.)
56
+ */
57
+ spaceID?: number;
58
+
59
+ /** ID of the application the window belongs to. */
60
+ applicationID?: number;
61
+
62
+ /** Title of the application the window belongs to. */
63
+ applicationName?: string;
64
+
65
+ /** X-axis position of the window relative to the top left of recorded area, ranging from 0 to 1. */
66
+ x: number;
67
+
68
+ /** Y-axis position of the window relative to the top left of recorded area, ranging from 0 to 1. */
69
+ y: number;
70
+
71
+ /** Width of the window relative to the top left of recorded area, ranging from 0 to 1. */
72
+ width: number;
73
+
74
+ /** Height of the window relative to the top left of recorded area, ranging from 0 to 1. */
75
+ height: number;
76
+ }
77
+
78
+ /**
79
+ * Marks the absence of a window at a point in time within a recording.
80
+ *
81
+ * Mirrors the Swift `RKWindowAbsence` struct (the `absent` case of
82
+ * `RKWindowPresence`).
83
+ *
84
+ * @group Recording
85
+ */
86
+ export interface WindowAbsence {
87
+ type: 'absent';
88
+
89
+ /** Time the event occurred in the recording. */
90
+ time: EventTime;
91
+ }
92
+
93
+ /**
94
+ * An event representing a video dimension change during recording.
95
+ * This can be caused by device rotation or other content size changes.
96
+ *
97
+ * Mirrors the Swift `RKVideoDimensionChange` struct.
98
+ *
99
+ * @group Recording
100
+ */
101
+ export interface VideoDimensionChange {
102
+ /** Time the dimension change occurred in the recording. */
103
+ time: EventTime;
104
+
105
+ /** The original (pre-rotation) dimensions of the content, in pixels. */
106
+ dimensions: {
107
+ /** Content width in pixels. */
108
+ width: number;
109
+ /** Content height in pixels. */
110
+ height: number;
111
+ };
112
+
113
+ /**
114
+ * The rotation in degrees (0, 90, 180, or -90) that a player should apply to display
115
+ * this segment correctly. Positive values are clockwise, negative are counterclockwise.
116
+ */
117
+ rotation: number;
118
+
119
+ /**
120
+ * Whether the player should animate the transition to this rotation.
121
+ * `true` when device rotated during continuous recording.
122
+ * `false` at recording start or after pause/resume (instant change).
123
+ */
124
+ animated: boolean;
125
+ }
@@ -0,0 +1,78 @@
1
+ import { computeAudioLevel, createWebAudioBuffer } from './WebAudioUtils.js';
2
+ import type { AudioStreamBuffer } from './Recorder.js';
3
+
4
+ function buf(channelData: number[][], sampleRate = 48000): AudioStreamBuffer {
5
+ return {
6
+ sampleRate,
7
+ numberOfChannels: channelData.length,
8
+ numberOfFrames: channelData[0]?.length ?? 0,
9
+ channelData: channelData.map((c) => Float32Array.from(c)),
10
+ };
11
+ }
12
+
13
+ describe('computeAudioLevel', () => {
14
+ it('reports silence as zero amplitude and -Infinity dB', () => {
15
+ const level = computeAudioLevel(buf([[0, 0, 0, 0]]));
16
+ expect(level.rms).toBe(0);
17
+ expect(level.peak).toBe(0);
18
+ expect(level.rmsDb).toBe(-Infinity);
19
+ expect(level.peakDb).toBe(-Infinity);
20
+ });
21
+
22
+ it('computes RMS and peak, and their dBFS equivalents', () => {
23
+ // constant magnitude 0.5 -> rms == peak == 0.5 -> ~ -6.02 dBFS
24
+ const level = computeAudioLevel(buf([[0.5, -0.5, 0.5, -0.5]]));
25
+ expect(level.rms).toBeCloseTo(0.5, 5);
26
+ expect(level.peak).toBeCloseTo(0.5, 5);
27
+ expect(level.rmsDb).toBeCloseTo(20 * Math.log10(0.5), 4);
28
+ expect(level.peakDb).toBeCloseTo(20 * Math.log10(0.5), 4);
29
+ });
30
+
31
+ it('takes the peak as the max absolute sample across all channels', () => {
32
+ const level = computeAudioLevel(buf([[0.1, -0.9], [0.3, 0.2]]));
33
+ expect(level.peak).toBeCloseTo(0.9, 5);
34
+ });
35
+
36
+ it('treats a full-scale signal as 0 dBFS', () => {
37
+ const level = computeAudioLevel(buf([[1, -1, 1, -1]]));
38
+ expect(level.peakDb).toBeCloseTo(0, 5);
39
+ });
40
+
41
+ it('handles an empty buffer without dividing by zero', () => {
42
+ const level = computeAudioLevel(buf([[]]));
43
+ expect(level.rms).toBe(0);
44
+ expect(level.peakDb).toBe(-Infinity);
45
+ });
46
+ });
47
+
48
+ describe('createWebAudioBuffer', () => {
49
+ // Minimal AudioContext stub: createBuffer returns an object backed by Float32Arrays.
50
+ function fakeContext(): AudioContext {
51
+ const channels: Float32Array[] = [];
52
+ return {
53
+ createBuffer(numberOfChannels: number, numberOfFrames: number, _sampleRate: number) {
54
+ for (let i = 0; i < numberOfChannels; i++) channels[i] = new Float32Array(numberOfFrames);
55
+ return { getChannelData: (i: number) => channels[i] };
56
+ },
57
+ } as unknown as AudioContext;
58
+ }
59
+
60
+ it('copies channel data into a created AudioBuffer', () => {
61
+ const out = createWebAudioBuffer(buf([[0.1, 0.2, 0.3]]), fakeContext());
62
+ expect(out).not.toBeNull();
63
+ const data = out!.getChannelData(0);
64
+ expect(data.length).toBe(3);
65
+ expect(data[0]).toBeCloseTo(0.1, 5);
66
+ expect(data[1]).toBeCloseTo(0.2, 5);
67
+ expect(data[2]).toBeCloseTo(0.3, 5);
68
+ });
69
+
70
+ it('returns null for a malformed buffer instead of throwing (safe in stream callbacks)', () => {
71
+ const bad = { sampleRate: 48000, numberOfChannels: 2, numberOfFrames: 4, channelData: [Float32Array.from([0, 0, 0, 0])] } as AudioStreamBuffer;
72
+ expect(createWebAudioBuffer(bad, fakeContext())).toBeNull();
73
+ });
74
+
75
+ it('returns null when no usable AudioContext is provided', () => {
76
+ expect(createWebAudioBuffer(buf([[0.1]]), undefined as unknown as AudioContext)).toBeNull();
77
+ });
78
+ });
@@ -105,4 +105,60 @@ export function createWebAudioBuffer(
105
105
  // Return null for any conversion failures - don't throw in streaming contexts
106
106
  return null;
107
107
  }
108
- }
108
+ }
109
+
110
+ /**
111
+ * An audio level measurement, as returned by {@link computeAudioLevel}.
112
+ *
113
+ * @group Recording
114
+ */
115
+ export interface AudioLevel {
116
+ /** Root-mean-square (average) amplitude across all channels. Usually in `[0, 1]`, but can exceed `1` for hot or clipped signals. */
117
+ rms: number
118
+ /** Peak absolute amplitude across all channels. Usually in `[0, 1]`, but can exceed `1` for hot or clipped signals. */
119
+ peak: number
120
+ /** {@link AudioLevel.rms} in decibels relative to full scale (0 dBFS). `-Infinity` for silence; can exceed `0` for clipped signals. */
121
+ rmsDb: number
122
+ /** {@link AudioLevel.peak} in decibels relative to full scale (0 dBFS). `-Infinity` for silence; can exceed `0` for clipped signals. */
123
+ peakDb: number
124
+ }
125
+
126
+ /**
127
+ * Computes RMS and peak audio levels from an {@link AudioStreamBuffer}, for rendering a microphone (or
128
+ * system-audio) level meter.
129
+ *
130
+ * Electron has no dedicated microphone-preview component; the supported way to render a live level meter
131
+ * is to use a microphone/system-audio `stream` output and call this helper in your `streamCallback`.
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * import { computeAudioLevel } from '@nonstrict/recordkit';
136
+ *
137
+ * const streamCallback = (audioBuffer) => {
138
+ * const { peakDb } = computeAudioLevel(audioBuffer);
139
+ * meterElement.style.height = `${Math.max(0, 100 + peakDb)}%`; // -100 dB..0 dB -> 0%..100%
140
+ * };
141
+ * ```
142
+ *
143
+ * @group Recording
144
+ */
145
+ export function computeAudioLevel(audioStreamBuffer: AudioStreamBuffer): AudioLevel {
146
+ let sumSquares = 0
147
+ let peak = 0
148
+ let count = 0
149
+
150
+ for (const channel of audioStreamBuffer.channelData) {
151
+ for (let i = 0; i < channel.length; i++) {
152
+ const sample = channel[i]
153
+ sumSquares += sample * sample
154
+ const abs = Math.abs(sample)
155
+ if (abs > peak) peak = abs
156
+ count++
157
+ }
158
+ }
159
+
160
+ const rms = count > 0 ? Math.sqrt(sumSquares / count) : 0
161
+ const toDb = (value: number) => value > 0 ? 20 * Math.log10(value) : -Infinity
162
+
163
+ return { rms, peak, rmsDb: toDb(rms), peakDb: toDb(peak) }
164
+ }
@@ -0,0 +1,34 @@
1
+ import { WINDOW_LEVELS } from './WindowLevels.js';
2
+
3
+ // Pins the named macOS window levels that mirror RKWindow.Level, including the documented
4
+ // shared-value aliases and front-to-back ordering.
5
+ describe('WINDOW_LEVELS', () => {
6
+ it('pins known macOS window level values', () => {
7
+ expect(WINDOW_LEVELS.baseWindow).toBe(-2147483648);
8
+ expect(WINDOW_LEVELS.normal).toBe(0);
9
+ expect(WINDOW_LEVELS.floating).toBe(3);
10
+ expect(WINDOW_LEVELS.mainMenu).toBe(24);
11
+ expect(WINDOW_LEVELS.screenSaver).toBe(1000);
12
+ expect(WINDOW_LEVELS.maximumWindow).toBe(2147483631);
13
+ });
14
+
15
+ it('keeps the documented shared-value aliases equal', () => {
16
+ expect(WINDOW_LEVELS.submenu).toBe(WINDOW_LEVELS.floating);
17
+ expect(WINDOW_LEVELS.tornOffMenu).toBe(WINDOW_LEVELS.floating);
18
+ expect(WINDOW_LEVELS.screenSaver).toBe(WINDOW_LEVELS.screenSaverWindow);
19
+ });
20
+
21
+ it('orders front-to-back by increasing value (Comparable in Swift)', () => {
22
+ expect(WINDOW_LEVELS.normal).toBeLessThan(WINDOW_LEVELS.floating);
23
+ expect(WINDOW_LEVELS.floating).toBeLessThan(WINDOW_LEVELS.mainMenu);
24
+ expect(WINDOW_LEVELS.mainMenu).toBeLessThan(WINDOW_LEVELS.cursorWindow);
25
+ });
26
+
27
+ it('every level is an integer within the signed 32-bit range', () => {
28
+ for (const v of Object.values(WINDOW_LEVELS)) {
29
+ expect(Number.isInteger(v)).toBe(true);
30
+ expect(v).toBeGreaterThanOrEqual(-2147483648);
31
+ expect(v).toBeLessThanOrEqual(2147483647);
32
+ }
33
+ });
34
+ });
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Named macOS window levels, mirroring RecordKit's `RKWindow.Level` Swift type.
3
+ *
4
+ * A {@link Window}'s `level` is a raw integer. macOS assigns windows to a small set of well-known
5
+ * levels; this map lets you compare a window's level against those named values, e.g.
6
+ *
7
+ * ```ts
8
+ * if (window.level === WINDOW_LEVELS.floating) { ... }
9
+ * if (window.level >= WINDOW_LEVELS.mainMenu) { ... } // at or above the menu bar
10
+ * ```
11
+ *
12
+ * Levels are `Comparable` in Swift: a higher number is drawn in front of a lower one. Some names
13
+ * share the same numeric value (e.g. `floating`, `submenu`, `tornOffMenu` are all `3`).
14
+ *
15
+ * @group Discovery
16
+ */
17
+ export const WINDOW_LEVELS = {
18
+ baseWindow: -2147483648,
19
+ minimumWindow: -2147483643,
20
+ desktopWindow: -2147483623,
21
+ desktopIconWindow: -2147483603,
22
+ backstopMenu: -20,
23
+ normal: 0,
24
+ floating: 3,
25
+ submenu: 3,
26
+ tornOffMenu: 3,
27
+ modalPanel: 8,
28
+ utilityWindow: 19,
29
+ mainMenu: 24,
30
+ statusBar: 25,
31
+ popUpMenu: 101,
32
+ overlayWindow: 102,
33
+ helpWindow: 200,
34
+ draggingWindow: 500,
35
+ screenSaver: 1000,
36
+ screenSaverWindow: 1000,
37
+ assistiveTechHighWindow: 1500,
38
+ cursorWindow: 2147483630,
39
+ maximumWindow: 2147483631,
40
+ } as const;
41
+
42
+ /**
43
+ * The name of a well-known macOS window level. See {@link WINDOW_LEVELS}.
44
+ *
45
+ * @group Discovery
46
+ */
47
+ export type WindowLevelName = keyof typeof WINDOW_LEVELS;
package/src/browser.ts CHANGED
@@ -1,7 +1,17 @@
1
1
  // Browser-only exports - safe for use in browser environments
2
2
  // This entry point excludes Node.js-specific functionality like EventEmitter, crypto, etc.
3
3
 
4
- export { createWebAudioBuffer } from './WebAudioUtils.js';
4
+ export { createWebAudioBuffer, computeAudioLevel } from './WebAudioUtils.js';
5
5
 
6
6
  // Type-only exports for browser use
7
- export type { AudioStreamBuffer } from './Recorder.js';
7
+ export type { AudioStreamBuffer } from './Recorder.js';
8
+ export type { AudioLevel } from './WebAudioUtils.js';
9
+
10
+ // Sidecar JSON types (pure types, safe in the browser) — useful when parsing a recording bundle's
11
+ // input-event / window-presence / dimension-change JSON files in a renderer process.
12
+ export type * from './InputEvents.js';
13
+ export type * from './RecordingMetadata.js';
14
+ export type * from './Errors.js';
15
+ export type * from './WindowLevels.js';
16
+ export { RECORDKIT_ERROR_CODE_NUMBERS } from './Errors.js';
17
+ export { WINDOW_LEVELS } from './WindowLevels.js';
package/src/index.ts CHANGED
@@ -1,3 +1,11 @@
1
1
  export type * from './RecordKit.js';
2
2
  export type * from './Recorder.js';
3
+ export type * from './InputEvents.js';
4
+ export type * from './RecordingMetadata.js';
5
+ export type * from './Errors.js';
6
+ export type * from './WindowLevels.js';
7
+ export type * from './WebAudioUtils.js';
3
8
  export { recordkit } from './RecordKit.js';
9
+ export { RECORDKIT_ERROR_CODE_NUMBERS } from './Errors.js';
10
+ export { WINDOW_LEVELS } from './WindowLevels.js';
11
+ export { computeAudioLevel, createWebAudioBuffer } from './WebAudioUtils.js';
@@ -1,24 +0,0 @@
1
- // Jest Snapshot v1, https://goo.gl/fbAQLP
2
-
3
- exports[`NonstrictRPC initialize sends init request w/o parameters 1`] = `
4
- {
5
- "id": Any<String>,
6
- "nsrpc": 1,
7
- "procedure": "init",
8
- "target": "TheTarget",
9
- "type": "TheType",
10
- }
11
- `;
12
-
13
- exports[`NonstrictRPC initialize sends init request w/parameters 1`] = `
14
- {
15
- "id": Any<String>,
16
- "nsrpc": 1,
17
- "params": {
18
- "aParameter": "TheValue",
19
- },
20
- "procedure": "init",
21
- "target": "TheTarget",
22
- "type": "TheType",
23
- }
24
- `;