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