@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/bin/README.md +1 -1
- package/bin/recordkit-rpc +0 -0
- package/out/RecordKit.d.ts +174 -1
- package/out/RecordKit.js +108 -2
- package/out/RecordKit.js.map +1 -1
- package/out/Recorder.d.ts +92 -2
- package/out/Recorder.js +93 -0
- package/out/Recorder.js.map +1 -1
- package/out/WebAudioUtils.d.ts +31 -0
- package/out/WebAudioUtils.js +95 -0
- package/out/WebAudioUtils.js.map +1 -0
- package/out/browser.d.ts +2 -0
- package/out/browser.js +4 -0
- package/out/browser.js.map +1 -0
- package/out/index.cjs +201 -2
- package/out/index.cjs.map +1 -1
- package/package.json +6 -3
- package/src/RecordKit.ts +214 -2
- package/src/Recorder.ts +196 -1
- package/src/WebAudioUtils.ts +108 -0
- package/src/browser.ts +7 -0
- package/tsconfig.json +2 -1
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. */
|