neoagent 3.1.1-beta.9 → 3.2.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.
Files changed (150) hide show
  1. package/.env.example +2 -2
  2. package/README.md +5 -1
  3. package/docs/architecture.md +1 -1
  4. package/docs/benchmarking.md +68 -87
  5. package/docs/capabilities.md +1 -1
  6. package/docs/clients-and-devices.md +3 -4
  7. package/docs/configuration.md +1 -1
  8. package/docs/devices.md +1 -2
  9. package/docs/health.md +24 -0
  10. package/docs/index.md +2 -2
  11. package/docs/integrations-architecture.md +6 -0
  12. package/docs/integrations.md +15 -1
  13. package/docs/models.md +1 -1
  14. package/docs/persistence.md +1 -1
  15. package/docs/why-neoagent.md +1 -1
  16. package/flutter_app/README.md +0 -5
  17. package/flutter_app/android/app/src/main/AndroidManifest.xml +0 -14
  18. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +0 -98
  19. package/flutter_app/lib/main.dart +0 -6
  20. package/flutter_app/lib/main_app_shell.dart +0 -2
  21. package/flutter_app/lib/main_controller.dart +2 -867
  22. package/flutter_app/lib/main_devices.dart +0 -73
  23. package/flutter_app/lib/main_integrations.dart +339 -187
  24. package/flutter_app/lib/main_launcher.dart +2 -212
  25. package/flutter_app/lib/main_models.dart +0 -259
  26. package/flutter_app/lib/main_navigation.dart +2 -14
  27. package/flutter_app/lib/main_operations.dart +322 -364
  28. package/flutter_app/lib/main_runtime.dart +9 -208
  29. package/flutter_app/lib/main_settings.dart +17 -199
  30. package/flutter_app/lib/main_shared.dart +20 -591
  31. package/flutter_app/lib/main_timeline.dart +26 -122
  32. package/flutter_app/lib/main_voice_assistant.dart +0 -5
  33. package/flutter_app/lib/src/backend_client.dart +0 -102
  34. package/flutter_app/lib/src/desktop_companion_io.dart +1 -65
  35. package/flutter_app/lib/src/desktop_companion_stub.dart +0 -12
  36. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +0 -4
  37. package/flutter_app/linux/flutter/generated_plugins.cmake +0 -1
  38. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +0 -2
  39. package/flutter_app/macos/Podfile.lock +56 -5
  40. package/flutter_app/pubspec.lock +0 -7
  41. package/flutter_app/pubspec.yaml +0 -2
  42. package/flutter_app/web/index.html +174 -0
  43. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +0 -3
  44. package/flutter_app/windows/flutter/generated_plugins.cmake +0 -1
  45. package/landing/index.html +0 -2
  46. package/lib/manager.js +1 -1
  47. package/lib/schema_migrations.js +75 -0
  48. package/package.json +1 -4
  49. package/server/db/database.js +1 -133
  50. package/server/http/middleware.js +2 -16
  51. package/server/http/routes.js +0 -2
  52. package/server/public/.last_build_id +1 -1
  53. package/server/public/assets/NOTICES +0 -25
  54. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  55. package/server/public/flutter_bootstrap.js +1 -1
  56. package/server/public/index.html +174 -0
  57. package/server/public/main.dart.js +88081 -90036
  58. package/server/routes/auth.js +11 -2
  59. package/server/routes/settings.js +0 -5
  60. package/server/routes/voice_assistant.js +1 -128
  61. package/server/services/account/erasure.js +0 -3
  62. package/server/services/ai/deliverables/artifact_helpers.js +1 -1
  63. package/server/services/ai/loop/conversation_loop.js +6 -2
  64. package/server/services/ai/loop/tool_dispatch.js +0 -3
  65. package/server/services/ai/messagingFallback.js +1 -1
  66. package/server/services/ai/settings.js +0 -6
  67. package/server/services/ai/toolEvidence.js +1 -4
  68. package/server/services/ai/tools.js +0 -234
  69. package/server/services/desktop/auth.js +0 -3
  70. package/server/services/desktop/registry.js +2 -50
  71. package/server/services/integrations/neoarchive/provider.js +616 -0
  72. package/server/services/integrations/neorecall/client.js +140 -0
  73. package/server/services/integrations/neorecall/constants.js +87 -0
  74. package/server/services/integrations/neorecall/provider.js +163 -0
  75. package/server/services/integrations/registry.js +4 -2
  76. package/server/services/manager.js +0 -22
  77. package/server/services/memory/manager.js +44 -19
  78. package/server/services/memory/retrieval_reasoning.js +36 -14
  79. package/server/services/messaging/whatsapp.js +18 -0
  80. package/server/services/security/tool_categories.js +0 -5
  81. package/server/services/social_video/service.js +2 -20
  82. package/server/services/tasks/adapters/index.js +1 -1
  83. package/server/services/tasks/integration_runtime.js +3 -67
  84. package/server/services/tasks/runtime.js +1 -6
  85. package/server/services/tasks/task_repository.js +6 -13
  86. package/server/services/timeline/service.js +0 -224
  87. package/server/services/voice/message.js +0 -24
  88. package/server/services/voice/providers.js +42 -1
  89. package/server/services/voice/runtimeManager.js +9 -5
  90. package/server/services/voice/turnRunner.js +0 -22
  91. package/server/services/wearable/service.js +0 -5
  92. package/docs/recordings-and-health.md +0 -41
  93. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +0 -21
  94. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +0 -586
  95. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +0 -78
  96. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +0 -104
  97. package/flutter_app/lib/main_recordings.dart +0 -920
  98. package/flutter_app/lib/src/desktop_ocr_bridge.dart +0 -2
  99. package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +0 -125
  100. package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +0 -30
  101. package/flutter_app/lib/src/desktop_passive_history.dart +0 -332
  102. package/flutter_app/lib/src/recording_bridge.dart +0 -232
  103. package/flutter_app/lib/src/recording_bridge_io.dart +0 -1027
  104. package/flutter_app/lib/src/recording_bridge_stub.dart +0 -120
  105. package/flutter_app/lib/src/recording_bridge_web.dart +0 -702
  106. package/flutter_app/lib/src/recording_chunk_queue.dart +0 -149
  107. package/flutter_app/lib/src/recording_chunk_queue_io.dart +0 -182
  108. package/flutter_app/lib/src/recording_payloads.dart +0 -95
  109. package/flutter_app/third_party/desktop_audio_capture/LICENSE +0 -21
  110. package/flutter_app/third_party/desktop_audio_capture/README.md +0 -262
  111. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +0 -65
  112. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +0 -153
  113. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +0 -110
  114. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +0 -461
  115. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +0 -91
  116. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +0 -106
  117. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +0 -219
  118. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +0 -336
  119. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +0 -101
  120. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +0 -692
  121. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +0 -35
  122. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +0 -36
  123. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +0 -32
  124. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +0 -32
  125. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +0 -878
  126. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +0 -27
  127. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +0 -1172
  128. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +0 -655
  129. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +0 -12
  130. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +0 -30
  131. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +0 -87
  132. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +0 -105
  133. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +0 -80
  134. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +0 -31
  135. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +0 -12
  136. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +0 -23
  137. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +0 -25
  138. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +0 -1117
  139. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +0 -115
  140. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +0 -777
  141. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +0 -87
  142. package/server/routes/recordings.js +0 -335
  143. package/server/routes/screenHistory.js +0 -190
  144. package/server/services/ai/recordingInsights.js +0 -105
  145. package/server/services/integrations/neomail/provider.js +0 -993
  146. package/server/services/recordings/deepgram.js +0 -58
  147. package/server/services/recordings/manager.js +0 -1273
  148. package/server/services/tasks/adapters/neomail_email_received.js +0 -46
  149. package/server/services/voice/screenshotContext.js +0 -73
  150. package/server/services/voice/turnCoordinator.js +0 -51
@@ -1,702 +0,0 @@
1
- // ignore_for_file: avoid_web_libraries_in_flutter, deprecated_member_use
2
-
3
- import 'dart:async';
4
- import 'dart:html' as html;
5
- import 'dart:js_util' as js_util;
6
- import 'dart:typed_data';
7
-
8
- import 'diagnostics_logger.dart';
9
- import 'recording_bridge.dart';
10
- import 'recording_chunk_queue.dart';
11
-
12
- RecordingBridge createPlatformRecordingBridge() => WebRecordingBridge();
13
-
14
- class WebRecordingBridge extends RecordingBridge {
15
- RecordingRuntimeStatus _status = const RecordingRuntimeStatus(
16
- supportsScreenAndMic: true,
17
- supportsBackgroundMic: false,
18
- platformLabel: 'Browser screen + microphone recorder',
19
- );
20
-
21
- html.MediaStream? _displayStream;
22
- html.MediaStream? _systemAudioStream;
23
- html.MediaStream? _microphoneStream;
24
- html.MediaRecorder? _screenRecorder;
25
- html.MediaRecorder? _systemAudioRecorder;
26
- html.MediaRecorder? _microphoneRecorder;
27
- final Map<String, int> _nextSequenceBySource = <String, int>{};
28
- final Map<String, int> _lastEndMsBySource = <String, int>{};
29
- // One durable, retry-until-success queue per source so a failed upload is
30
- // re-sent instead of silently dropped, and a stall in one source does not
31
- // block the others.
32
- final Map<String, MemoryChunkUploadQueue> _uploadQueues =
33
- <String, MemoryChunkUploadQueue>{};
34
- Stopwatch? _stopwatch;
35
- String? _baseUrl;
36
- String? _sessionId;
37
- bool _stopping = false;
38
- StreamSubscription<html.Event>? _displayEndedSub;
39
- static const Set<String> _safeUploadHeaderKeys = <String>{
40
- 'content-type',
41
- 'x-recording-sequence',
42
- 'x-recording-start-ms',
43
- 'x-recording-end-ms',
44
- };
45
-
46
- @override
47
- RecordingRuntimeStatus get status => _status;
48
-
49
- void _log(
50
- String event, {
51
- Map<String, Object?> data = const <String, Object?>{},
52
- Object? error,
53
- StackTrace? stackTrace,
54
- }) {
55
- AppDiagnostics.log(
56
- 'recording.bridge.web',
57
- event,
58
- data: data,
59
- error: error,
60
- stackTrace: stackTrace,
61
- );
62
- }
63
-
64
- @override
65
- Future<void> refreshStatus() async {}
66
-
67
- @override
68
- Future<void> startWebRecording({
69
- required String baseUrl,
70
- required String sessionId,
71
- }) async {
72
- _log(
73
- 'start_web.request',
74
- data: <String, Object?>{
75
- 'sessionId': sessionId,
76
- 'baseUrl': baseUrl,
77
- 'alreadyActive': _status.active,
78
- },
79
- );
80
- if (_status.active) {
81
- throw const RecordingBridgeException(
82
- 'A recording is already in progress.',
83
- );
84
- }
85
-
86
- try {
87
- final mediaDevices = html.window.navigator.mediaDevices;
88
- if (mediaDevices == null) {
89
- throw const RecordingBridgeException(
90
- 'This browser does not expose media device APIs.',
91
- );
92
- }
93
-
94
- final displayStream = await js_util.promiseToFuture<html.MediaStream>(
95
- js_util.callMethod(mediaDevices, 'getDisplayMedia', <Object?>[
96
- js_util.jsify(<String, Object>{'video': true, 'audio': true}),
97
- ]),
98
- );
99
- if (displayStream.getVideoTracks().isEmpty) {
100
- throw const RecordingBridgeException(
101
- 'Screen sharing was cancelled before capture started.',
102
- );
103
- }
104
- if (displayStream.getAudioTracks().isEmpty) {
105
- displayStream.getTracks().forEach((track) => track.stop());
106
- throw const RecordingBridgeException(
107
- 'Screen share did not include audio. Share a tab/window and enable audio.',
108
- );
109
- }
110
-
111
- final microphoneStream = await mediaDevices.getUserMedia(
112
- <String, dynamic>{
113
- 'audio': <String, dynamic>{
114
- 'channelCount': 1,
115
- 'echoCancellation': false,
116
- 'noiseSuppression': false,
117
- 'autoGainControl': false,
118
- },
119
- },
120
- );
121
- if (microphoneStream.getAudioTracks().isEmpty) {
122
- displayStream.getTracks().forEach((track) => track.stop());
123
- throw const RecordingBridgeException(
124
- 'Microphone permission is required to start recording.',
125
- );
126
- }
127
-
128
- // Split the display capture: video-only feeds the screen-analysis source,
129
- // while the shared system/tab audio gets its own transcribable source so
130
- // remote participants are captured in the transcript (the mic alone does
131
- // not reliably pick up other speakers, e.g. when wearing headphones).
132
- final screenVideoStream = html.MediaStream();
133
- for (final track in displayStream.getVideoTracks()) {
134
- screenVideoStream.addTrack(track);
135
- }
136
- final systemAudioStream = html.MediaStream();
137
- for (final track in displayStream.getAudioTracks()) {
138
- systemAudioStream.addTrack(track);
139
- }
140
-
141
- _baseUrl = baseUrl;
142
- _sessionId = sessionId;
143
- _displayStream = displayStream;
144
- _systemAudioStream = systemAudioStream;
145
- _microphoneStream = microphoneStream;
146
- _nextSequenceBySource
147
- ..clear()
148
- ..addAll(<String, int>{'screen': 0, 'system': 0, 'microphone': 0});
149
- _lastEndMsBySource
150
- ..clear()
151
- ..addAll(<String, int>{'screen': 0, 'system': 0, 'microphone': 0});
152
- _initUploadQueues(<String>['screen', 'system', 'microphone']);
153
- _stopwatch = Stopwatch()..start();
154
-
155
- final screenMimeType = _pickMimeType(<String>[
156
- 'video/webm;codecs=vp9,opus',
157
- 'video/webm;codecs=vp8,opus',
158
- 'video/webm',
159
- ]);
160
- final audioMimeType = _pickMimeType(<String>[
161
- 'audio/webm;codecs=opus',
162
- 'audio/webm',
163
- ]);
164
-
165
- _screenRecorder = html.MediaRecorder(
166
- screenVideoStream,
167
- screenMimeType == null
168
- ? null
169
- : <String, String>{'mimeType': screenMimeType},
170
- );
171
-
172
- _displayEndedSub = displayStream.getVideoTracks().first.onEnded.listen((
173
- _,
174
- ) {
175
- unawaited(_handleExternalStop());
176
- });
177
-
178
- _bindRecorder(
179
- recorder: _screenRecorder!,
180
- sourceKey: 'screen',
181
- mimeType: screenMimeType ?? 'video/webm',
182
- );
183
- _systemAudioRecorder = html.MediaRecorder(
184
- systemAudioStream,
185
- audioMimeType == null ? null : <String, String>{'mimeType': audioMimeType},
186
- );
187
- _bindRecorder(
188
- recorder: _systemAudioRecorder!,
189
- sourceKey: 'system',
190
- mimeType: audioMimeType ?? 'audio/webm',
191
- );
192
- _microphoneRecorder = html.MediaRecorder(
193
- microphoneStream,
194
- audioMimeType == null ? null : <String, String>{'mimeType': audioMimeType},
195
- );
196
- _bindRecorder(
197
- recorder: _microphoneRecorder!,
198
- sourceKey: 'microphone',
199
- mimeType: audioMimeType ?? 'audio/webm',
200
- );
201
-
202
- _screenRecorder!.start(4000);
203
- _systemAudioRecorder!.start(4000);
204
- _microphoneRecorder!.start(4000);
205
- _status = _status.copyWith(
206
- active: true,
207
- paused: false,
208
- sessionId: sessionId,
209
- startedAt: DateTime.now(),
210
- errorMessage: null,
211
- );
212
- _log(
213
- 'start_web.done',
214
- data: <String, Object?>{
215
- 'sessionId': sessionId,
216
- 'screenMimeType': screenMimeType ?? 'video/webm',
217
- 'systemMimeType': audioMimeType ?? 'audio/webm',
218
- 'micMimeType': audioMimeType ?? 'audio/webm',
219
- },
220
- );
221
- notifyListeners();
222
- } catch (error) {
223
- await _disposeStreams();
224
- _status = _status.copyWith(
225
- active: false,
226
- paused: false,
227
- sessionId: null,
228
- startedAt: null,
229
- errorMessage: error.toString(),
230
- );
231
- _log('start_web.failed', error: error);
232
- notifyListeners();
233
- rethrow;
234
- }
235
- }
236
-
237
- @override
238
- Future<void> startWebMicrophoneRecording({
239
- required String baseUrl,
240
- required String sessionId,
241
- }) async {
242
- _log(
243
- 'start_web_mic_only.request',
244
- data: <String, Object?>{
245
- 'sessionId': sessionId,
246
- 'baseUrl': baseUrl,
247
- 'alreadyActive': _status.active,
248
- },
249
- );
250
- if (_status.active) {
251
- throw const RecordingBridgeException(
252
- 'A recording is already in progress.',
253
- );
254
- }
255
-
256
- try {
257
- final mediaDevices = html.window.navigator.mediaDevices;
258
- if (mediaDevices == null) {
259
- throw const RecordingBridgeException(
260
- 'This browser does not expose media device APIs.',
261
- );
262
- }
263
-
264
- final microphoneStream = await mediaDevices.getUserMedia(
265
- <String, dynamic>{
266
- 'audio': <String, dynamic>{
267
- 'channelCount': 1,
268
- 'echoCancellation': false,
269
- 'noiseSuppression': false,
270
- 'autoGainControl': false,
271
- },
272
- },
273
- );
274
- if (microphoneStream.getAudioTracks().isEmpty) {
275
- throw const RecordingBridgeException(
276
- 'Microphone permission is required to start recording.',
277
- );
278
- }
279
-
280
- _baseUrl = baseUrl;
281
- _sessionId = sessionId;
282
- _displayStream = null;
283
- _microphoneStream = microphoneStream;
284
- _nextSequenceBySource
285
- ..clear()
286
- ..addAll(<String, int>{'microphone': 0});
287
- _lastEndMsBySource
288
- ..clear()
289
- ..addAll(<String, int>{'microphone': 0});
290
- _initUploadQueues(<String>['microphone']);
291
- _stopwatch = Stopwatch()..start();
292
-
293
- final micMimeType = _pickMimeType(<String>[
294
- 'audio/webm;codecs=opus',
295
- 'audio/webm',
296
- ]);
297
-
298
- _displayEndedSub = null;
299
- _screenRecorder = null;
300
- _microphoneRecorder = html.MediaRecorder(
301
- microphoneStream,
302
- micMimeType == null ? null : <String, String>{'mimeType': micMimeType},
303
- );
304
- _bindRecorder(
305
- recorder: _microphoneRecorder!,
306
- sourceKey: 'microphone',
307
- mimeType: micMimeType ?? 'audio/webm',
308
- );
309
-
310
- _microphoneRecorder!.start(4000);
311
- _status = _status.copyWith(
312
- active: true,
313
- paused: false,
314
- sessionId: sessionId,
315
- startedAt: DateTime.now(),
316
- errorMessage: null,
317
- );
318
- _log(
319
- 'start_web_mic_only.done',
320
- data: <String, Object?>{
321
- 'sessionId': sessionId,
322
- 'micMimeType': micMimeType ?? 'audio/webm',
323
- },
324
- );
325
- notifyListeners();
326
- } catch (error) {
327
- await _disposeStreams();
328
- _status = _status.copyWith(
329
- active: false,
330
- paused: false,
331
- sessionId: null,
332
- startedAt: null,
333
- errorMessage: error.toString(),
334
- );
335
- _log('start_web_mic_only.failed', error: error);
336
- notifyListeners();
337
- rethrow;
338
- }
339
- }
340
-
341
- @override
342
- Future<void> startBackgroundRecording({
343
- required String baseUrl,
344
- required String sessionCookie,
345
- required String sessionId,
346
- }) async {
347
- throw const RecordingBridgeException(
348
- 'Background microphone recording is available on Android only.',
349
- );
350
- }
351
-
352
- @override
353
- Future<void> startDesktopAudioRecording({
354
- required String baseUrl,
355
- required String sessionCookie,
356
- required String sessionId,
357
- bool autoShowToolbar = true,
358
- }) async {
359
- throw const RecordingBridgeException(
360
- 'Native desktop audio recording is not available in the browser.',
361
- );
362
- }
363
-
364
- @override
365
- Future<void> pauseBackgroundRecording() async {
366
- throw const RecordingBridgeException(
367
- 'Pause is only available for Android background recording.',
368
- );
369
- }
370
-
371
- @override
372
- Future<void> pauseDesktopRecording() async {
373
- throw const RecordingBridgeException(
374
- 'Desktop recording controls are not available in the browser.',
375
- );
376
- }
377
-
378
- @override
379
- Future<void> resumeBackgroundRecording() async {
380
- throw const RecordingBridgeException(
381
- 'Resume is only available for Android background recording.',
382
- );
383
- }
384
-
385
- @override
386
- Future<void> resumeDesktopRecording() async {
387
- throw const RecordingBridgeException(
388
- 'Desktop recording controls are not available in the browser.',
389
- );
390
- }
391
-
392
- @override
393
- Future<void> showFloatingToolbar() async {}
394
-
395
- @override
396
- Future<void> hideFloatingToolbar() async {}
397
-
398
- @override
399
- Future<void> openMicrophoneSettings() async {
400
- throw const RecordingBridgeException(
401
- 'Open the browser site settings to manage microphone access.',
402
- );
403
- }
404
-
405
- @override
406
- Future<void> openSystemAudioSettings() async {
407
- throw const RecordingBridgeException(
408
- 'Browser screen-share audio permissions are controlled by the browser.',
409
- );
410
- }
411
-
412
- @override
413
- Future<void> stopActiveRecording({bool notifyEnded = false}) async {
414
- if (_stopping || !_status.active) {
415
- return;
416
- }
417
- _stopping = true;
418
- final sessionId = _sessionId;
419
- _log(
420
- 'stop_active.request',
421
- data: <String, Object?>{
422
- 'sessionId': sessionId,
423
- 'notifyEnded': notifyEnded,
424
- 'active': _status.active,
425
- },
426
- );
427
- try {
428
- await _stopRecorders();
429
- await Future.wait(
430
- _uploadQueues.values.map((queue) => queue.flush()),
431
- );
432
- await _disposeStreams();
433
- _status = _status.copyWith(
434
- active: false,
435
- paused: false,
436
- startedAt: null,
437
- sessionId: sessionId,
438
- );
439
- _log(
440
- 'stop_active.done',
441
- data: <String, Object?>{
442
- 'sessionId': sessionId,
443
- 'notifyEnded': notifyEnded,
444
- },
445
- );
446
- notifyListeners();
447
- if (notifyEnded && sessionId != null && onRecordingStopped != null) {
448
- _log(
449
- 'stop_active.notify_ended',
450
- data: <String, Object?>{'sessionId': sessionId},
451
- );
452
- await onRecordingStopped!(sessionId);
453
- }
454
- } finally {
455
- _stopping = false;
456
- }
457
- }
458
-
459
- void _initUploadQueues(List<String> sourceKeys) {
460
- for (final queue in _uploadQueues.values) {
461
- unawaited(queue.dispose());
462
- }
463
- _uploadQueues
464
- ..clear()
465
- ..addEntries(
466
- sourceKeys.map(
467
- (sourceKey) => MapEntry<String, MemoryChunkUploadQueue>(
468
- sourceKey,
469
- MemoryChunkUploadQueue(
470
- uploader: _performChunkUpload,
471
- logger:
472
- (
473
- event, {
474
- Map<String, Object?> data = const <String, Object?>{},
475
- Object? error,
476
- }) => _log(event, data: data, error: error),
477
- ),
478
- ),
479
- ),
480
- );
481
- }
482
-
483
- void _bindRecorder({
484
- required html.MediaRecorder recorder,
485
- required String sourceKey,
486
- required String mimeType,
487
- }) {
488
- recorder.addEventListener('dataavailable', (html.Event event) {
489
- final blob = js_util.getProperty<html.Blob?>(event, 'data');
490
- if (blob == null || blob.size == 0) {
491
- return;
492
- }
493
- final endMs = _stopwatch?.elapsedMilliseconds ?? 0;
494
- final startMs = _lastEndMsBySource[sourceKey] ?? 0;
495
- _lastEndMsBySource[sourceKey] = endMs;
496
- final sequence = _nextSequenceBySource[sourceKey] ?? 0;
497
- _nextSequenceBySource[sourceKey] = sequence + 1;
498
- final queue = _uploadQueues[sourceKey];
499
- if (queue == null) {
500
- return;
501
- }
502
- // Read the blob bytes, then hand the chunk to the durable queue, which
503
- // uploads and retries until the server confirms it (so a transient
504
- // failure no longer drops the chunk or stalls the rest of the source).
505
- unawaited(() async {
506
- try {
507
- final bytes = await _blobToBytes(blob);
508
- queue.enqueue(
509
- PendingChunk(
510
- sourceKey: sourceKey,
511
- sequence: sequence,
512
- startMs: startMs,
513
- endMs: endMs,
514
- mimeType: mimeType,
515
- bytes: bytes,
516
- ),
517
- );
518
- } catch (error, stackTrace) {
519
- _log(
520
- 'chunk.read.failed',
521
- data: <String, Object?>{
522
- 'sourceKey': sourceKey,
523
- 'sequence': sequence,
524
- },
525
- error: error,
526
- stackTrace: stackTrace,
527
- );
528
- }
529
- }());
530
- });
531
- }
532
-
533
- Future<void> _performChunkUpload(PendingChunk chunk) async {
534
- final sessionId = _sessionId;
535
- final baseUrl = _baseUrl;
536
- if (sessionId == null || baseUrl == null) {
537
- throw const RecordingBridgeException(
538
- 'Recording session is not initialized.',
539
- );
540
- }
541
- final uri = _resolveUri(baseUrl, '/api/recordings/$sessionId/chunks');
542
- final headers = <String, String>{
543
- 'Content-Type': chunk.mimeType,
544
- 'X-Recording-Source-Key': chunk.sourceKey,
545
- 'X-Recording-Sequence': '${chunk.sequence}',
546
- 'X-Recording-Start-Ms': '${chunk.startMs}',
547
- 'X-Recording-End-Ms': '${chunk.endMs}',
548
- };
549
- _log(
550
- 'chunk.upload.attempt',
551
- data: <String, Object?>{
552
- 'sessionId': sessionId,
553
- 'sourceKey': chunk.sourceKey,
554
- 'sequence': chunk.sequence,
555
- 'size': chunk.bytes.length,
556
- 'headers': _filterHeadersForLog(headers),
557
- },
558
- );
559
- await html.HttpRequest.request(
560
- uri.toString(),
561
- method: 'POST',
562
- sendData: chunk.bytes,
563
- requestHeaders: headers,
564
- withCredentials: true,
565
- );
566
- _log(
567
- 'chunk.upload.done',
568
- data: <String, Object?>{
569
- 'sessionId': sessionId,
570
- 'sourceKey': chunk.sourceKey,
571
- 'sequence': chunk.sequence,
572
- 'size': chunk.bytes.length,
573
- },
574
- );
575
- }
576
-
577
- Future<void> _handleExternalStop() async {
578
- final sessionId = _sessionId;
579
- if (sessionId == null) {
580
- return;
581
- }
582
- _log(
583
- 'external_stop.detected',
584
- data: <String, Object?>{'sessionId': sessionId},
585
- );
586
- await stopActiveRecording(notifyEnded: true);
587
- }
588
-
589
- Future<void> _stopRecorders() async {
590
- final futures = <Future<void>>[];
591
- if (_screenRecorder != null && _screenRecorder!.state != 'inactive') {
592
- futures.add(_waitForStop(_screenRecorder!));
593
- _screenRecorder!.stop();
594
- }
595
- if (_systemAudioRecorder != null &&
596
- _systemAudioRecorder!.state != 'inactive') {
597
- futures.add(_waitForStop(_systemAudioRecorder!));
598
- _systemAudioRecorder!.stop();
599
- }
600
- if (_microphoneRecorder != null &&
601
- _microphoneRecorder!.state != 'inactive') {
602
- futures.add(_waitForStop(_microphoneRecorder!));
603
- _microphoneRecorder!.stop();
604
- }
605
- await Future.wait(futures);
606
- }
607
-
608
- Future<void> _disposeStreams() async {
609
- await _displayEndedSub?.cancel();
610
- _displayEndedSub = null;
611
- _displayStream?.getTracks().forEach((track) => track.stop());
612
- _systemAudioStream?.getTracks().forEach((track) => track.stop());
613
- _microphoneStream?.getTracks().forEach((track) => track.stop());
614
- _displayStream = null;
615
- _systemAudioStream = null;
616
- _microphoneStream = null;
617
- _screenRecorder = null;
618
- _systemAudioRecorder = null;
619
- _microphoneRecorder = null;
620
- for (final queue in _uploadQueues.values) {
621
- await queue.dispose();
622
- }
623
- _uploadQueues.clear();
624
- _stopwatch?.stop();
625
- _stopwatch = null;
626
- }
627
-
628
- Map<String, String> _filterHeadersForLog(Map<String, String> headers) {
629
- final filtered = <String, String>{};
630
- headers.forEach((key, value) {
631
- if (_safeUploadHeaderKeys.contains(key.toLowerCase())) {
632
- filtered[key] = value;
633
- }
634
- });
635
- return filtered;
636
- }
637
-
638
- Future<Uint8List> _blobToBytes(html.Blob blob) {
639
- final completer = Completer<Uint8List>();
640
- final reader = html.FileReader();
641
- reader.onError.first.then((_) {
642
- if (!completer.isCompleted) {
643
- completer.completeError(
644
- const RecordingBridgeException(
645
- 'Could not read recorded browser chunk.',
646
- ),
647
- );
648
- }
649
- });
650
- reader.onLoadEnd.first.then((_) {
651
- if (completer.isCompleted) {
652
- return;
653
- }
654
- final result = reader.result;
655
- if (result is ByteBuffer) {
656
- completer.complete(result.asUint8List());
657
- return;
658
- }
659
- if (result is Uint8List) {
660
- completer.complete(result);
661
- return;
662
- }
663
- completer.completeError(
664
- const RecordingBridgeException(
665
- 'Browser returned an unsupported chunk format.',
666
- ),
667
- );
668
- });
669
- reader.readAsArrayBuffer(blob);
670
- return completer.future;
671
- }
672
-
673
- Future<void> _waitForStop(html.MediaRecorder recorder) {
674
- final completer = Completer<void>();
675
- late html.EventListener listener;
676
- listener = (html.Event _) {
677
- recorder.removeEventListener('stop', listener);
678
- if (!completer.isCompleted) {
679
- completer.complete();
680
- }
681
- };
682
- recorder.addEventListener('stop', listener);
683
- return completer.future;
684
- }
685
-
686
- String? _pickMimeType(List<String> candidates) {
687
- for (final candidate in candidates) {
688
- if (html.MediaRecorder.isTypeSupported(candidate)) {
689
- return candidate;
690
- }
691
- }
692
- return null;
693
- }
694
-
695
- Uri _resolveUri(String baseUrl, String path) {
696
- final trimmed = baseUrl.trim();
697
- if (trimmed.isEmpty) {
698
- return Uri.parse(path);
699
- }
700
- return Uri.parse(trimmed.replaceFirst(RegExp(r'/$'), '') + path);
701
- }
702
- }