@nonstrict/recordkit 0.87.2 → 0.97.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 (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
@@ -22,6 +22,17 @@ export class IpcRecordKit {
22
22
  childProcess.on('spawn', () => { resolve(childProcess) })
23
23
  })
24
24
 
25
+ this.childProcess.on('close', (code, signal) => {
26
+ // No response can arrive anymore; fail all in-flight and future requests instead of letting
27
+ // them hang forever.
28
+ this.nsrpc.terminate(new Error(`RecordKit: [RPC] Process is gone (closed with code ${code} and signal ${signal}).`));
29
+ })
30
+ this.childProcess.stdin?.on('error', (error) => {
31
+ // Without an error listener, a failed write to a dead process would crash Node with an
32
+ // unhandled 'error' event. The 'close' handler above already fails all requests.
33
+ console.error(`RecordKit: [RPC] !! Failed to write to RPC process: ${error}`)
34
+ })
35
+
25
36
  const { stdout, stderr } = this.childProcess
26
37
  if (!stdout) { throw new Error('RecordKit: [RPC] !! No stdout stream on child process.') }
27
38
 
@@ -39,6 +50,7 @@ export class IpcRecordKit {
39
50
  private write(message: String) {
40
51
  const stdin = this.childProcess?.stdin;
41
52
  if (!stdin) { throw new Error('RecordKit: [RPC] !! Missing stdin stream.') }
53
+ if (stdin.destroyed) { throw new Error('RecordKit: [RPC] !! Process is gone, cannot write to its stdin.') }
42
54
  stdin.write(message + "\n")
43
55
  }
44
56
  }
@@ -7,7 +7,30 @@ import v8 from 'v8';
7
7
  // eval('%CollectGarbage(true)');
8
8
 
9
9
  describe('NonstrictRPC', () => {
10
- it('needs tests', () => { });
10
+ describe('terminate', () => {
11
+ it('rejects all in-flight requests', async () => {
12
+ const rpc = new NSRPC(() => { });
13
+ const pending = rpc.perform({ type: 'Recorder', action: 'getDisplays' });
14
+ rpc.terminate(new Error('RPC process is gone'));
15
+ await expect(pending).rejects.toThrow('RPC process is gone');
16
+ });
17
+
18
+ it('fails future requests immediately instead of letting them hang', async () => {
19
+ const rpc = new NSRPC(() => { });
20
+ rpc.terminate(new Error('RPC process is gone'));
21
+ await expect(rpc.perform({ type: 'Recorder', action: 'getDisplays' })).rejects.toThrow('RPC process is gone');
22
+ });
23
+
24
+ it('does not affect requests that already completed', async () => {
25
+ const sent: string[] = [];
26
+ const rpc = new NSRPC((data) => sent.push(data));
27
+ const pending = rpc.perform({ type: 'Recorder', action: 'getDisplays' });
28
+ const request = JSON.parse(sent[0]);
29
+ rpc.receive(JSON.stringify({ nsrpc: 1, id: request.id, status: 200, result: ['display'] }));
30
+ await expect(pending).resolves.toEqual(['display']);
31
+ rpc.terminate(new Error('RPC process is gone'));
32
+ });
33
+ });
11
34
 
12
35
  // beforeAll(() => {
13
36
  // v8.setFlagsFromString('--allow-natives-syntax');
@@ -116,11 +116,28 @@ export class NSRPC {
116
116
 
117
117
  private responseHandlers: Map<string, PromiseSource> = new Map();
118
118
  private closureTargets: Map<string, ClosureTarget> = new Map();
119
+ private terminationError?: Error;
119
120
 
120
121
  constructor(send: (data: string) => void) {
121
122
  this.send = send;
122
123
  }
123
124
 
125
+ /**
126
+ * Marks the RPC connection as permanently gone, e.g. because the external process exited.
127
+ *
128
+ * All in-flight requests are rejected with the given error and any future request fails
129
+ * immediately with the same error, instead of waiting forever for a response that can no
130
+ * longer arrive.
131
+ */
132
+ terminate(error: Error) {
133
+ this.terminationError = error;
134
+ const pendingHandlers = [...this.responseHandlers.values()];
135
+ this.responseHandlers.clear();
136
+ for (const handler of pendingHandlers) {
137
+ handler.reject(error);
138
+ }
139
+ }
140
+
124
141
  receive(data: string) {
125
142
  // TODO: For now we just assume the message is a valid NSRPC message, but we should:
126
143
  // - Check if the nsrpc property is set to a number in the range of 1..<2
@@ -181,6 +198,9 @@ export class NSRPC {
181
198
  private async sendRequest(
182
199
  request: NSRPCRequestBody
183
200
  ): Promise<unknown> {
201
+ if (this.terminationError !== undefined) {
202
+ throw this.terminationError;
203
+ }
184
204
  const id = "req_" + randomUUID();
185
205
  const response = new Promise((resolve, reject) => {
186
206
  this.responseHandlers.set(id, { resolve, reject });
@@ -273,17 +293,21 @@ export class NSRPC {
273
293
  params?: Record<string, unknown>;
274
294
  lifecycle: Object;
275
295
  }) {
276
- const target = args.target
277
- finalizationRegistry.register(args.lifecycle, async () => {
278
- await this.release(target);
279
- });
280
-
281
296
  await this.sendRequest({
282
297
  target: args.target,
283
298
  type: args.type,
284
299
  params: args.params,
285
300
  procedure: "init",
286
301
  });
302
+
303
+ // Register the GC release only after a successful init; registering earlier would later send a
304
+ // release for a target the external process never knew about.
305
+ const target = args.target
306
+ finalizationRegistry.register(args.lifecycle, () => {
307
+ // Swallow rejections: the external process may already be gone, in which case there is
308
+ // nothing left to release.
309
+ this.release(target).catch(() => { });
310
+ });
287
311
  }
288
312
 
289
313
  async perform(body: { // TODO: Add support for static method calls.
package/src/RecordKit.ts CHANGED
@@ -1,9 +1,34 @@
1
1
  import { IpcRecordKit } from "./IpcRecordKit.js";
2
2
  import { Recorder, RecorderSchemaItem } from "./Recorder.js";
3
- import type { SystemAudioBackend } from "./Recorder.js";
3
+ import type { SystemAudioBackend, RecorderSettings, Size } from "./Recorder.js";
4
4
  import { EventEmitter } from "events";
5
5
  import { existsSync } from "node:fs";
6
6
 
7
+ /** @internal */
8
+ function windowIdOf(window: Window | number): number {
9
+ return typeof window == 'number' ? window : window.id
10
+ }
11
+ /** @internal */
12
+ function displayIdOf(display: Display | number | undefined): number | undefined {
13
+ return display == null ? undefined : (typeof display == 'number' ? display : display.id)
14
+ }
15
+ /** @internal */
16
+ function cameraIdOf(camera: Camera | string): string {
17
+ return typeof camera == 'string' ? camera : camera.id
18
+ }
19
+ /** @internal */
20
+ function microphoneIdOf(microphone: Microphone | string): string {
21
+ return typeof microphone == 'string' ? microphone : microphone.id
22
+ }
23
+ /** @internal */
24
+ function appleDeviceIdOf(device: AppleDevice | string): string {
25
+ return typeof device == 'string' ? device : device.id
26
+ }
27
+ /** @internal */
28
+ function applicationIdOf(application: RunningApplication | number): number {
29
+ return typeof application == 'number' ? application : application.id
30
+ }
31
+
7
32
  /**
8
33
  * Entry point for the RecordKit SDK, an instance is available as `recordkit` that can be imported from the module. Do not instantiate this class directly.
9
34
  *
@@ -15,6 +40,15 @@ import { existsSync } from "node:fs";
15
40
  *
16
41
  * @groupDescription Logging
17
42
  * Log what's going on to the console for easy debugging and troubleshooting. See the [Logging and Error Handling guide](https://recordkit.dev/guides/logging-and-errors) for more information.
43
+ *
44
+ * @groupDescription Preferred Devices
45
+ * Read and update the user's preferred devices, so you can pre-select sensible defaults in your UI.
46
+ *
47
+ * @groupDescription Window Control
48
+ * Move, resize, center and maximize windows of other applications (requires Accessibility Control permission).
49
+ *
50
+ * @groupDescription Device Control
51
+ * Configure capture devices, such as selecting a camera's active format or fetching an application's icon.
18
52
  */
19
53
  export class RecordKit extends EventEmitter {
20
54
  private ipcRecordKit = new IpcRecordKit()
@@ -61,8 +95,9 @@ export class RecordKit extends EventEmitter {
61
95
 
62
96
  const logHandlerInstance = this.ipcRecordKit.nsrpc.registerClosure({
63
97
  handler: (params) => {
64
- console.log('RecordKit:', params.formattedMessage)
65
- this.emit('log', params)
98
+ const message = params as unknown as LogMessage
99
+ console.log('RecordKit:', message.formattedMessage)
100
+ this.emit('log', message)
66
101
  },
67
102
  prefix: 'RecordKit.logHandler',
68
103
  lifecycle: this
@@ -76,9 +111,9 @@ export class RecordKit extends EventEmitter {
76
111
 
77
112
  /**
78
113
  * Set the global log level. Defaults to `debug`.
79
- *
114
+ *
80
115
  * Messages with a lower level than this will be ignored and not passed to any log handlers.
81
- *
116
+ *
82
117
  * @group Logging
83
118
  */
84
119
  async setLogLevel(logLevel: LogLevel): Promise<void> {
@@ -87,9 +122,9 @@ export class RecordKit extends EventEmitter {
87
122
 
88
123
  /**
89
124
  * Overrides the global log level for a specific category. Defaults to the global log level.
90
- *
125
+ *
91
126
  * Messages in the given category with a lower level than this will be ignored and not passed to any log handlers.
92
- *
127
+ *
93
128
  * @group Logging
94
129
  */
95
130
  async setCategoryLogLevel(params: { category: string, logLevel?: LogLevel }): Promise<void> {
@@ -151,6 +186,191 @@ export class RecordKit extends EventEmitter {
151
186
  return await this.ipcRecordKit.nsrpc.perform({ type: 'Recorder', action: 'getRunningApplications' }) as RunningApplication[]
152
187
  }
153
188
 
189
+ /**
190
+ * The user's preferred devices for each source type, ordered most-preferred first.
191
+ *
192
+ * RecordKit remembers which devices the user last recorded with (unless disabled via the recorder's
193
+ * `updatesUserPreferred` setting). Use this to pre-select a sensible default device in your UI.
194
+ *
195
+ * @group Preferred Devices
196
+ */
197
+ async getUserPreferred(): Promise<UserPreferred> {
198
+ return await this.ipcRecordKit.nsrpc.perform({ type: 'UserPreferred', action: 'getPreferred' }) as UserPreferred
199
+ }
200
+
201
+ /**
202
+ * Records the given microphone as the user's most-preferred microphone.
203
+ *
204
+ * Call this whenever the user manually selects a microphone, so it can be pre-selected later via
205
+ * {@link getUserPreferred}. The selection moves to the front of {@link UserPreferred.microphoneIDs}.
206
+ *
207
+ * @param microphone - The microphone to prefer, either a {@link Microphone} or its {@link Microphone.id}.
208
+ * @group Preferred Devices
209
+ */
210
+ async updatePreferredMicrophone(microphone: Microphone | string): Promise<void> {
211
+ const id = microphoneIdOf(microphone)
212
+ await this.ipcRecordKit.nsrpc.perform({ type: 'UserPreferred', action: 'updateMicrophone', params: { id } })
213
+ }
214
+
215
+ /**
216
+ * Records the given camera as the user's most-preferred camera.
217
+ *
218
+ * Call this whenever the user manually selects a camera, so it can be pre-selected later via
219
+ * {@link getUserPreferred}. The selection moves to the front of {@link UserPreferred.cameraIDs}.
220
+ *
221
+ * @param camera - The camera to prefer, either a {@link Camera} or its {@link Camera.id}.
222
+ * @group Preferred Devices
223
+ */
224
+ async updatePreferredCamera(camera: Camera | string): Promise<void> {
225
+ const id = cameraIdOf(camera)
226
+ await this.ipcRecordKit.nsrpc.perform({ type: 'UserPreferred', action: 'updateCamera', params: { id } })
227
+ }
228
+
229
+ /**
230
+ * Records the given display as the user's most-preferred display.
231
+ *
232
+ * Call this whenever the user manually selects a display, so it can be pre-selected later via
233
+ * {@link getUserPreferred}. The selection moves to the front of {@link UserPreferred.displayIDs}.
234
+ *
235
+ * @param display - The display to prefer, either a {@link Display} or its {@link Display.id}.
236
+ * @group Preferred Devices
237
+ */
238
+ async updatePreferredDisplay(display: Display | number): Promise<void> {
239
+ const id = displayIdOf(display)
240
+ await this.ipcRecordKit.nsrpc.perform({ type: 'UserPreferred', action: 'updateDisplay', params: { id } })
241
+ }
242
+
243
+ /**
244
+ * Records the given Apple device as the user's most-preferred Apple device.
245
+ *
246
+ * Call this whenever the user manually selects an Apple device, so it can be pre-selected later via
247
+ * {@link getUserPreferred}. The selection moves to the front of {@link UserPreferred.appleDeviceIDs}.
248
+ *
249
+ * @param device - The Apple device to prefer, either an {@link AppleDevice} or its {@link AppleDevice.id}.
250
+ * @group Preferred Devices
251
+ */
252
+ async updatePreferredAppleDevice(device: AppleDevice | string): Promise<void> {
253
+ const id = appleDeviceIdOf(device)
254
+ await this.ipcRecordKit.nsrpc.perform({ type: 'UserPreferred', action: 'updateAppleDevice', params: { id } })
255
+ }
256
+
257
+ /**
258
+ * Maximizes the given window, resizing it to fill the display's visible area (excluding the menu bar and Dock)
259
+ * and centering it on that display.
260
+ *
261
+ * Requires Accessibility Control permission (see {@link getAccessibilityControlAccess}).
262
+ *
263
+ * @remarks
264
+ * Rejects if the window cannot be maximized — typically because Accessibility permission is missing,
265
+ * the target display cannot be found, or the window is minimized or closed.
266
+ *
267
+ * @param window - The window to maximize, either a {@link Window} or its {@link Window.id}.
268
+ * @param options.display - The display to maximize onto, either a {@link Display} or its {@link Display.id}. Defaults to the window's current display.
269
+ * @group Window Control
270
+ */
271
+ async maximizeWindow(window: Window | number, options?: { display?: Display | number }): Promise<void> {
272
+ await this.ipcRecordKit.nsrpc.perform({ type: 'Recorder', action: 'windowMaximize', params: { window: windowIdOf(window), display: displayIdOf(options?.display) } })
273
+ }
274
+
275
+ /**
276
+ * Resizes the given window to the given size (in points), keeping it centered on its display.
277
+ *
278
+ * Requires Accessibility Control permission (see {@link getAccessibilityControlAccess}).
279
+ *
280
+ * @remarks
281
+ * The requested size is clipped to the display's visible frame if it would be larger. After resizing,
282
+ * the window is re-centered so that a window which could not shrink/grow to the requested size still
283
+ * ends up centered. Rejects if Accessibility permission is missing, the target display cannot be found,
284
+ * or the window is minimized, closed, or does not support resizing.
285
+ *
286
+ * @param window - The window to resize, either a {@link Window} or its {@link Window.id}.
287
+ * @param size - The new size in points.
288
+ * @param options.display - The display to center on, either a {@link Display} or its {@link Display.id}. Defaults to the window's current display.
289
+ * @group Window Control
290
+ */
291
+ async resizeWindow(window: Window | number, size: Size, options?: { display?: Display | number }): Promise<void> {
292
+ await this.ipcRecordKit.nsrpc.perform({ type: 'Recorder', action: 'windowResize', params: { window: windowIdOf(window), width: size.width, height: size.height, display: displayIdOf(options?.display) } })
293
+ }
294
+
295
+ /**
296
+ * Centers the given window within the visible area (excluding the menu bar and Dock) of its display,
297
+ * keeping its current size.
298
+ *
299
+ * Requires Accessibility Control permission (see {@link getAccessibilityControlAccess}).
300
+ *
301
+ * @remarks
302
+ * Rejects if Accessibility permission is missing, the target display cannot be found, or the window is
303
+ * minimized, closed, or does not support moving.
304
+ *
305
+ * @param window - The window to center, either a {@link Window} or its {@link Window.id}.
306
+ * @param options.display - The display to center on, either a {@link Display} or its {@link Display.id}. Defaults to the window's current display.
307
+ * @group Window Control
308
+ */
309
+ async centerWindow(window: Window | number, options?: { display?: Display | number }): Promise<void> {
310
+ await this.ipcRecordKit.nsrpc.perform({ type: 'Recorder', action: 'windowCenter', params: { window: windowIdOf(window), display: displayIdOf(options?.display) } })
311
+ }
312
+
313
+ /**
314
+ * Moves the given window so its top-left corner is at the given position (in points, top-left origin).
315
+ *
316
+ * Requires Accessibility Control permission (see {@link getAccessibilityControlAccess}).
317
+ *
318
+ * @remarks
319
+ * Rejects if Accessibility permission is missing, or the window is minimized, closed, or does not
320
+ * support moving.
321
+ *
322
+ * @param window - The window to move, either a {@link Window} or its {@link Window.id}.
323
+ * @param position - The new top-left origin for the window, in points (top-left coordinate space).
324
+ * @group Window Control
325
+ */
326
+ async moveWindow(window: Window | number, position: { x: number, y: number }): Promise<void> {
327
+ await this.ipcRecordKit.nsrpc.perform({ type: 'Recorder', action: 'windowMove', params: { window: windowIdOf(window), x: position.x, y: position.y } })
328
+ }
329
+
330
+ /**
331
+ * Selects the camera's active capture format that best matches the given dimensions (in pixels).
332
+ *
333
+ * Use this when you want the camera to deliver a specific resolution — typically before recording or
334
+ * before showing a live preview, so the preview renders at the intended resolution. The format stays
335
+ * in effect until something else changes it.
336
+ *
337
+ * The chosen format is the smallest format whose dimensions are ≥ the target, preferring biplanar YUV
338
+ * pixel formats, and falling back to the largest available format if nothing meets the target.
339
+ *
340
+ * @remarks
341
+ * Rejects if the camera is unavailable, has no usable video format, or its configuration is locked by
342
+ * another process.
343
+ *
344
+ * @param camera - The camera to configure, either a {@link Camera} or its {@link Camera.id}.
345
+ * @param dimensions - Target dimensions in pixels.
346
+ * @group Device Control
347
+ */
348
+ async setCameraActiveFormat(camera: Camera | string, dimensions: Size): Promise<void> {
349
+ await this.ipcRecordKit.nsrpc.perform({ type: 'Recorder', action: 'cameraSetActiveFormat', params: { camera: cameraIdOf(camera), width: dimensions.width, height: dimensions.height } })
350
+ }
351
+
352
+ /**
353
+ * Returns the camera capture format that {@link setCameraActiveFormat} would select for the given
354
+ * dimensions (in pixels), without applying it. Returns `undefined` if the camera has no suitable format.
355
+ *
356
+ * @group Device Control
357
+ */
358
+ async getCameraBestFormat(camera: Camera | string, dimensions: Size): Promise<CameraFormat | undefined> {
359
+ return await this.ipcRecordKit.nsrpc.perform({ type: 'Recorder', action: 'cameraBestFormat', params: { camera: cameraIdOf(camera), width: dimensions.width, height: dimensions.height } }) as CameraFormat | undefined
360
+ }
361
+
362
+ /**
363
+ * Returns the icon of the given running application as a `data:image/png;base64,...` URL,
364
+ * usable directly as the `src` of an HTML `<img>` tag.
365
+ *
366
+ * @group Device Control
367
+ */
368
+ async getApplicationIcon(application: RunningApplication | number): Promise<string> {
369
+ const id = applicationIdOf(application)
370
+ const result = await this.ipcRecordKit.nsrpc.perform({ type: 'Recorder', action: 'getApplicationIcon', params: { application: id } }) as { icon: string }
371
+ return result.icon
372
+ }
373
+
154
374
  /**
155
375
  * Indicates if camera can be used.
156
376
  *
@@ -197,6 +417,26 @@ export class RecordKit extends EventEmitter {
197
417
  }) as boolean
198
418
  }
199
419
 
420
+ /**
421
+ * Probes whether system audio can actually be recorded with the given backend by attempting a short silent capture.
422
+ *
423
+ * Unlike {@link getSystemAudioRecordingAccess}, which reads the recorded permission state, this verifies the
424
+ * permission is truly usable, immediately detecting cases where the OS reports a permission as granted but
425
+ * capture would still fail (e.g. after the user revokes it).
426
+ *
427
+ * @remarks If the permission state is still undetermined, this may trigger the system audio permission prompt.
428
+ * @group Permissions
429
+ */
430
+ async probeSystemAudioRecordingAccess(options?: { backend?: SystemAudioPermissionBackend }): Promise<boolean> {
431
+ return await this.ipcRecordKit.nsrpc.perform({
432
+ type: 'AuthorizationStatus',
433
+ action: 'probeSystemAudioRecordingAccess',
434
+ params: {
435
+ backend: options?.backend ?? 'default'
436
+ }
437
+ }) as boolean
438
+ }
439
+
200
440
  /**
201
441
  * Indicates if keystroke events of other apps can be recorded via Input Monitoring.
202
442
  *
@@ -258,7 +498,9 @@ export class RecordKit extends EventEmitter {
258
498
  * - `screenCaptureKit`: Screen Recording permission
259
499
  * - `_beta_coreAudio`: deprecated alias for `coreAudio`
260
500
  *
261
- * Afterwards, the users needs to restart this app, for the permission to become active in the app.
501
+ * For the `screenCaptureKit` backend the user must restart the app before the granted permission
502
+ * becomes active. The `default` and `coreAudio` backends return the live granted/denied result
503
+ * with no restart required (macOS 14.2+).
262
504
  *
263
505
  * @returns Boolean value that indicates whether the user granted or denied access to your app.
264
506
  * @group Permissions
@@ -301,18 +543,43 @@ export class RecordKit extends EventEmitter {
301
543
  return await this.ipcRecordKit.nsrpc.perform({ type: 'AuthorizationStatus', action: 'requestAccessibilityControlAccess' }) as void
302
544
  }
303
545
 
546
+ /**
547
+ * Creates a {@link Recorder} for the given schema.
548
+ *
549
+ * The schema describes what to record (its `items`, e.g. a webcam, display, microphone or system audio),
550
+ * where to write the resulting RecordKit bundle (`output_directory`), and optional session-wide
551
+ * {@link RecorderSettings}. Call {@link Recorder.prepare} then {@link Recorder.start} on the returned recorder.
552
+ *
553
+ * @remarks The given `schema` is consumed: device/window objects in its `items` are replaced by their IDs and
554
+ * any callbacks are registered internally. Pass a fresh schema object per call rather than reusing one.
555
+ *
556
+ * @group Recording
557
+ */
304
558
  async createRecorder(
305
559
  schema: {
306
560
  output_directory?: string
307
561
  items: RecorderSchemaItem[]
308
- settings?: {
309
- allowFrameReordering?: boolean
310
- }
562
+ settings?: RecorderSettings
311
563
  }): Promise<Recorder> {
312
564
  return Recorder.newInstance(this.ipcRecordKit.nsrpc, schema);
313
565
  }
314
566
  }
315
567
 
568
+ /**
569
+ * Typed event overloads for {@link RecordKit}. Declaration-merges with the class so that
570
+ * `recordkit.on('log', message => …)` receives a typed {@link LogMessage} instead of `any`.
571
+ *
572
+ * @group Logging
573
+ */
574
+ export interface RecordKit {
575
+ /** Fires for every log message emitted by RecordKit. See {@link LogMessage}. */
576
+ on(event: 'log', listener: (message: LogMessage) => void): this;
577
+ /** @see {@link RecordKit.on} */
578
+ once(event: 'log', listener: (message: LogMessage) => void): this;
579
+ /** @see {@link RecordKit.on} */
580
+ off(event: 'log', listener: (message: LogMessage) => void): this;
581
+ }
582
+
316
583
  /** @ignore */
317
584
  export let recordkit = new RecordKit();
318
585
 
@@ -400,6 +667,9 @@ export interface Camera {
400
667
  /** An identifier that uniquely identifies the camera. */
401
668
  id: string;
402
669
 
670
+ /** Indicates whether this is a virtual camera (e.g. provided by software rather than physical hardware). */
671
+ isVirtual: boolean;
672
+
403
673
  /** A localized camera name for display in the user interface. */
404
674
  name: string;
405
675
 
@@ -428,6 +698,20 @@ export interface Camera {
428
698
  preview_url?: string;
429
699
  }
430
700
 
701
+ /**
702
+ * A camera capture format, as returned by {@link RecordKit.getCameraBestFormat}.
703
+ *
704
+ * @group Discovery
705
+ */
706
+ export interface CameraFormat {
707
+ /** Width of the format, in pixels. */
708
+ width: number
709
+ /** Height of the format, in pixels. */
710
+ height: number
711
+ /** Highest frame rate supported by this format, in frames per second. */
712
+ maxFrameRate: number
713
+ }
714
+
431
715
  /**
432
716
  * A microphone whose audio can be recorded.
433
717
  *
@@ -437,6 +721,9 @@ export interface Microphone {
437
721
  /** An identifier that uniquely identifies the microphone. */
438
722
  id: string;
439
723
 
724
+ /** Indicates whether this is a virtual microphone (e.g. provided by software rather than physical hardware). */
725
+ isVirtual: boolean;
726
+
440
727
  /** A localized microphone name for display in the user interface. */
441
728
  name: string;
442
729
 
@@ -469,9 +756,15 @@ export interface Display {
469
756
  /** Name of this display. */
470
757
  localizedName?: string;
471
758
 
759
+ /** SF Symbol name representing this display (e.g. `laptopcomputer`, `display`), suitable for use in the UI. */
760
+ symbolName: string;
761
+
472
762
  /** Frame of the display, relative to the main display. Uses top-left coordinate space. */
473
763
  frame: Bounds
474
764
 
765
+ /** Visible frame of the display (excluding the menu bar and Dock), relative to the main display. Top-left coordinate space. */
766
+ visibleFrame?: Bounds
767
+
475
768
  /** Indicates if this is the main display. */
476
769
  isMain: boolean
477
770
 
@@ -522,7 +815,50 @@ export interface Bounds {
522
815
  height: number;
523
816
  }
524
817
 
818
+ /**
819
+ * The user's preferred device IDs for each source type, ordered most-preferred first.
820
+ *
821
+ * @group Preferred Devices
822
+ */
823
+ export interface UserPreferred {
824
+ /** Preferred microphone IDs (matching {@link Microphone.id}), most-preferred first. */
825
+ microphoneIDs: string[]
826
+ /** Preferred camera IDs (matching {@link Camera.id}), most-preferred first. */
827
+ cameraIDs: string[]
828
+ /** Preferred display IDs (matching {@link Display.id}), most-preferred first. */
829
+ displayIDs: number[]
830
+ /** Preferred Apple device IDs (matching {@link AppleDevice.id}), most-preferred first. */
831
+ appleDeviceIDs: string[]
832
+ }
833
+
525
834
  /**
526
835
  * @group Logging
527
836
  */
528
837
  export type LogLevel = 'trace' | 'debug' | 'info' | 'warning' | 'error' | 'critical'
838
+
839
+ /**
840
+ * A structured log message emitted by RecordKit, delivered as the payload of the `'log'` event.
841
+ *
842
+ * @example
843
+ * ```ts
844
+ * recordkit.on('log', (message) => {
845
+ * if (message.level === 'error') myLogger.error(message.category, message.message, message.metadata)
846
+ * })
847
+ * ```
848
+ *
849
+ * @group Logging
850
+ */
851
+ export interface LogMessage {
852
+ /** Time the message was logged, in milliseconds since the Unix epoch. Use `new Date(timestamp)` to get a `Date`. */
853
+ timestamp: number
854
+ /** Severity level of the message. */
855
+ level: LogLevel
856
+ /** Category the message belongs to (e.g. the subsystem that emitted it). */
857
+ category: string
858
+ /** The log message text. */
859
+ message: string
860
+ /** Additional structured key/value metadata attached to the message. */
861
+ metadata: Record<string, string>
862
+ /** Pre-formatted, human-readable representation of the message (timestamp, level, category, message and metadata). */
863
+ formattedMessage: string
864
+ }