@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
package/out/index.cjs CHANGED
@@ -32,9 +32,25 @@ class NSRPC {
32
32
  send;
33
33
  responseHandlers = new Map();
34
34
  closureTargets = new Map();
35
+ terminationError;
35
36
  constructor(send) {
36
37
  this.send = send;
37
38
  }
39
+ /**
40
+ * Marks the RPC connection as permanently gone, e.g. because the external process exited.
41
+ *
42
+ * All in-flight requests are rejected with the given error and any future request fails
43
+ * immediately with the same error, instead of waiting forever for a response that can no
44
+ * longer arrive.
45
+ */
46
+ terminate(error) {
47
+ this.terminationError = error;
48
+ const pendingHandlers = [...this.responseHandlers.values()];
49
+ this.responseHandlers.clear();
50
+ for (const handler of pendingHandlers) {
51
+ handler.reject(error);
52
+ }
53
+ }
38
54
  receive(data) {
39
55
  // TODO: For now we just assume the message is a valid NSRPC message, but we should:
40
56
  // - Check if the nsrpc property is set to a number in the range of 1..<2
@@ -90,6 +106,9 @@ class NSRPC {
90
106
  this.sendMessage({ ...response, nsrpc: 1, id });
91
107
  }
92
108
  async sendRequest(request) {
109
+ if (this.terminationError !== undefined) {
110
+ throw this.terminationError;
111
+ }
93
112
  const id = "req_" + crypto.randomUUID();
94
113
  const response = new Promise((resolve, reject) => {
95
114
  this.responseHandlers.set(id, { resolve, reject });
@@ -164,16 +183,20 @@ class NSRPC {
164
183
  }
165
184
  /* Perform remote procedures */
166
185
  async initialize(args) {
167
- const target = args.target;
168
- finalizationRegistry.register(args.lifecycle, async () => {
169
- await this.release(target);
170
- });
171
186
  await this.sendRequest({
172
187
  target: args.target,
173
188
  type: args.type,
174
189
  params: args.params,
175
190
  procedure: "init",
176
191
  });
192
+ // Register the GC release only after a successful init; registering earlier would later send a
193
+ // release for a target the external process never knew about.
194
+ const target = args.target;
195
+ finalizationRegistry.register(args.lifecycle, () => {
196
+ // Swallow rejections: the external process may already be gone, in which case there is
197
+ // nothing left to release.
198
+ this.release(target).catch(() => { });
199
+ });
177
200
  }
178
201
  async perform(body) {
179
202
  return await this.sendRequest({
@@ -222,6 +245,16 @@ class IpcRecordKit {
222
245
  childProcess.on('exit', (code, signal) => { console.error(`RecordKit: [RPC] Exited with code ${code} and signal ${signal}`); });
223
246
  childProcess.on('spawn', () => { resolve(childProcess); });
224
247
  });
248
+ this.childProcess.on('close', (code, signal) => {
249
+ // No response can arrive anymore; fail all in-flight and future requests instead of letting
250
+ // them hang forever.
251
+ this.nsrpc.terminate(new Error(`RecordKit: [RPC] Process is gone (closed with code ${code} and signal ${signal}).`));
252
+ });
253
+ this.childProcess.stdin?.on('error', (error) => {
254
+ // Without an error listener, a failed write to a dead process would crash Node with an
255
+ // unhandled 'error' event. The 'close' handler above already fails all requests.
256
+ console.error(`RecordKit: [RPC] !! Failed to write to RPC process: ${error}`);
257
+ });
225
258
  const { stdout, stderr } = this.childProcess;
226
259
  if (!stdout) {
227
260
  throw new Error('RecordKit: [RPC] !! No stdout stream on child process.');
@@ -240,6 +273,9 @@ class IpcRecordKit {
240
273
  if (!stdin) {
241
274
  throw new Error('RecordKit: [RPC] !! Missing stdin stream.');
242
275
  }
276
+ if (stdin.destroyed) {
277
+ throw new Error('RecordKit: [RPC] !! Process is gone, cannot write to its stdin.');
278
+ }
243
279
  stdin.write(message + "\n");
244
280
  }
245
281
  }
@@ -285,6 +321,21 @@ function convertRPCParamsToAudioStreamBuffer(params) {
285
321
  return null;
286
322
  }
287
323
  }
324
+ /**
325
+ * Registers the per-segment callback of a {@link JSONOutputOptions} (if any) as an RPC closure,
326
+ * replacing the function with the closure target so the options object can be serialized.
327
+ * @internal
328
+ */
329
+ function registerJSONOutputSegmentCallback(output, rpc, object, prefix) {
330
+ if (output && output.output == 'segmented' && output.segmentCallback) {
331
+ const segmentHandler = output.segmentCallback;
332
+ output.segmentCallback = rpc.registerClosure({
333
+ handler: (params) => { segmentHandler(params.path); },
334
+ prefix,
335
+ lifecycle: object
336
+ });
337
+ }
338
+ }
288
339
  /**
289
340
  * @group Recording
290
341
  */
@@ -324,6 +375,7 @@ class Recorder extends events.EventEmitter {
324
375
  lifecycle: object
325
376
  });
326
377
  }
378
+ registerJSONOutputSegmentCallback(item.inputEventsOutput, rpc, object, 'Display.onInputEventsSegment');
327
379
  }
328
380
  if (item.type == 'windowBasedCrop') {
329
381
  if (typeof item.window != 'number') {
@@ -337,6 +389,21 @@ class Recorder extends events.EventEmitter {
337
389
  lifecycle: object
338
390
  });
339
391
  }
392
+ registerJSONOutputSegmentCallback(item.inputEventsOutput, rpc, object, 'WindowBasedCrop.onInputEventsSegment');
393
+ }
394
+ if (item.type == 'desktopIndependentWindow') {
395
+ if (typeof item.window != 'number') {
396
+ item.window = item.window.id;
397
+ }
398
+ if (item.output == 'segmented' && item.segmentCallback) {
399
+ const segmentHandler = item.segmentCallback;
400
+ item.segmentCallback = rpc.registerClosure({
401
+ handler: (params) => { segmentHandler(params.path); },
402
+ prefix: 'DesktopIndependentWindow.onSegment',
403
+ lifecycle: object
404
+ });
405
+ }
406
+ registerJSONOutputSegmentCallback(item.inputEventsOutput, rpc, object, 'DesktopIndependentWindow.onInputEventsSegment');
340
407
  }
341
408
  if (item.type == 'appleDeviceStaticOrientation') {
342
409
  if (typeof item.device != 'string') {
@@ -427,10 +494,15 @@ class Recorder extends events.EventEmitter {
427
494
  prefix: 'Recorder.onAbort',
428
495
  lifecycle: object
429
496
  });
497
+ const onSignalsChangedInstance = rpc.registerClosure({
498
+ handler: (params) => { weakRefObject.deref()?.emit('signals', params.signals); },
499
+ prefix: 'Recorder.onSignalsChanged',
500
+ lifecycle: object
501
+ });
430
502
  await rpc.initialize({
431
503
  target,
432
504
  type: 'Recorder',
433
- params: { schema, onAbortInstance },
505
+ params: { schema, onAbortInstance, onSignalsChangedInstance },
434
506
  lifecycle: object
435
507
  });
436
508
  return object;
@@ -441,20 +513,86 @@ class Recorder extends events.EventEmitter {
441
513
  this.rpc = rpc;
442
514
  this.target = target;
443
515
  }
516
+ /**
517
+ * Prepares the recording session for instant recording, allocating resources and validating the
518
+ * configuration.
519
+ *
520
+ * Preparing ahead of time lets {@link start} begin recording instantly; without it, starting incurs
521
+ * a setup delay.
522
+ *
523
+ * @returns The expected {@link BundleInfo} describing the file assets that will be produced by this
524
+ * recording, allowing you to inspect the planned output (filenames, asset types, sizes) before
525
+ * recording starts.
526
+ */
444
527
  async prepare() {
445
- await this.rpc.perform({ target: this.target, action: 'prepare' });
528
+ return await this.rpc.perform({ target: this.target, action: 'prepare' });
446
529
  }
530
+ /**
531
+ * Starts recording. If the session was not already {@link prepare}d this performs setup first,
532
+ * incurring a short delay; call {@link prepare} ahead of time to start instantly.
533
+ */
447
534
  async start() {
448
535
  await this.rpc.perform({ target: this.target, action: 'start' });
449
536
  }
537
+ /**
538
+ * Pauses the recording. The capture hardware remains active so recording can be resumed quickly.
539
+ *
540
+ * Call {@link resume} to continue recording, or {@link stop} to finish.
541
+ */
542
+ async pause() {
543
+ await this.rpc.perform({ target: this.target, action: 'pause' });
544
+ }
545
+ /**
546
+ * Resumes a recording that was previously paused with {@link pause}.
547
+ */
548
+ async resume() {
549
+ await this.rpc.perform({ target: this.target, action: 'resume' });
550
+ }
551
+ /**
552
+ * Stops the recording, finalizes the output files, and returns the {@link RecordingResult}
553
+ * describing the completed bundle. The recorder cannot be reused after stopping.
554
+ *
555
+ * @remarks Known limitation: when the recording failed, the returned promise rejects with the
556
+ * failure but the partial recording result is not available over the RPC bridge (the Swift API
557
+ * surfaces it as `PartialResultError`). Any partially-written files do remain on disk in the
558
+ * bundle inside the schema's `output_directory`.
559
+ */
450
560
  async stop() {
451
561
  return await this.rpc.perform({ target: this.target, action: 'stop' });
452
562
  }
563
+ /**
564
+ * Cancels the recording and releases its resources without finalizing output. Use this to discard
565
+ * an in-progress or prepared recording; call {@link stop} instead to keep the result.
566
+ */
453
567
  async cancel() {
454
568
  await this.rpc.manualRelease(this.target);
455
569
  }
456
570
  }
457
571
 
572
+ /** @internal */
573
+ function windowIdOf(window) {
574
+ return typeof window == 'number' ? window : window.id;
575
+ }
576
+ /** @internal */
577
+ function displayIdOf(display) {
578
+ return display == null ? undefined : (typeof display == 'number' ? display : display.id);
579
+ }
580
+ /** @internal */
581
+ function cameraIdOf(camera) {
582
+ return typeof camera == 'string' ? camera : camera.id;
583
+ }
584
+ /** @internal */
585
+ function microphoneIdOf(microphone) {
586
+ return typeof microphone == 'string' ? microphone : microphone.id;
587
+ }
588
+ /** @internal */
589
+ function appleDeviceIdOf(device) {
590
+ return typeof device == 'string' ? device : device.id;
591
+ }
592
+ /** @internal */
593
+ function applicationIdOf(application) {
594
+ return typeof application == 'number' ? application : application.id;
595
+ }
458
596
  /**
459
597
  * 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.
460
598
  *
@@ -466,6 +604,15 @@ class Recorder extends events.EventEmitter {
466
604
  *
467
605
  * @groupDescription Logging
468
606
  * 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.
607
+ *
608
+ * @groupDescription Preferred Devices
609
+ * Read and update the user's preferred devices, so you can pre-select sensible defaults in your UI.
610
+ *
611
+ * @groupDescription Window Control
612
+ * Move, resize, center and maximize windows of other applications (requires Accessibility Control permission).
613
+ *
614
+ * @groupDescription Device Control
615
+ * Configure capture devices, such as selecting a camera's active format or fetching an application's icon.
469
616
  */
470
617
  class RecordKit extends events.EventEmitter {
471
618
  ipcRecordKit = new IpcRecordKit();
@@ -491,8 +638,9 @@ class RecordKit extends events.EventEmitter {
491
638
  await this.ipcRecordKit.initialize(rpcBinaryPath, args.logRpcMessages);
492
639
  const logHandlerInstance = this.ipcRecordKit.nsrpc.registerClosure({
493
640
  handler: (params) => {
494
- console.log('RecordKit:', params.formattedMessage);
495
- this.emit('log', params);
641
+ const message = params;
642
+ console.log('RecordKit:', message.formattedMessage);
643
+ this.emit('log', message);
496
644
  },
497
645
  prefix: 'RecordKit.logHandler',
498
646
  lifecycle: this
@@ -571,6 +719,179 @@ class RecordKit extends events.EventEmitter {
571
719
  async getRunningApplications() {
572
720
  return await this.ipcRecordKit.nsrpc.perform({ type: 'Recorder', action: 'getRunningApplications' });
573
721
  }
722
+ /**
723
+ * The user's preferred devices for each source type, ordered most-preferred first.
724
+ *
725
+ * RecordKit remembers which devices the user last recorded with (unless disabled via the recorder's
726
+ * `updatesUserPreferred` setting). Use this to pre-select a sensible default device in your UI.
727
+ *
728
+ * @group Preferred Devices
729
+ */
730
+ async getUserPreferred() {
731
+ return await this.ipcRecordKit.nsrpc.perform({ type: 'UserPreferred', action: 'getPreferred' });
732
+ }
733
+ /**
734
+ * Records the given microphone as the user's most-preferred microphone.
735
+ *
736
+ * Call this whenever the user manually selects a microphone, so it can be pre-selected later via
737
+ * {@link getUserPreferred}. The selection moves to the front of {@link UserPreferred.microphoneIDs}.
738
+ *
739
+ * @param microphone - The microphone to prefer, either a {@link Microphone} or its {@link Microphone.id}.
740
+ * @group Preferred Devices
741
+ */
742
+ async updatePreferredMicrophone(microphone) {
743
+ const id = microphoneIdOf(microphone);
744
+ await this.ipcRecordKit.nsrpc.perform({ type: 'UserPreferred', action: 'updateMicrophone', params: { id } });
745
+ }
746
+ /**
747
+ * Records the given camera as the user's most-preferred camera.
748
+ *
749
+ * Call this whenever the user manually selects a camera, so it can be pre-selected later via
750
+ * {@link getUserPreferred}. The selection moves to the front of {@link UserPreferred.cameraIDs}.
751
+ *
752
+ * @param camera - The camera to prefer, either a {@link Camera} or its {@link Camera.id}.
753
+ * @group Preferred Devices
754
+ */
755
+ async updatePreferredCamera(camera) {
756
+ const id = cameraIdOf(camera);
757
+ await this.ipcRecordKit.nsrpc.perform({ type: 'UserPreferred', action: 'updateCamera', params: { id } });
758
+ }
759
+ /**
760
+ * Records the given display as the user's most-preferred display.
761
+ *
762
+ * Call this whenever the user manually selects a display, so it can be pre-selected later via
763
+ * {@link getUserPreferred}. The selection moves to the front of {@link UserPreferred.displayIDs}.
764
+ *
765
+ * @param display - The display to prefer, either a {@link Display} or its {@link Display.id}.
766
+ * @group Preferred Devices
767
+ */
768
+ async updatePreferredDisplay(display) {
769
+ const id = displayIdOf(display);
770
+ await this.ipcRecordKit.nsrpc.perform({ type: 'UserPreferred', action: 'updateDisplay', params: { id } });
771
+ }
772
+ /**
773
+ * Records the given Apple device as the user's most-preferred Apple device.
774
+ *
775
+ * Call this whenever the user manually selects an Apple device, so it can be pre-selected later via
776
+ * {@link getUserPreferred}. The selection moves to the front of {@link UserPreferred.appleDeviceIDs}.
777
+ *
778
+ * @param device - The Apple device to prefer, either an {@link AppleDevice} or its {@link AppleDevice.id}.
779
+ * @group Preferred Devices
780
+ */
781
+ async updatePreferredAppleDevice(device) {
782
+ const id = appleDeviceIdOf(device);
783
+ await this.ipcRecordKit.nsrpc.perform({ type: 'UserPreferred', action: 'updateAppleDevice', params: { id } });
784
+ }
785
+ /**
786
+ * Maximizes the given window, resizing it to fill the display's visible area (excluding the menu bar and Dock)
787
+ * and centering it on that display.
788
+ *
789
+ * Requires Accessibility Control permission (see {@link getAccessibilityControlAccess}).
790
+ *
791
+ * @remarks
792
+ * Rejects if the window cannot be maximized — typically because Accessibility permission is missing,
793
+ * the target display cannot be found, or the window is minimized or closed.
794
+ *
795
+ * @param window - The window to maximize, either a {@link Window} or its {@link Window.id}.
796
+ * @param options.display - The display to maximize onto, either a {@link Display} or its {@link Display.id}. Defaults to the window's current display.
797
+ * @group Window Control
798
+ */
799
+ async maximizeWindow(window, options) {
800
+ await this.ipcRecordKit.nsrpc.perform({ type: 'Recorder', action: 'windowMaximize', params: { window: windowIdOf(window), display: displayIdOf(options?.display) } });
801
+ }
802
+ /**
803
+ * Resizes the given window to the given size (in points), keeping it centered on its display.
804
+ *
805
+ * Requires Accessibility Control permission (see {@link getAccessibilityControlAccess}).
806
+ *
807
+ * @remarks
808
+ * The requested size is clipped to the display's visible frame if it would be larger. After resizing,
809
+ * the window is re-centered so that a window which could not shrink/grow to the requested size still
810
+ * ends up centered. Rejects if Accessibility permission is missing, the target display cannot be found,
811
+ * or the window is minimized, closed, or does not support resizing.
812
+ *
813
+ * @param window - The window to resize, either a {@link Window} or its {@link Window.id}.
814
+ * @param size - The new size in points.
815
+ * @param options.display - The display to center on, either a {@link Display} or its {@link Display.id}. Defaults to the window's current display.
816
+ * @group Window Control
817
+ */
818
+ async resizeWindow(window, size, options) {
819
+ await this.ipcRecordKit.nsrpc.perform({ type: 'Recorder', action: 'windowResize', params: { window: windowIdOf(window), width: size.width, height: size.height, display: displayIdOf(options?.display) } });
820
+ }
821
+ /**
822
+ * Centers the given window within the visible area (excluding the menu bar and Dock) of its display,
823
+ * keeping its current size.
824
+ *
825
+ * Requires Accessibility Control permission (see {@link getAccessibilityControlAccess}).
826
+ *
827
+ * @remarks
828
+ * Rejects if Accessibility permission is missing, the target display cannot be found, or the window is
829
+ * minimized, closed, or does not support moving.
830
+ *
831
+ * @param window - The window to center, either a {@link Window} or its {@link Window.id}.
832
+ * @param options.display - The display to center on, either a {@link Display} or its {@link Display.id}. Defaults to the window's current display.
833
+ * @group Window Control
834
+ */
835
+ async centerWindow(window, options) {
836
+ await this.ipcRecordKit.nsrpc.perform({ type: 'Recorder', action: 'windowCenter', params: { window: windowIdOf(window), display: displayIdOf(options?.display) } });
837
+ }
838
+ /**
839
+ * Moves the given window so its top-left corner is at the given position (in points, top-left origin).
840
+ *
841
+ * Requires Accessibility Control permission (see {@link getAccessibilityControlAccess}).
842
+ *
843
+ * @remarks
844
+ * Rejects if Accessibility permission is missing, or the window is minimized, closed, or does not
845
+ * support moving.
846
+ *
847
+ * @param window - The window to move, either a {@link Window} or its {@link Window.id}.
848
+ * @param position - The new top-left origin for the window, in points (top-left coordinate space).
849
+ * @group Window Control
850
+ */
851
+ async moveWindow(window, position) {
852
+ await this.ipcRecordKit.nsrpc.perform({ type: 'Recorder', action: 'windowMove', params: { window: windowIdOf(window), x: position.x, y: position.y } });
853
+ }
854
+ /**
855
+ * Selects the camera's active capture format that best matches the given dimensions (in pixels).
856
+ *
857
+ * Use this when you want the camera to deliver a specific resolution — typically before recording or
858
+ * before showing a live preview, so the preview renders at the intended resolution. The format stays
859
+ * in effect until something else changes it.
860
+ *
861
+ * The chosen format is the smallest format whose dimensions are ≥ the target, preferring biplanar YUV
862
+ * pixel formats, and falling back to the largest available format if nothing meets the target.
863
+ *
864
+ * @remarks
865
+ * Rejects if the camera is unavailable, has no usable video format, or its configuration is locked by
866
+ * another process.
867
+ *
868
+ * @param camera - The camera to configure, either a {@link Camera} or its {@link Camera.id}.
869
+ * @param dimensions - Target dimensions in pixels.
870
+ * @group Device Control
871
+ */
872
+ async setCameraActiveFormat(camera, dimensions) {
873
+ await this.ipcRecordKit.nsrpc.perform({ type: 'Recorder', action: 'cameraSetActiveFormat', params: { camera: cameraIdOf(camera), width: dimensions.width, height: dimensions.height } });
874
+ }
875
+ /**
876
+ * Returns the camera capture format that {@link setCameraActiveFormat} would select for the given
877
+ * dimensions (in pixels), without applying it. Returns `undefined` if the camera has no suitable format.
878
+ *
879
+ * @group Device Control
880
+ */
881
+ async getCameraBestFormat(camera, dimensions) {
882
+ return await this.ipcRecordKit.nsrpc.perform({ type: 'Recorder', action: 'cameraBestFormat', params: { camera: cameraIdOf(camera), width: dimensions.width, height: dimensions.height } });
883
+ }
884
+ /**
885
+ * Returns the icon of the given running application as a `data:image/png;base64,...` URL,
886
+ * usable directly as the `src` of an HTML `<img>` tag.
887
+ *
888
+ * @group Device Control
889
+ */
890
+ async getApplicationIcon(application) {
891
+ const id = applicationIdOf(application);
892
+ const result = await this.ipcRecordKit.nsrpc.perform({ type: 'Recorder', action: 'getApplicationIcon', params: { application: id } });
893
+ return result.icon;
894
+ }
574
895
  /**
575
896
  * Indicates if camera can be used.
576
897
  *
@@ -613,6 +934,25 @@ class RecordKit extends events.EventEmitter {
613
934
  }
614
935
  });
615
936
  }
937
+ /**
938
+ * Probes whether system audio can actually be recorded with the given backend by attempting a short silent capture.
939
+ *
940
+ * Unlike {@link getSystemAudioRecordingAccess}, which reads the recorded permission state, this verifies the
941
+ * permission is truly usable, immediately detecting cases where the OS reports a permission as granted but
942
+ * capture would still fail (e.g. after the user revokes it).
943
+ *
944
+ * @remarks If the permission state is still undetermined, this may trigger the system audio permission prompt.
945
+ * @group Permissions
946
+ */
947
+ async probeSystemAudioRecordingAccess(options) {
948
+ return await this.ipcRecordKit.nsrpc.perform({
949
+ type: 'AuthorizationStatus',
950
+ action: 'probeSystemAudioRecordingAccess',
951
+ params: {
952
+ backend: options?.backend ?? 'default'
953
+ }
954
+ });
955
+ }
616
956
  /**
617
957
  * Indicates if keystroke events of other apps can be recorded via Input Monitoring.
618
958
  *
@@ -669,7 +1009,9 @@ class RecordKit extends events.EventEmitter {
669
1009
  * - `screenCaptureKit`: Screen Recording permission
670
1010
  * - `_beta_coreAudio`: deprecated alias for `coreAudio`
671
1011
  *
672
- * Afterwards, the users needs to restart this app, for the permission to become active in the app.
1012
+ * For the `screenCaptureKit` backend the user must restart the app before the granted permission
1013
+ * becomes active. The `default` and `coreAudio` backends return the live granted/denied result
1014
+ * with no restart required (macOS 14.2+).
673
1015
  *
674
1016
  * @returns Boolean value that indicates whether the user granted or denied access to your app.
675
1017
  * @group Permissions
@@ -709,6 +1051,18 @@ class RecordKit extends events.EventEmitter {
709
1051
  async requestAccessibilityControlAccess() {
710
1052
  return await this.ipcRecordKit.nsrpc.perform({ type: 'AuthorizationStatus', action: 'requestAccessibilityControlAccess' });
711
1053
  }
1054
+ /**
1055
+ * Creates a {@link Recorder} for the given schema.
1056
+ *
1057
+ * The schema describes what to record (its `items`, e.g. a webcam, display, microphone or system audio),
1058
+ * where to write the resulting RecordKit bundle (`output_directory`), and optional session-wide
1059
+ * {@link RecorderSettings}. Call {@link Recorder.prepare} then {@link Recorder.start} on the returned recorder.
1060
+ *
1061
+ * @remarks The given `schema` is consumed: device/window objects in its `items` are replaced by their IDs and
1062
+ * any callbacks are registered internally. Pass a fresh schema object per call rather than reusing one.
1063
+ *
1064
+ * @group Recording
1065
+ */
712
1066
  async createRecorder(schema) {
713
1067
  return Recorder.newInstance(this.ipcRecordKit.nsrpc, schema);
714
1068
  }
@@ -716,5 +1070,245 @@ class RecordKit extends events.EventEmitter {
716
1070
  /** @ignore */
717
1071
  let recordkit = new RecordKit();
718
1072
 
1073
+ // Error code types mirroring RecordKit's `RKError` / `RKError.Code` Swift enum, JSON-for-JSON, as
1074
+ // surfaced over the RPC bridge. `RecordKitError.code` carries the Swift case name (from
1075
+ // `RKError.Code.description`) and `RecordKitError.codeNumber` the corresponding `Int` raw value;
1076
+ // user-facing text is on `RecordKitError.message`, technical detail on `RecordKitError.debugDescription`.
1077
+ // See the RecordKitErrorCode docs below and https://recordkit.dev/guides/logging-and-errors#error-handling
1078
+ /**
1079
+ * Mapping from each {@link RecordKitErrorCode} name to its numeric raw value.
1080
+ *
1081
+ * The numbers correspond to the `Int` raw values of the Swift `RKError.Code`
1082
+ * enum and to the `RecordKitError.codeNumber` reported over RPC.
1083
+ *
1084
+ * @group Recording
1085
+ */
1086
+ const RECORDKIT_ERROR_CODE_NUMBERS = {
1087
+ // Configuration Errors
1088
+ invalidLicense: -1001,
1089
+ invalidConfiguration: -1002,
1090
+ // Permission Errors
1091
+ microphonePermissionRequired: -1101,
1092
+ cameraPermissionRequired: -1102,
1093
+ screenRecordingPermissionRequired: -1103,
1094
+ systemAudioPermissionRequired: -1104,
1095
+ // Device Availability Errors
1096
+ microphoneUnavailable: -1201,
1097
+ cameraUnavailable: -1202,
1098
+ displayUnavailable: -1203,
1099
+ windowUnavailable: -1204,
1100
+ systemAudioUnavailable: -1205,
1101
+ appleDeviceUnavailable: -1206,
1102
+ inputRecordingUnavailable: -1207,
1103
+ // Recording State Errors
1104
+ noVideoFramesReceived: -1301,
1105
+ noAudioSamplesReceived: -1302,
1106
+ screenCaptureStoppedLowDiskSpace: -1303,
1107
+ screenCaptureStoppedWithError: -1304,
1108
+ screenCaptureStoppedWithoutError: -1305,
1109
+ insufficientDiskSpace: -1306,
1110
+ // Internal Errors
1111
+ internalError: -1600,
1112
+ uncaughtError: -1601,
1113
+ internalConductorError: -1602,
1114
+ configurationFailed: -1603,
1115
+ configurationNotSupported: -1604,
1116
+ audioFormatError: -1605,
1117
+ videoFormatError: -1606,
1118
+ audioFormatConfigurationFailed: -1607,
1119
+ videoFormatConfigurationFailed: -1608,
1120
+ mediaFormatInitializationFailed: -1609,
1121
+ mediaFormatConfigurationFailed: -1610,
1122
+ audioDeviceInitializationFailed: -1611,
1123
+ audioDeviceConfigurationFailed: -1612,
1124
+ assetWriterFailed: -1613,
1125
+ tccUnavailableError: -1614,
1126
+ // Processing/Operation Errors
1127
+ audioProcessingFailed: -1701,
1128
+ audioBufferProcessingFailed: -1702,
1129
+ audioBufferCreationFailed: -1703,
1130
+ inputEventProcessingFailed: -1704,
1131
+ assetWriterCreationFailed: -1705,
1132
+ fileOperationFailed: -1706,
1133
+ windowOperationFailed: -1707,
1134
+ };
1135
+
1136
+ /**
1137
+ * Named macOS window levels, mirroring RecordKit's `RKWindow.Level` Swift type.
1138
+ *
1139
+ * A {@link Window}'s `level` is a raw integer. macOS assigns windows to a small set of well-known
1140
+ * levels; this map lets you compare a window's level against those named values, e.g.
1141
+ *
1142
+ * ```ts
1143
+ * if (window.level === WINDOW_LEVELS.floating) { ... }
1144
+ * if (window.level >= WINDOW_LEVELS.mainMenu) { ... } // at or above the menu bar
1145
+ * ```
1146
+ *
1147
+ * Levels are `Comparable` in Swift: a higher number is drawn in front of a lower one. Some names
1148
+ * share the same numeric value (e.g. `floating`, `submenu`, `tornOffMenu` are all `3`).
1149
+ *
1150
+ * @group Discovery
1151
+ */
1152
+ const WINDOW_LEVELS = {
1153
+ baseWindow: -2147483648,
1154
+ minimumWindow: -2147483643,
1155
+ desktopWindow: -2147483623,
1156
+ desktopIconWindow: -2147483603,
1157
+ backstopMenu: -20,
1158
+ normal: 0,
1159
+ floating: 3,
1160
+ submenu: 3,
1161
+ tornOffMenu: 3,
1162
+ modalPanel: 8,
1163
+ utilityWindow: 19,
1164
+ mainMenu: 24,
1165
+ statusBar: 25,
1166
+ popUpMenu: 101,
1167
+ overlayWindow: 102,
1168
+ helpWindow: 200,
1169
+ draggingWindow: 500,
1170
+ screenSaver: 1000,
1171
+ screenSaverWindow: 1000,
1172
+ assistiveTechHighWindow: 1500,
1173
+ cursorWindow: 2147483630,
1174
+ maximumWindow: 2147483631,
1175
+ };
1176
+
1177
+ /**
1178
+ * Creates a Web Audio API AudioBuffer from a RecordKit AudioStreamBuffer.
1179
+ *
1180
+ * This utility converts RecordKit's streaming audio format to the standard Web Audio API format,
1181
+ * handling various edge cases and IPC serialization issues that may occur in Electron environments.
1182
+ *
1183
+ * @param audioStreamBuffer - The RecordKit AudioStreamBuffer to convert
1184
+ * @param audioContext - The Web Audio API AudioContext to use for buffer creation
1185
+ * @returns The created AudioBuffer, or null if conversion failed
1186
+ *
1187
+ * @example
1188
+ * ```typescript
1189
+ * import { createWebAudioBuffer } from '@nonstrict/recordkit';
1190
+ *
1191
+ * // In your stream callback
1192
+ * const streamCallback = (audioBuffer: AudioStreamBuffer) => {
1193
+ * const audioContext = new AudioContext();
1194
+ * const webAudioBuffer = createWebAudioBuffer(audioBuffer, audioContext);
1195
+ *
1196
+ * if (webAudioBuffer) {
1197
+ * // Use the buffer with Web Audio API
1198
+ * const source = audioContext.createBufferSource();
1199
+ * source.buffer = webAudioBuffer;
1200
+ * source.connect(audioContext.destination);
1201
+ * source.start();
1202
+ * }
1203
+ * };
1204
+ * ```
1205
+ */
1206
+ function createWebAudioBuffer(audioStreamBuffer, audioContext) {
1207
+ // Input validation
1208
+ if (!audioStreamBuffer || typeof audioStreamBuffer !== 'object') {
1209
+ return null;
1210
+ }
1211
+ if (!audioContext || typeof audioContext.createBuffer !== 'function') {
1212
+ return null;
1213
+ }
1214
+ try {
1215
+ const { sampleRate, numberOfChannels, numberOfFrames, channelData } = audioStreamBuffer;
1216
+ // Validate required properties
1217
+ if (typeof sampleRate !== 'number' || sampleRate <= 0 || sampleRate > 192000) {
1218
+ return null;
1219
+ }
1220
+ if (typeof numberOfChannels !== 'number' || numberOfChannels <= 0 || numberOfChannels > 32) {
1221
+ return null;
1222
+ }
1223
+ if (typeof numberOfFrames !== 'number' || numberOfFrames <= 0 || numberOfFrames > 1048576) {
1224
+ return null;
1225
+ }
1226
+ if (!Array.isArray(channelData) || channelData.length !== numberOfChannels) {
1227
+ return null;
1228
+ }
1229
+ // Validate channel data arrays
1230
+ for (let i = 0; i < numberOfChannels; i++) {
1231
+ const channel = channelData[i];
1232
+ if (!channel || (!Array.isArray(channel) && !(channel instanceof Float32Array))) {
1233
+ return null;
1234
+ }
1235
+ // Check length matches expected frame count
1236
+ if (channel.length !== numberOfFrames) {
1237
+ return null;
1238
+ }
1239
+ }
1240
+ // Create Web Audio AudioBuffer
1241
+ const audioBuffer = audioContext.createBuffer(numberOfChannels, numberOfFrames, sampleRate);
1242
+ // Copy channel data to AudioBuffer
1243
+ for (let channel = 0; channel < numberOfChannels; channel++) {
1244
+ const outputArray = audioBuffer.getChannelData(channel);
1245
+ const inputArray = channelData[channel];
1246
+ // Handle both Float32Array and regular arrays (from Electron IPC serialization)
1247
+ if (inputArray instanceof Float32Array) {
1248
+ // Direct copy for Float32Array
1249
+ outputArray.set(inputArray);
1250
+ }
1251
+ else if (Array.isArray(inputArray)) {
1252
+ // Convert regular array to Float32Array for better performance
1253
+ const float32Array = new Float32Array(inputArray);
1254
+ outputArray.set(float32Array);
1255
+ }
1256
+ else {
1257
+ // Fallback: manual copy with type conversion
1258
+ for (let i = 0; i < numberOfFrames; i++) {
1259
+ const sample = inputArray[i];
1260
+ outputArray[i] = typeof sample === 'number' && isFinite(sample) ? sample : 0;
1261
+ }
1262
+ }
1263
+ }
1264
+ return audioBuffer;
1265
+ }
1266
+ catch (error) {
1267
+ // Return null for any conversion failures - don't throw in streaming contexts
1268
+ return null;
1269
+ }
1270
+ }
1271
+ /**
1272
+ * Computes RMS and peak audio levels from an {@link AudioStreamBuffer}, for rendering a microphone (or
1273
+ * system-audio) level meter.
1274
+ *
1275
+ * Electron has no dedicated microphone-preview component; the supported way to render a live level meter
1276
+ * is to use a microphone/system-audio `stream` output and call this helper in your `streamCallback`.
1277
+ *
1278
+ * @example
1279
+ * ```typescript
1280
+ * import { computeAudioLevel } from '@nonstrict/recordkit';
1281
+ *
1282
+ * const streamCallback = (audioBuffer) => {
1283
+ * const { peakDb } = computeAudioLevel(audioBuffer);
1284
+ * meterElement.style.height = `${Math.max(0, 100 + peakDb)}%`; // -100 dB..0 dB -> 0%..100%
1285
+ * };
1286
+ * ```
1287
+ *
1288
+ * @group Recording
1289
+ */
1290
+ function computeAudioLevel(audioStreamBuffer) {
1291
+ let sumSquares = 0;
1292
+ let peak = 0;
1293
+ let count = 0;
1294
+ for (const channel of audioStreamBuffer.channelData) {
1295
+ for (let i = 0; i < channel.length; i++) {
1296
+ const sample = channel[i];
1297
+ sumSquares += sample * sample;
1298
+ const abs = Math.abs(sample);
1299
+ if (abs > peak)
1300
+ peak = abs;
1301
+ count++;
1302
+ }
1303
+ }
1304
+ const rms = count > 0 ? Math.sqrt(sumSquares / count) : 0;
1305
+ const toDb = (value) => value > 0 ? 20 * Math.log10(value) : -Infinity;
1306
+ return { rms, peak, rmsDb: toDb(rms), peakDb: toDb(peak) };
1307
+ }
1308
+
1309
+ exports.RECORDKIT_ERROR_CODE_NUMBERS = RECORDKIT_ERROR_CODE_NUMBERS;
1310
+ exports.WINDOW_LEVELS = WINDOW_LEVELS;
1311
+ exports.computeAudioLevel = computeAudioLevel;
1312
+ exports.createWebAudioBuffer = createWebAudioBuffer;
719
1313
  exports.recordkit = recordkit;
720
1314
  //# sourceMappingURL=index.cjs.map