@nonstrict/recordkit 0.51.1 → 0.53.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/src/Recorder.ts CHANGED
@@ -3,6 +3,54 @@ import { NSRPC } from "./NonstrictRPC.js";
3
3
  import { EventEmitter } from "stream";
4
4
  import { AppleDevice, Camera, Display, Microphone, RunningApplication, Window } from "./RecordKit.js";
5
5
 
6
+ /**
7
+ * Converts RPC audio buffer data to AudioStreamBuffer format
8
+ * @internal
9
+ */
10
+ function convertRPCParamsToAudioStreamBuffer(params: any): AudioStreamBuffer | null {
11
+ try {
12
+ // params is the AudioBufferData directly from Swift
13
+ const rawAudioBuffer = params as any;
14
+
15
+ if (!rawAudioBuffer || !Array.isArray(rawAudioBuffer.channelData)) {
16
+ console.error('RecordKit: Invalid audio buffer received from RPC');
17
+ return null;
18
+ }
19
+
20
+ const channelData: Float32Array[] = [];
21
+
22
+ for (const base64Data of rawAudioBuffer.channelData) {
23
+ if (typeof base64Data !== 'string') {
24
+ console.error('RecordKit: Invalid base64 data received');
25
+ return null;
26
+ }
27
+
28
+ // Decode base64 to binary data
29
+ const binaryString = atob(base64Data);
30
+ const bytes = new Uint8Array(binaryString.length);
31
+ for (let i = 0; i < binaryString.length; i++) {
32
+ bytes[i] = binaryString.charCodeAt(i);
33
+ }
34
+
35
+ // Convert bytes to Float32Array
36
+ const float32Array = new Float32Array(bytes.buffer);
37
+ channelData.push(float32Array);
38
+ }
39
+
40
+ const audioStreamBuffer: AudioStreamBuffer = {
41
+ sampleRate: rawAudioBuffer.sampleRate,
42
+ numberOfChannels: rawAudioBuffer.numberOfChannels,
43
+ numberOfFrames: rawAudioBuffer.numberOfFrames,
44
+ channelData: channelData
45
+ };
46
+
47
+ return audioStreamBuffer;
48
+ } catch (error) {
49
+ console.error('RecordKit: Error processing audio stream buffer:', error);
50
+ return null;
51
+ }
52
+ }
53
+
6
54
  /**
7
55
  * @group Recording
8
56
  */
@@ -70,6 +118,19 @@ export class Recorder extends EventEmitter {
70
118
  lifecycle: object
71
119
  });
72
120
  }
121
+ if (item.output == 'stream' && item.streamCallback) {
122
+ const streamHandler = item.streamCallback;
123
+ (item as any).streamCallback = rpc.registerClosure({
124
+ handler: (params) => {
125
+ const audioBuffer = convertRPCParamsToAudioStreamBuffer(params);
126
+ if (audioBuffer) {
127
+ streamHandler(audioBuffer);
128
+ }
129
+ },
130
+ prefix: 'SystemAudioStream.onAudioBuffer',
131
+ lifecycle: object
132
+ });
133
+ }
73
134
  }
74
135
  if (item.type == 'applicationAudio') {
75
136
  if (item.output == 'segmented' && item.segmentCallback) {
@@ -80,6 +141,45 @@ export class Recorder extends EventEmitter {
80
141
  lifecycle: object
81
142
  });
82
143
  }
144
+ if (item.output == 'stream' && item.streamCallback) {
145
+ const streamHandler = item.streamCallback;
146
+ (item as any).streamCallback = rpc.registerClosure({
147
+ handler: (params) => {
148
+ const audioBuffer = convertRPCParamsToAudioStreamBuffer(params);
149
+ if (audioBuffer) {
150
+ streamHandler(audioBuffer);
151
+ }
152
+ },
153
+ prefix: 'ApplicationAudioStream.onAudioBuffer',
154
+ lifecycle: object
155
+ });
156
+ }
157
+ }
158
+ if (item.type == 'microphone') {
159
+ if (typeof item.microphone != 'string') {
160
+ item.microphone = item.microphone.id
161
+ }
162
+ if (item.output == 'segmented' && item.segmentCallback) {
163
+ const segmentHandler = item.segmentCallback;
164
+ (item as any).segmentCallback = rpc.registerClosure({
165
+ handler: (params) => { segmentHandler(params.path as string) },
166
+ prefix: 'Microphone.onSegment',
167
+ lifecycle: object
168
+ });
169
+ }
170
+ if (item.output == 'stream' && item.streamCallback) {
171
+ const streamHandler = item.streamCallback;
172
+ (item as any).streamCallback = rpc.registerClosure({
173
+ handler: (params) => {
174
+ const audioBuffer = convertRPCParamsToAudioStreamBuffer(params);
175
+ if (audioBuffer) {
176
+ streamHandler(audioBuffer);
177
+ }
178
+ },
179
+ prefix: 'MicrophoneStream.onAudioBuffer',
180
+ lifecycle: object
181
+ });
182
+ }
83
183
  }
84
184
  })
85
185
 
@@ -134,8 +234,11 @@ export type RecorderSchemaItem =
134
234
  | AppleDeviceStaticOrientationSchema
135
235
  | SystemAudioSchema
136
236
  | ApplicationAudioSchema
237
+ | MicrophoneSchema
137
238
 
138
239
  /**
240
+ * Creates a recorder item for a webcam movie file, using the provided microphone and camera. Output is stored in a RecordKit bundle.
241
+ *
139
242
  * @group Recording Schemas
140
243
  */
141
244
  export interface WebcamSchema {
@@ -146,6 +249,8 @@ export interface WebcamSchema {
146
249
  }
147
250
 
148
251
  /**
252
+ * Creates a recorder item for recording a single display. Output is stored in a RecordKit bundle.
253
+ *
149
254
  * @group Recording Schemas
150
255
  */
151
256
  export type DisplaySchema = {
@@ -170,6 +275,8 @@ export type DisplaySchema = {
170
275
  }
171
276
 
172
277
  /**
278
+ * Creates a recorder item for recording the initial crop of a window on a display. Output is stored in a RecordKit bundle.
279
+ *
173
280
  * @group Recording Schemas
174
281
  */
175
282
  export type WindowBasedCropSchema = {
@@ -192,6 +299,8 @@ export type WindowBasedCropSchema = {
192
299
  }
193
300
 
194
301
  /**
302
+ * Creates a recorder item for an Apple device screen recording, using the provided deviceID. Output is stored in a RecordKit bundle.
303
+ *
195
304
  * @group Recording Schemas
196
305
  */
197
306
  export interface AppleDeviceStaticOrientationSchema {
@@ -206,6 +315,11 @@ export interface AppleDeviceStaticOrientationSchema {
206
315
  export type SystemAudioMode = 'exclude' | 'include'
207
316
 
208
317
  /**
318
+ * Enumeration specifying the backend to use for system audio recording.
319
+ *
320
+ * - `screenCaptureKit`: Use ScreenCaptureKit for system audio recording.
321
+ * - `_beta_coreAudio`: This a beta feature, it is not fully implemented yet. Do not use in production. Currently only records single files in .caf format.
322
+ *
209
323
  * @group Recording Schemas
210
324
  */
211
325
  export type SystemAudioBackend = 'screenCaptureKit' | '_beta_coreAudio'
@@ -213,9 +327,19 @@ export type SystemAudioBackend = 'screenCaptureKit' | '_beta_coreAudio'
213
327
  /**
214
328
  * @group Recording Schemas
215
329
  */
216
- export type AudioOutputOptionsType = 'singleFile' | 'segmented'
330
+ export type AudioOutputOptionsType = 'singleFile' | 'segmented' | 'stream'
331
+
332
+ /**
333
+ * @group Recording Schemas
334
+ */
335
+ export type MicrophoneOutputOptionsType = 'singleFile' | 'segmented' | 'stream'
217
336
 
218
337
  /**
338
+ * Creates a recorder item for recording system audio. By default current process audio is excluded. Output is stored in a RecordKit bundle.
339
+ *
340
+ * When using `mode: 'exclude'`, all system audio is recorded except for excluded applications.
341
+ * When using `mode: 'include'`, only audio from specified applications is recorded.
342
+ *
219
343
  * @group Recording Schemas
220
344
  */
221
345
  export type SystemAudioSchema = {
@@ -235,6 +359,15 @@ export type SystemAudioSchema = {
235
359
  output: 'segmented'
236
360
  filenamePrefix?: string
237
361
  segmentCallback?: (url: string) => void
362
+ } | {
363
+ type: 'systemAudio'
364
+ mode: 'exclude'
365
+ backend?: SystemAudioBackend
366
+ excludeOptions?: ('currentProcess')[]
367
+ excludedProcessIDs?: number[] // Int32
368
+ output: 'stream'
369
+ /** Called with real-time audio buffer data compatible with Web Audio API. Requires _beta_coreAudio backend and macOS 14.2+ */
370
+ streamCallback?: (audioBuffer: AudioStreamBuffer) => void
238
371
  } | {
239
372
  type: 'systemAudio'
240
373
  mode: 'include'
@@ -250,9 +383,19 @@ export type SystemAudioSchema = {
250
383
  output: 'segmented'
251
384
  filenamePrefix?: string
252
385
  segmentCallback?: (url: string) => void
386
+ } | {
387
+ type: 'systemAudio'
388
+ mode: 'include'
389
+ backend?: SystemAudioBackend
390
+ includedApplicationIDs?: number[] // Int32
391
+ output: 'stream'
392
+ /** Called with real-time audio buffer data compatible with Web Audio API. Requires _beta_coreAudio backend and macOS 14.2+ */
393
+ streamCallback?: (audioBuffer: AudioStreamBuffer) => void
253
394
  }
254
395
 
255
396
  /**
397
+ * Creates a recorder item for recording the audio of a single application. Output is stored in a RecordKit bundle.
398
+ *
256
399
  * @group Recording Schemas
257
400
  */
258
401
  export type ApplicationAudioSchema = {
@@ -268,8 +411,60 @@ export type ApplicationAudioSchema = {
268
411
  output: 'segmented'
269
412
  filenamePrefix?: string
270
413
  segmentCallback?: (url: string) => void
414
+ } | {
415
+ type: 'applicationAudio'
416
+ applicationID: number // Int32
417
+ backend?: SystemAudioBackend
418
+ output: 'stream'
419
+ /** Called with real-time audio buffer data compatible with Web Audio API. Requires _beta_coreAudio backend and macOS 14.2+ */
420
+ streamCallback?: (audioBuffer: AudioStreamBuffer) => void
421
+ }
422
+
423
+ /**
424
+ * Creates a recorder item for an audio file, using the provided microphone. Output is stored in a RecordKit bundle.
425
+ *
426
+ * @group Recording Schemas
427
+ */
428
+ export type MicrophoneSchema = {
429
+ type: 'microphone'
430
+ microphone: Microphone | string
431
+ leftChannelOnly?: boolean
432
+ audioDelay?: number
433
+ output?: 'singleFile'
434
+ filename?: string
435
+ } | {
436
+ type: 'microphone'
437
+ microphone: Microphone | string
438
+ leftChannelOnly?: boolean
439
+ audioDelay?: number
440
+ output: 'segmented'
441
+ filenamePrefix?: string
442
+ segmentCallback?: (url: string) => void
443
+ } | {
444
+ type: 'microphone'
445
+ microphone: Microphone | string
446
+ output: 'stream'
447
+ /** Called with real-time audio buffer data compatible with Web Audio API */
448
+ streamCallback?: (audioBuffer: AudioStreamBuffer) => void
449
+ }
450
+
451
+ /**
452
+ * Audio buffer compatible with Web Audio API
453
+ *
454
+ * @group Recording
455
+ */
456
+ export interface AudioStreamBuffer {
457
+ /** Sample rate in Hz (e.g., 44100, 48000) */
458
+ sampleRate: number
459
+ /** Number of audio channels */
460
+ numberOfChannels: number
461
+ /** Number of frames per channel */
462
+ numberOfFrames: number
463
+ /** Non-interleaved Float32 audio data - one array per channel */
464
+ channelData: Float32Array[]
271
465
  }
272
466
 
467
+
273
468
  /**
274
469
  * @group Recording
275
470
  */
@@ -0,0 +1,108 @@
1
+ import { AudioStreamBuffer } from './Recorder.js';
2
+
3
+ /**
4
+ * Creates a Web Audio API AudioBuffer from a RecordKit AudioStreamBuffer.
5
+ *
6
+ * This utility converts RecordKit's streaming audio format to the standard Web Audio API format,
7
+ * handling various edge cases and IPC serialization issues that may occur in Electron environments.
8
+ *
9
+ * @param audioStreamBuffer - The RecordKit AudioStreamBuffer to convert
10
+ * @param audioContext - The Web Audio API AudioContext to use for buffer creation
11
+ * @returns The created AudioBuffer, or null if conversion failed
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import { createWebAudioBuffer } from '@nonstrict/recordkit';
16
+ *
17
+ * // In your stream callback
18
+ * const streamCallback = (audioBuffer: AudioStreamBuffer) => {
19
+ * const audioContext = new AudioContext();
20
+ * const webAudioBuffer = createWebAudioBuffer(audioBuffer, audioContext);
21
+ *
22
+ * if (webAudioBuffer) {
23
+ * // Use the buffer with Web Audio API
24
+ * const source = audioContext.createBufferSource();
25
+ * source.buffer = webAudioBuffer;
26
+ * source.connect(audioContext.destination);
27
+ * source.start();
28
+ * }
29
+ * };
30
+ * ```
31
+ */
32
+ export function createWebAudioBuffer(
33
+ audioStreamBuffer: AudioStreamBuffer,
34
+ audioContext: AudioContext
35
+ ): AudioBuffer | null {
36
+ // Input validation
37
+ if (!audioStreamBuffer || typeof audioStreamBuffer !== 'object') {
38
+ return null;
39
+ }
40
+
41
+ if (!audioContext || typeof audioContext.createBuffer !== 'function') {
42
+ return null;
43
+ }
44
+
45
+ try {
46
+ const { sampleRate, numberOfChannels, numberOfFrames, channelData } = audioStreamBuffer;
47
+
48
+ // Validate required properties
49
+ if (typeof sampleRate !== 'number' || sampleRate <= 0 || sampleRate > 192000) {
50
+ return null;
51
+ }
52
+
53
+ if (typeof numberOfChannels !== 'number' || numberOfChannels <= 0 || numberOfChannels > 32) {
54
+ return null;
55
+ }
56
+
57
+ if (typeof numberOfFrames !== 'number' || numberOfFrames <= 0 || numberOfFrames > 1048576) {
58
+ return null;
59
+ }
60
+
61
+ if (!Array.isArray(channelData) || channelData.length !== numberOfChannels) {
62
+ return null;
63
+ }
64
+
65
+ // Validate channel data arrays
66
+ for (let i = 0; i < numberOfChannels; i++) {
67
+ const channel = channelData[i];
68
+ if (!channel || (!Array.isArray(channel) && !(channel instanceof Float32Array))) {
69
+ return null;
70
+ }
71
+
72
+ // Check length matches expected frame count
73
+ if (channel.length !== numberOfFrames) {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ // Create Web Audio AudioBuffer
79
+ const audioBuffer = audioContext.createBuffer(numberOfChannels, numberOfFrames, sampleRate);
80
+
81
+ // Copy channel data to AudioBuffer
82
+ for (let channel = 0; channel < numberOfChannels; channel++) {
83
+ const outputArray = audioBuffer.getChannelData(channel);
84
+ const inputArray = channelData[channel];
85
+
86
+ // Handle both Float32Array and regular arrays (from Electron IPC serialization)
87
+ if (inputArray instanceof Float32Array) {
88
+ // Direct copy for Float32Array
89
+ outputArray.set(inputArray);
90
+ } else if (Array.isArray(inputArray)) {
91
+ // Convert regular array to Float32Array for better performance
92
+ const float32Array = new Float32Array(inputArray);
93
+ outputArray.set(float32Array);
94
+ } else {
95
+ // Fallback: manual copy with type conversion
96
+ for (let i = 0; i < numberOfFrames; i++) {
97
+ const sample = inputArray[i];
98
+ outputArray[i] = typeof sample === 'number' && isFinite(sample) ? sample : 0;
99
+ }
100
+ }
101
+ }
102
+
103
+ return audioBuffer;
104
+ } catch (error) {
105
+ // Return null for any conversion failures - don't throw in streaming contexts
106
+ return null;
107
+ }
108
+ }
package/src/browser.ts ADDED
@@ -0,0 +1,7 @@
1
+ // Browser-only exports - safe for use in browser environments
2
+ // This entry point excludes Node.js-specific functionality like EventEmitter, crypto, etc.
3
+
4
+ export { createWebAudioBuffer } from './WebAudioUtils.js';
5
+
6
+ // Type-only exports for browser use
7
+ export type { AudioStreamBuffer } from './Recorder.js';
package/tsconfig.json CHANGED
@@ -17,7 +17,8 @@
17
17
  /* Language and Environment */
18
18
  "target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
19
19
  "lib": [
20
- "ES2022"
20
+ "ES2022",
21
+ "DOM"
21
22
  ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
22
23
  // "jsx": "preserve", /* Specify what JSX code is generated. */
23
24
  // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */