@nonstrict/recordkit 0.87.2 → 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.
- package/bin/README.md +1 -1
- package/bin/recordkit-rpc +0 -0
- package/out/Errors.d.ts +91 -0
- package/out/Errors.js +63 -0
- package/out/Errors.js.map +1 -0
- package/out/InputEvents.d.ts +661 -0
- package/out/InputEvents.js +8 -0
- package/out/InputEvents.js.map +1 -0
- package/out/IpcRecordKit.js +13 -0
- package/out/IpcRecordKit.js.map +1 -1
- package/out/NonstrictRPC.d.ts +9 -0
- package/out/NonstrictRPC.js +27 -4
- package/out/NonstrictRPC.js.map +1 -1
- package/out/RecordKit.d.ts +267 -5
- package/out/RecordKit.js +243 -3
- package/out/RecordKit.js.map +1 -1
- package/out/Recorder.d.ts +451 -53
- package/out/Recorder.js +80 -2
- package/out/Recorder.js.map +1 -1
- package/out/RecordingMetadata.d.ts +96 -0
- package/out/RecordingMetadata.js +12 -0
- package/out/RecordingMetadata.js.map +1 -0
- package/out/WebAudioUtils.d.ts +35 -0
- package/out/WebAudioUtils.js +37 -0
- package/out/WebAudioUtils.js.map +1 -1
- package/out/WindowLevels.d.ts +46 -0
- package/out/WindowLevels.js +41 -0
- package/out/WindowLevels.js.map +1 -0
- package/out/browser.d.ts +8 -1
- package/out/browser.js +3 -1
- package/out/browser.js.map +1 -1
- package/out/index.cjs +603 -9
- package/out/index.cjs.map +1 -1
- package/out/index.d.ts +8 -0
- package/out/index.js +3 -0
- package/out/index.js.map +1 -1
- package/package.json +1 -1
- package/src/Errors.test.ts +38 -0
- package/src/Errors.ts +151 -0
- package/src/InputEvents.ts +695 -0
- package/src/IpcRecordKit.ts +12 -0
- package/src/NonstrictRPC.test.ts +24 -1
- package/src/NonstrictRPC.ts +29 -5
- package/src/RecordKit.ts +347 -11
- package/src/Recorder.schema.test.ts +167 -0
- package/src/Recorder.ts +558 -80
- package/src/RecordingMetadata.ts +125 -0
- package/src/WebAudioUtils.test.ts +78 -0
- package/src/WebAudioUtils.ts +57 -1
- package/src/WindowLevels.test.ts +34 -0
- package/src/WindowLevels.ts +47 -0
- package/src/browser.ts +12 -2
- package/src/index.ts +8 -0
- 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
|
-
|
|
495
|
-
|
|
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
|
-
*
|
|
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
|