@recallai/desktop-sdk 2.0.7 → 2.0.9

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/CHANGELOG.md CHANGED
@@ -1,4 +1,40 @@
1
- ### 2.0.7 [patch] (MacOS+Windows) - 2026-02-11
1
+ ### 2.0.9 [patch] (MacOS+Windows) - 2026-03-27
2
+ - Forward invalid command errors back to user on Windows.
3
+ - Restart video capture on windows as necessary.
4
+ - Improvements to audio device tracking algorithms on Windows.
5
+ - Teams audio capture fixes on Windows.
6
+ - Teams delayed detection fixes on Windows.
7
+ - Fixed issue where Chromium browsers would occasionally experience slow detection speed when joining the meeting with the microphone muted on MacOS
8
+ - Reduced initial startup burden associated with mic logs on MacOS
9
+ - Zoom screenshare detection works with annotations disabled
10
+ - Fixed issues with speaker labels being misattributed in > 2 participant meetings on MacOS
11
+ - Fixed issue with Teams not firing meeting-closed when closed from PIP view with main window minimized on MacOS
12
+ - Fixed issues with Arc / Brave sometimes failing to detect after-call screen on MacOS
13
+ - Fixed issues with chromium browsers prematurely ending meeting when switching tabs without PIP enabled on MacOS
14
+ - Fixed issue where transcript events would stop firing after recovering from network outage on MacOS
15
+ - Fixed issue where having video frame capture enabled would occasionally cause crashes on MacOS
16
+ - Fixed issue where Brave meetings would sometimes not be detected after leaving and rejoining meeting quickly on MacOS
17
+ - Fixed issue where audio would sometimes die mid-stream causing transcription and audio loss
18
+ - Fixed issue where audio streams would die and never recover
19
+ - Add additional telemetry to audio stream lifecycle
20
+ - Add additional telemetry and fix to commands not properly responding on macOS
21
+
22
+ ### 2.0.8 [patch] (MacOS+Windows) - 2026-03-13
23
+ - Reduced blips/audio-glitches in the audio mixing process
24
+ - Fixed Windows audio latency during capture
25
+ - Added Windows network-status event and automatic recording stopping.
26
+ - More robust device switching and restarting on windows.
27
+ - Prevent google meet updated events after recording stopped on macOS.
28
+ - Fixed Brave detection on macOS
29
+ - Fixed Chrome fullscreen detection on macOS
30
+ - Fixed issue for Google Meet where compliance message would send to wrong input when different window is focused on macOS
31
+ - Fixed issue for Zoom where host would incorrectly get recognized as a separate participant on macOS
32
+ - Fixed issue for screenshare capturing where screen sharing indicator would sometimes persist after recording ended on macOS
33
+ - Added ZoomAudioDevice to mic capture blacklist on macOS
34
+ - Fixed Chrome title detection on macOS
35
+ - Supported title on MS Teams on macOS
36
+
37
+ ### 2.0.7 [patch] (MacOS+Windows) - 2026-02-26
2
38
  - Fixed issues capturing audio when devices change on windows.
3
39
  - Added a network shutdown event on macOS.
4
40
  - Set recording id on SDK events on macOS and windows.
@@ -7,8 +43,7 @@
7
43
  ### 2.0.6 [patch] (MacOS+Windows) - 2026-02-11
8
44
  - Fix case where recording-ended would not trigger on Windows
9
45
 
10
- ### 2.0.5 [patch] (MacOS+Windows) - 2026-02-11
11
-
46
+ ### 2.0.5 [patch] (MacOS+Windows) - 2026-02-11 (deprecated)
12
47
  - Support for Google Chrome 145.0.7632.46+
13
48
  - Improvements for Brave
14
49
  - Automatically detect which microphone is being used by the meeting platform
package/index.d.ts CHANGED
@@ -33,6 +33,10 @@ export interface RecallAiSdkConfig {
33
33
  acquirePermissionsOnStartup?: Permission[];
34
34
  restartOnError?: boolean;
35
35
  dev?: boolean;
36
+ testMode?: boolean;
37
+ testSpeedModifier?: string;
38
+ testTargetBundleId?: string;
39
+ testTargetBundleIdRemapped?: string;
36
40
  }
37
41
  export interface StartRecordingConfig {
38
42
  windowId: string;
@@ -48,7 +52,7 @@ export interface ResumeRecordingConfig {
48
52
  windowId: string;
49
53
  }
50
54
  /**
51
- * @deprecated Recordings are uploaded based on the upload token config.
55
+ * @deprecated Recordings are automatically uploaded based on your rentention configuration. This is now a no-op.
52
56
  */
53
57
  export interface UploadRecordingConfig {
54
58
  windowId: string;
@@ -130,13 +134,15 @@ export declare function stopRecording({ windowId }: StopRecordingConfig): Promis
130
134
  export declare function pauseRecording({ windowId }: PauseRecordingConfig): Promise<null>;
131
135
  export declare function resumeRecording({ windowId }: ResumeRecordingConfig): Promise<null>;
132
136
  /**
133
- * @deprecated Recordings are uploaded based on the upload token config.
137
+ * @deprecated Recordings are automatically uploaded based on your rentention configuration. This is now a no-op.
134
138
  */
135
139
  export declare function uploadRecording({ windowId }: UploadRecordingConfig): Promise<null>;
136
140
  export declare function prepareDesktopAudioRecording(): Promise<string>;
137
141
  export declare function requestPermission(permission: Permission): Promise<null>;
138
142
  export declare function testUnexpectedShutdown(): Promise<null>;
139
143
  export declare function addEventListener<T extends keyof EventTypeToPayloadMap>(type: T, callback: (event: EventTypeToPayloadMap[T]) => void): void;
144
+ export declare function removeEventListener<T extends keyof EventTypeToPayloadMap>(type: T, callback: (event: EventTypeToPayloadMap[T]) => void): void;
145
+ export declare function removeAllEventListeners(): void;
140
146
  declare const RecallAiSdk: {
141
147
  init: typeof init;
142
148
  shutdown: typeof shutdown;
@@ -148,6 +154,8 @@ declare const RecallAiSdk: {
148
154
  prepareDesktopAudioRecording: typeof prepareDesktopAudioRecording;
149
155
  requestPermission: typeof requestPermission;
150
156
  addEventListener: typeof addEventListener;
157
+ removeEventListener: typeof removeEventListener;
158
+ removeAllEventListeners: typeof removeAllEventListeners;
151
159
  testUnexpectedShutdown: typeof testUnexpectedShutdown;
152
160
  };
153
161
  export default RecallAiSdk;
package/index.js CHANGED
@@ -46,6 +46,8 @@ exports.prepareDesktopAudioRecording = prepareDesktopAudioRecording;
46
46
  exports.requestPermission = requestPermission;
47
47
  exports.testUnexpectedShutdown = testUnexpectedShutdown;
48
48
  exports.addEventListener = addEventListener;
49
+ exports.removeEventListener = removeEventListener;
50
+ exports.removeAllEventListeners = removeAllEventListeners;
49
51
  const path = __importStar(require("path"));
50
52
  const child_process_1 = require("child_process");
51
53
  const readline = __importStar(require("node:readline"));
@@ -76,6 +78,14 @@ let lastOptions;
76
78
  let remainingAutomaticRestarts = 10;
77
79
  let unexpectedShutdown = false;
78
80
  let exiting = false;
81
+ let pendingStdinWrites = [];
82
+ let flushingPendingStdinWrites = false;
83
+ let waitingForStdinDrain = false;
84
+ let stdinWriteSession = 0;
85
+ let nextWriteSequence = 1;
86
+ let activeDrainStartMs = null;
87
+ let activeDrainStartIso = null;
88
+ let activeDrainWriteSequence = null;
79
89
  let logBuffer = [];
80
90
  let logIndex = 0;
81
91
  let packageVersion;
@@ -185,6 +195,9 @@ async function doLog(level, log, echo = true) {
185
195
  }
186
196
  }
187
197
  catch (e) {
198
+ if (proc?.exitCode === 0 || exiting) {
199
+ return;
200
+ }
188
201
  console.error("Failed to send log to Desktop SDK", e.stack, e.message);
189
202
  }
190
203
  }
@@ -213,6 +226,157 @@ function flushPendingCommands(err) {
213
226
  delete pendingCommands[commandId];
214
227
  });
215
228
  }
229
+ function resolvePendingCommand(commandId, result) {
230
+ const pendingCommand = pendingCommands[commandId];
231
+ if (!pendingCommand) {
232
+ return;
233
+ }
234
+ pendingCommand.resolve(result);
235
+ delete pendingCommands[commandId];
236
+ }
237
+ function rejectPendingCommand(commandId, err) {
238
+ const pendingCommand = pendingCommands[commandId];
239
+ if (!pendingCommand) {
240
+ return;
241
+ }
242
+ pendingCommand.reject(err);
243
+ delete pendingCommands[commandId];
244
+ }
245
+ function resetPendingStdinWrites() {
246
+ pendingStdinWrites = [];
247
+ flushingPendingStdinWrites = false;
248
+ waitingForStdinDrain = false;
249
+ stdinWriteSession++;
250
+ activeDrainStartMs = null;
251
+ activeDrainStartIso = null;
252
+ activeDrainWriteSequence = null;
253
+ }
254
+ // Serialize writes into the child stdin and stop when the stream applies backpressure.
255
+ function flushPendingStdinWrites() {
256
+ if (flushingPendingStdinWrites || waitingForStdinDrain) {
257
+ return;
258
+ }
259
+ const currentProc = proc;
260
+ const stdin = currentProc?.stdin;
261
+ if (!currentProc || !stdin || currentProc.exitCode !== null) {
262
+ return;
263
+ }
264
+ flushingPendingStdinWrites = true;
265
+ const session = stdinWriteSession;
266
+ try {
267
+ while (pendingStdinWrites.length > 0) {
268
+ if (proc !== currentProc || currentProc.stdin !== stdin || currentProc.exitCode !== null) {
269
+ return;
270
+ }
271
+ const nextWrite = pendingStdinWrites.shift();
272
+ if (!nextWrite) {
273
+ break;
274
+ }
275
+ const canContinue = stdin.write(nextWrite.data, (err) => {
276
+ if (!err) {
277
+ return;
278
+ }
279
+ if (session !== stdinWriteSession) {
280
+ return;
281
+ }
282
+ rejectPendingCommand(nextWrite.commandId, new Error(`Failed writing ${nextWrite.command} command [${nextWrite.writeSequence}] (${nextWrite.commandId}) to Desktop SDK stdin: ${err.message}`));
283
+ if (currentProc.exitCode === null && !currentProc.killed) {
284
+ currentProc.kill();
285
+ }
286
+ });
287
+ if (!canContinue) {
288
+ activeDrainStartMs = Date.now();
289
+ activeDrainStartIso = new Date(activeDrainStartMs).toISOString();
290
+ activeDrainWriteSequence = nextWrite.writeSequence;
291
+ waitingForStdinDrain = true;
292
+ stdin.once('drain', () => {
293
+ if (session !== stdinWriteSession) {
294
+ return;
295
+ }
296
+ if (proc !== currentProc || currentProc.stdin !== stdin || currentProc.exitCode !== null) {
297
+ return;
298
+ }
299
+ const drainStopMs = Date.now();
300
+ const drainStartMs = activeDrainStartMs;
301
+ const drainStartIso = activeDrainStartIso;
302
+ const drainWriteSequence = activeDrainWriteSequence;
303
+ activeDrainStartMs = null;
304
+ activeDrainStartIso = null;
305
+ activeDrainWriteSequence = null;
306
+ waitingForStdinDrain = false;
307
+ flushingPendingStdinWrites = false;
308
+ if (drainStartMs !== null && drainStartIso) {
309
+ const drainStopIso = new Date(drainStopMs).toISOString();
310
+ const durationMs = drainStopMs - drainStartMs;
311
+ void enqueueCommand("log", {
312
+ log: `stdin drain completed | start=${drainStartIso} | stop=${drainStopIso} | durationMs=${durationMs} | blockedWriteSequence=${drainWriteSequence ?? "unknown"}`,
313
+ level: "info",
314
+ echo: false
315
+ }, { front: true, suppressSendLog: true }).catch((error) => {
316
+ if (proc?.exitCode === 0 || exiting) {
317
+ return;
318
+ }
319
+ console.error("Failed to enqueue stdin drain log", error.stack, error.message);
320
+ });
321
+ return;
322
+ }
323
+ flushPendingStdinWrites();
324
+ });
325
+ return;
326
+ }
327
+ }
328
+ }
329
+ catch (err) {
330
+ const error = err instanceof Error ? err : new Error(String(err));
331
+ resetPendingStdinWrites();
332
+ flushPendingCommands(error);
333
+ return;
334
+ }
335
+ finally {
336
+ if (!waitingForStdinDrain) {
337
+ flushingPendingStdinWrites = false;
338
+ }
339
+ }
340
+ }
341
+ function enqueueStdinWrite(write, front = false) {
342
+ if (front) {
343
+ pendingStdinWrites.unshift(write);
344
+ }
345
+ else {
346
+ pendingStdinWrites.push(write);
347
+ }
348
+ flushPendingStdinWrites();
349
+ }
350
+ function enqueueCommand(command, params = {}, options = {}) {
351
+ return new Promise((resolve, reject) => {
352
+ const currentProc = proc;
353
+ const stdin = currentProc?.stdin;
354
+ if (!currentProc ||
355
+ currentProc.exitCode !== null ||
356
+ currentProc.killed ||
357
+ !stdin ||
358
+ stdin.destroyed ||
359
+ stdin.writableEnded ||
360
+ !stdin.writable) {
361
+ reject(new Error("The Desktop SDK is not started or is no longer accepting commands; call `shutdown` and `init` to start it again."));
362
+ return;
363
+ }
364
+ const commandId = (0, uuid_1.v4)();
365
+ const writeSequence = nextWriteSequence++;
366
+ pendingCommands[commandId] = { resolve, reject, command, writeSequence };
367
+ const payload = {
368
+ command,
369
+ commandId,
370
+ writeSequence,
371
+ params
372
+ };
373
+ const payloadStr = JSON.stringify(payload);
374
+ enqueueStdinWrite({ command, commandId, data: payloadStr + "\n", writeSequence }, options.front ?? false);
375
+ if (!options.suppressSendLog && command !== "log") {
376
+ doLog("info", [`Sending command [${writeSequence}]: ` + payloadStr], false);
377
+ }
378
+ });
379
+ }
216
380
  function startProcess() {
217
381
  if (proc && proc.exitCode === null) {
218
382
  logError("Desktop SDK: Trying to start process while it is already started");
@@ -227,6 +391,12 @@ function startProcess() {
227
391
  return;
228
392
  }
229
393
  let envExtra = {};
394
+ // forward all env starting with RECALL_
395
+ Object.keys(process.env).forEach(key => {
396
+ if (key.startsWith("RECALL_")) {
397
+ envExtra[key] = process.env[key];
398
+ }
399
+ });
230
400
  if (process.platform === "win32" && process.env.GLOBAL_GST_RECALL !== "1") {
231
401
  envExtra["GST_PLUGIN_PATH"] = path.join(path.dirname(exe_path), "gstreamer-1.0");
232
402
  }
@@ -244,6 +414,18 @@ function startProcess() {
244
414
  if (sdk_version) {
245
415
  envExtra["SDK_VERSION"] = sdk_version;
246
416
  }
417
+ if (lastOptions?.testMode) {
418
+ envExtra["TEST_MODE"] = "1";
419
+ }
420
+ if (lastOptions?.testTargetBundleId) {
421
+ envExtra["TEST_TARGET_BUNDLE_ID"] = lastOptions.testTargetBundleId;
422
+ }
423
+ if (lastOptions?.testTargetBundleIdRemapped) {
424
+ envExtra["TEST_TARGET_BUNDLE_ID_REMAP"] = lastOptions.testTargetBundleIdRemapped;
425
+ }
426
+ if (lastOptions?.testSpeedModifier) {
427
+ envExtra["TEST_SPEED_MODIFIER"] = String(lastOptions.testSpeedModifier);
428
+ }
247
429
  const gst_dump_dir = process.env.RECALLAI_DESKTOP_SDK_DEV ? path.join(os.tmpdir(), "gst.nocommit") : os.tmpdir();
248
430
  if (!fs.existsSync(gst_dump_dir)) {
249
431
  try {
@@ -257,7 +439,7 @@ function startProcess() {
257
439
  stdio: (process.platform === "darwin" ? ["pipe", "pipe", "pipe", "pipe"] : "pipe"),
258
440
  env: {
259
441
  GST_RECALL_DEBUG: "2,transcriber:4,rtewebsocketsink:4,rtewebhooksink:4,audiomixer:4,galleryview:4,dsdks3sink:5",
260
- GST_DEBUG: "3,GST_CAPS:5,GST_SCHEDULING:5,GST_PADS:5,video-scaler:1,transcriber:4,filebuffereds3sink:4,seekables3sink:4,rtpsession:1,videodecoder:2,basesink:2,webrtcbin:2,websocketsink:4,audiomixer:4,galleryview:4,removeonsinkeosbin:4,fallbackswitch:6,sendmessageonsinkeosbin:4,dsdks3sink:5",
442
+ GST_DEBUG: "3,GST_CAPS:5,GST_SCHEDULING:5,GST_PADS:5,video-scaler:1,transcriber:4,filebuffereds3sink:4,seekables3sink:4,rtpsession:1,audioresample:1,videodecoder:2,basesink:2,webrtcbin:2,websocketsink:4,audiomixer:4,galleryview:4,removeonsinkeosbin:4,fallbackswitch:6,sendmessageonsinkeosbin:4,dsdks3sink:5",
261
443
  GST_DEBUG_DUMP_DOT_DIR: gst_dump_dir,
262
444
  RUST_BACKTRACE: "1",
263
445
  // "DYLD_INSERT_LIBRARIES":"/opt/homebrew/lib/libjemalloc.dylib",
@@ -268,6 +450,10 @@ function startProcess() {
268
450
  ...envExtra,
269
451
  }
270
452
  });
453
+ const currentProc = proc;
454
+ stdinWriteSession++;
455
+ flushingPendingStdinWrites = false;
456
+ waitingForStdinDrain = false;
271
457
  if (process.platform === "darwin") {
272
458
  const fd3 = proc.stdio[3];
273
459
  if (fd3 && fd3 instanceof node_stream_1.Readable) {
@@ -288,15 +474,13 @@ function startProcess() {
288
474
  }
289
475
  break;
290
476
  case "response":
291
- const pendingCommand = pendingCommands[data.commandId];
292
- if (pendingCommand) {
477
+ if (pendingCommands[data.commandId]) {
293
478
  if (data.status === "success") {
294
- pendingCommand.resolve(data.result);
479
+ resolvePendingCommand(data.commandId, data.result);
295
480
  }
296
481
  else {
297
- pendingCommand.reject(new Error(data.result));
482
+ rejectPendingCommand(data.commandId, new Error(data.result));
298
483
  }
299
- delete pendingCommands[data.commandId];
300
484
  }
301
485
  break;
302
486
  }
@@ -320,7 +504,18 @@ function startProcess() {
320
504
  }
321
505
  });
322
506
  proc.on('error', (error) => {
507
+ if (proc !== currentProc) {
508
+ return;
509
+ }
510
+ proc = null;
511
+ resetPendingStdinWrites();
323
512
  flushPendingCommands(new Error(`Process error: ${error.message}`));
513
+ rlData?.close();
514
+ rlStdout?.close();
515
+ rlStderr?.close();
516
+ rlData = null;
517
+ rlStdout = null;
518
+ rlStderr = null;
324
519
  emitEvent('error', {
325
520
  type: 'process',
326
521
  message: `The Desktop SDK server process has failed to start or exited improperly.`
@@ -328,10 +523,17 @@ function startProcess() {
328
523
  logError(`Desktop SDK: Process error: ${error.message}`);
329
524
  });
330
525
  proc.on('close', async (code, signal) => {
526
+ if (proc !== currentProc) {
527
+ return;
528
+ }
529
+ proc = null;
530
+ resetPendingStdinWrites();
331
531
  flushPendingCommands(new Error(`Process exited with code ${code}, signal ${signal}.`));
332
532
  emitEvent('shutdown', { code: code ?? 0, signal: signal ?? '' });
533
+ rlData?.close();
333
534
  rlStdout?.close();
334
535
  rlStderr?.close();
536
+ rlData = null;
335
537
  rlStdout = null;
336
538
  rlStderr = null;
337
539
  if (code === 0 || signal === 'SIGINT' || exiting) {
@@ -342,7 +544,6 @@ function startProcess() {
342
544
  type: 'process',
343
545
  message: "The Desktop SDK server process exited unexpectedly."
344
546
  });
345
- proc = null;
346
547
  unexpectedShutdown = true;
347
548
  if (lastOptions.restartOnError && remainingAutomaticRestarts > 0) {
348
549
  remainingAutomaticRestarts--;
@@ -354,24 +555,7 @@ function startProcess() {
354
555
  });
355
556
  }
356
557
  function sendCommand(command, params = {}) {
357
- return new Promise((resolve, reject) => {
358
- if (!proc || !proc.stdin) {
359
- reject(new Error("The Desktop SDK is not started; call `init` to start it."));
360
- return;
361
- }
362
- const commandId = (0, uuid_1.v4)();
363
- pendingCommands[commandId] = { resolve, reject };
364
- const payload = {
365
- command,
366
- commandId,
367
- params
368
- };
369
- const payloadStr = JSON.stringify(payload);
370
- proc.stdin.write(payloadStr + "\n");
371
- if (command !== "log") {
372
- doLog("info", ["Sending command: " + payloadStr], false);
373
- }
374
- });
558
+ return enqueueCommand(command, params);
375
559
  }
376
560
  async function doInit(options) {
377
561
  startProcess();
@@ -404,11 +588,19 @@ async function shutdown() {
404
588
  const result = await sendCommand("shutdown");
405
589
  if (proc) {
406
590
  const currentProc = proc;
407
- setTimeout(() => {
408
- if (!currentProc.killed) {
409
- currentProc.kill();
591
+ await new Promise((resolve) => {
592
+ if (currentProc.exitCode !== null) {
593
+ resolve();
594
+ return;
410
595
  }
411
- }, 5000);
596
+ currentProc.once("close", () => resolve());
597
+ setTimeout(() => {
598
+ if (!currentProc.killed) {
599
+ currentProc.kill();
600
+ }
601
+ resolve();
602
+ }, 5000);
603
+ });
412
604
  }
413
605
  return result;
414
606
  }
@@ -431,7 +623,7 @@ function resumeRecording({ windowId }) {
431
623
  return sendCommand("resumeRecording", { windowId });
432
624
  }
433
625
  /**
434
- * @deprecated Recordings are uploaded based on the upload token config.
626
+ * @deprecated Recordings are automatically uploaded based on your rentention configuration. This is now a no-op.
435
627
  */
436
628
  function uploadRecording({ windowId }) {
437
629
  return sendCommand("uploadRecording", { windowId });
@@ -448,6 +640,15 @@ function testUnexpectedShutdown() {
448
640
  function addEventListener(type, callback) {
449
641
  listeners.push({ type, callback });
450
642
  }
643
+ function removeEventListener(type, callback) {
644
+ const index = listeners.findIndex(l => l.type === type && l.callback === callback);
645
+ if (index !== -1) {
646
+ listeners.splice(index, 1);
647
+ }
648
+ }
649
+ function removeAllEventListeners() {
650
+ listeners.length = 0;
651
+ }
451
652
  const RecallAiSdk = {
452
653
  init,
453
654
  shutdown,
@@ -459,6 +660,8 @@ const RecallAiSdk = {
459
660
  prepareDesktopAudioRecording,
460
661
  requestPermission,
461
662
  addEventListener,
663
+ removeEventListener,
664
+ removeAllEventListeners,
462
665
  testUnexpectedShutdown
463
666
  };
464
667
  exports.default = RecallAiSdk;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recallai/desktop-sdk",
3
- "version": "2.0.7",
3
+ "version": "2.0.9",
4
4
  "description": "Recall Desktop SDK",
5
5
  "main": "./index.js",
6
6
  "types": "./index.d.ts",
@@ -20,5 +20,5 @@
20
20
  "@types/node": "^24.2.0",
21
21
  "typescript": "^5.3.3"
22
22
  },
23
- "commit_sha": "541a0e106905afe34c005d84802157d1251cc35a"
23
+ "commit_sha": "2bc571dbad7ed8b15a925c9a4cdd0e7fab3c6955"
24
24
  }