neoagent 2.3.1-beta.2 → 2.3.1-beta.21

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 (264) hide show
  1. package/.env.example +39 -0
  2. package/README.md +2 -0
  3. package/docs/capabilities.md +2 -2
  4. package/docs/configuration.md +13 -5
  5. package/docs/integrations.md +4 -1
  6. package/flutter_app/.metadata +42 -0
  7. package/flutter_app/README.md +21 -0
  8. package/flutter_app/analysis_options.yaml +32 -0
  9. package/flutter_app/android/app/build.gradle.kts +109 -0
  10. package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
  11. package/flutter_app/android/app/src/main/AndroidManifest.xml +147 -0
  12. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +747 -0
  13. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
  14. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
  15. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
  16. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
  17. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
  18. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
  19. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
  20. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
  21. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
  22. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
  23. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
  24. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
  25. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
  26. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
  27. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
  28. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
  29. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
  30. package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
  31. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
  32. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
  33. package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  34. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
  35. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
  36. package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
  37. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  38. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  39. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  40. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  41. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  42. package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
  43. package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
  44. package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
  45. package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
  46. package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
  47. package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
  48. package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
  49. package/flutter_app/android/build.gradle.kts +24 -0
  50. package/flutter_app/android/ci-release.keystore +0 -0
  51. package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  52. package/flutter_app/android/gradle.properties +3 -0
  53. package/flutter_app/android/key.properties +4 -0
  54. package/flutter_app/android/settings.gradle.kts +26 -0
  55. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  56. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  57. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  58. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  59. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  60. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  61. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  62. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  63. package/flutter_app/lib/features/location/location_service.dart +119 -0
  64. package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
  65. package/flutter_app/lib/main.dart +23057 -0
  66. package/flutter_app/lib/main_app_shell.dart +1682 -0
  67. package/flutter_app/lib/main_integrations.dart +931 -0
  68. package/flutter_app/lib/main_launcher.dart +959 -0
  69. package/flutter_app/lib/main_launcher_entry.dart +5 -0
  70. package/flutter_app/lib/main_models.dart +3473 -0
  71. package/flutter_app/lib/main_shared.dart +2861 -0
  72. package/flutter_app/lib/main_theme.dart +204 -0
  73. package/flutter_app/lib/main_voice_assistant.dart +831 -0
  74. package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
  75. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
  76. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
  77. package/flutter_app/lib/src/android_app_installer.dart +22 -0
  78. package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
  79. package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
  80. package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
  81. package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
  82. package/flutter_app/lib/src/app_release_updater.dart +511 -0
  83. package/flutter_app/lib/src/backend_client.dart +1833 -0
  84. package/flutter_app/lib/src/desktop_companion.dart +2 -0
  85. package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
  86. package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
  87. package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
  88. package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
  89. package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
  90. package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
  91. package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
  92. package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
  93. package/flutter_app/lib/src/health_bridge.dart +136 -0
  94. package/flutter_app/lib/src/live_voice_capture.dart +85 -0
  95. package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
  96. package/flutter_app/lib/src/network/app_http_client.dart +53 -0
  97. package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
  98. package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
  99. package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
  100. package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
  101. package/flutter_app/lib/src/oauth_launcher.dart +33 -0
  102. package/flutter_app/lib/src/oauth_launcher_io.dart +77 -0
  103. package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
  104. package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
  105. package/flutter_app/lib/src/recording_bridge.dart +232 -0
  106. package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
  107. package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
  108. package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
  109. package/flutter_app/lib/src/recording_payloads.dart +86 -0
  110. package/flutter_app/lib/src/theme/palette.dart +81 -0
  111. package/flutter_app/lib/src/widget_bridge.dart +49 -0
  112. package/flutter_app/linux/CMakeLists.txt +128 -0
  113. package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
  114. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +43 -0
  115. package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
  116. package/flutter_app/linux/flutter/generated_plugins.cmake +31 -0
  117. package/flutter_app/linux/runner/CMakeLists.txt +26 -0
  118. package/flutter_app/linux/runner/main.cc +6 -0
  119. package/flutter_app/linux/runner/my_application.cc +144 -0
  120. package/flutter_app/linux/runner/my_application.h +18 -0
  121. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  122. package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
  123. package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
  124. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +40 -0
  125. package/flutter_app/macos/Podfile +42 -0
  126. package/flutter_app/macos/Podfile.lock +87 -0
  127. package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
  128. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  129. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  130. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  131. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  132. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  133. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  134. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  135. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  136. package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
  137. package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  138. package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
  139. package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
  140. package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
  141. package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
  142. package/flutter_app/macos/Runner/Info.plist +36 -0
  143. package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
  144. package/flutter_app/macos/Runner/Release.entitlements +12 -0
  145. package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
  146. package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  147. package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  148. package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
  149. package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  150. package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
  151. package/flutter_app/patch_strings.py +12 -0
  152. package/flutter_app/pubspec.lock +1088 -0
  153. package/flutter_app/pubspec.yaml +53 -0
  154. package/flutter_app/test/messaging_access_summary_test.dart +22 -0
  155. package/flutter_app/test/recording_payloads_test.dart +53 -0
  156. package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
  157. package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
  158. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
  159. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
  160. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
  161. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
  162. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
  163. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
  164. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
  165. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
  166. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
  167. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
  168. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
  169. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
  170. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
  171. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
  172. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
  173. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
  174. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
  175. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
  176. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
  177. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
  178. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
  179. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
  180. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
  181. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
  182. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
  183. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
  184. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
  185. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
  186. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
  187. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
  188. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
  189. package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
  190. package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
  191. package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
  192. package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
  193. package/flutter_app/tool/generate_desktop_branding.py +219 -0
  194. package/flutter_app/web/favicon.png +0 -0
  195. package/flutter_app/web/favicon.svg +12 -0
  196. package/flutter_app/web/icons/Icon-192.png +0 -0
  197. package/flutter_app/web/icons/Icon-512.png +0 -0
  198. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  199. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  200. package/flutter_app/web/index.html +39 -0
  201. package/flutter_app/web/manifest.json +35 -0
  202. package/flutter_app/windows/CMakeLists.txt +108 -0
  203. package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
  204. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +47 -0
  205. package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
  206. package/flutter_app/windows/flutter/generated_plugins.cmake +35 -0
  207. package/flutter_app/windows/runner/CMakeLists.txt +41 -0
  208. package/flutter_app/windows/runner/Runner.rc +121 -0
  209. package/flutter_app/windows/runner/flutter_window.cpp +533 -0
  210. package/flutter_app/windows/runner/flutter_window.h +37 -0
  211. package/flutter_app/windows/runner/main.cpp +53 -0
  212. package/flutter_app/windows/runner/resource.h +16 -0
  213. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  214. package/flutter_app/windows/runner/runner.exe.manifest +14 -0
  215. package/flutter_app/windows/runner/utils.cpp +65 -0
  216. package/flutter_app/windows/runner/utils.h +19 -0
  217. package/flutter_app/windows/runner/win32_window.cpp +299 -0
  218. package/flutter_app/windows/runner/win32_window.h +102 -0
  219. package/lib/manager.js +231 -7
  220. package/package.json +3 -1
  221. package/server/db/database.js +68 -0
  222. package/server/http/middleware.js +50 -0
  223. package/server/http/routes.js +3 -1
  224. package/server/index.js +1 -0
  225. package/server/public/.last_build_id +1 -1
  226. package/server/public/assets/NOTICES +61 -0
  227. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  228. package/server/public/flutter_bootstrap.js +1 -1
  229. package/server/public/main.dart.js +65262 -64422
  230. package/server/routes/integrations.js +86 -0
  231. package/server/routes/memory.js +11 -2
  232. package/server/routes/screenHistory.js +46 -0
  233. package/server/routes/triggers.js +81 -0
  234. package/server/services/ai/models.js +30 -0
  235. package/server/services/ai/providers/githubCopilot.js +97 -0
  236. package/server/services/ai/providers/openai.js +2 -1
  237. package/server/services/ai/providers/openaiCodex.js +31 -0
  238. package/server/services/ai/settings.js +20 -0
  239. package/server/services/ai/systemPrompt.js +1 -1
  240. package/server/services/ai/tools.js +35 -6
  241. package/server/services/browser/controller.js +47 -3
  242. package/server/services/desktop/screenRecorder.js +172 -0
  243. package/server/services/integrations/env.js +5 -0
  244. package/server/services/integrations/github/common.js +106 -0
  245. package/server/services/integrations/github/provider.js +499 -0
  246. package/server/services/integrations/github/repos.js +1124 -0
  247. package/server/services/integrations/home_assistant/provider.js +306 -26
  248. package/server/services/integrations/manager.js +63 -7
  249. package/server/services/integrations/oauth_provider.js +13 -6
  250. package/server/services/integrations/provider_config_store.js +76 -0
  251. package/server/services/integrations/registry.js +4 -0
  252. package/server/services/integrations/trello/provider.js +744 -0
  253. package/server/services/integrations/whatsapp/provider.js +6 -2
  254. package/server/services/manager.js +22 -0
  255. package/server/services/memory/manager.js +39 -2
  256. package/server/services/skills/base_catalog.js +33 -0
  257. package/server/services/tasks/adapters/index.js +1 -0
  258. package/server/services/tasks/adapters/manual.js +12 -0
  259. package/server/services/tasks/runtime.js +1 -1
  260. package/server/services/voice/openaiClient.js +4 -1
  261. package/server/services/voice/providers.js +2 -1
  262. package/server/services/widgets/service.js +49 -4
  263. package/server/utils/local_secrets.js +56 -0
  264. package/server/utils/logger.js +37 -9
@@ -0,0 +1,689 @@
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
+
11
+ RecordingBridge createPlatformRecordingBridge() => WebRecordingBridge();
12
+
13
+ class WebRecordingBridge extends RecordingBridge {
14
+ RecordingRuntimeStatus _status = const RecordingRuntimeStatus(
15
+ supportsScreenAndMic: true,
16
+ supportsBackgroundMic: false,
17
+ platformLabel: 'Browser screen + microphone recorder',
18
+ );
19
+
20
+ html.MediaStream? _displayStream;
21
+ html.MediaStream? _microphoneStream;
22
+ html.MediaRecorder? _screenRecorder;
23
+ html.MediaRecorder? _microphoneRecorder;
24
+ final Map<String, int> _nextSequenceBySource = <String, int>{};
25
+ final Map<String, int> _lastEndMsBySource = <String, int>{};
26
+ final Map<String, Future<void>> _uploadQueueBySource =
27
+ <String, Future<void>>{};
28
+ Stopwatch? _stopwatch;
29
+ String? _baseUrl;
30
+ String? _sessionId;
31
+ bool _stopping = false;
32
+ StreamSubscription<html.Event>? _displayEndedSub;
33
+ static const Set<String> _safeUploadHeaderKeys = <String>{
34
+ 'content-type',
35
+ 'x-recording-sequence',
36
+ 'x-recording-start-ms',
37
+ 'x-recording-end-ms',
38
+ };
39
+
40
+ @override
41
+ RecordingRuntimeStatus get status => _status;
42
+
43
+ void _log(
44
+ String event, {
45
+ Map<String, Object?> data = const <String, Object?>{},
46
+ Object? error,
47
+ StackTrace? stackTrace,
48
+ }) {
49
+ AppDiagnostics.log(
50
+ 'recording.bridge.web',
51
+ event,
52
+ data: data,
53
+ error: error,
54
+ stackTrace: stackTrace,
55
+ );
56
+ }
57
+
58
+ @override
59
+ Future<void> refreshStatus() async {}
60
+
61
+ @override
62
+ Future<void> startWebRecording({
63
+ required String baseUrl,
64
+ required String sessionId,
65
+ }) async {
66
+ _log(
67
+ 'start_web.request',
68
+ data: <String, Object?>{
69
+ 'sessionId': sessionId,
70
+ 'baseUrl': baseUrl,
71
+ 'alreadyActive': _status.active,
72
+ },
73
+ );
74
+ if (_status.active) {
75
+ throw const RecordingBridgeException(
76
+ 'A recording is already in progress.',
77
+ );
78
+ }
79
+
80
+ try {
81
+ final mediaDevices = html.window.navigator.mediaDevices;
82
+ if (mediaDevices == null) {
83
+ throw const RecordingBridgeException(
84
+ 'This browser does not expose media device APIs.',
85
+ );
86
+ }
87
+
88
+ final displayStream = await js_util.promiseToFuture<html.MediaStream>(
89
+ js_util.callMethod(mediaDevices, 'getDisplayMedia', <Object?>[
90
+ js_util.jsify(<String, Object>{'video': true, 'audio': true}),
91
+ ]),
92
+ );
93
+ if (displayStream.getVideoTracks().isEmpty) {
94
+ throw const RecordingBridgeException(
95
+ 'Screen sharing was cancelled before capture started.',
96
+ );
97
+ }
98
+ if (displayStream.getAudioTracks().isEmpty) {
99
+ displayStream.getTracks().forEach((track) => track.stop());
100
+ throw const RecordingBridgeException(
101
+ 'Screen share did not include audio. Share a tab/window and enable audio.',
102
+ );
103
+ }
104
+
105
+ final microphoneStream = await mediaDevices.getUserMedia(
106
+ <String, dynamic>{
107
+ 'audio': <String, dynamic>{
108
+ 'channelCount': 1,
109
+ 'echoCancellation': false,
110
+ 'noiseSuppression': false,
111
+ 'autoGainControl': false,
112
+ },
113
+ },
114
+ );
115
+ if (microphoneStream.getAudioTracks().isEmpty) {
116
+ displayStream.getTracks().forEach((track) => track.stop());
117
+ throw const RecordingBridgeException(
118
+ 'Microphone permission is required to start recording.',
119
+ );
120
+ }
121
+
122
+ _baseUrl = baseUrl;
123
+ _sessionId = sessionId;
124
+ _displayStream = displayStream;
125
+ _microphoneStream = microphoneStream;
126
+ _nextSequenceBySource
127
+ ..clear()
128
+ ..addAll(<String, int>{'screen': 0, 'microphone': 0});
129
+ _lastEndMsBySource
130
+ ..clear()
131
+ ..addAll(<String, int>{'screen': 0, 'microphone': 0});
132
+ _uploadQueueBySource
133
+ ..clear()
134
+ ..addAll(<String, Future<void>>{
135
+ 'screen': Future<void>.value(),
136
+ 'microphone': Future<void>.value(),
137
+ });
138
+ _stopwatch = Stopwatch()..start();
139
+
140
+ final screenMimeType = _pickMimeType(<String>[
141
+ 'video/webm;codecs=vp9,opus',
142
+ 'video/webm;codecs=vp8,opus',
143
+ 'video/webm',
144
+ ]);
145
+ final micMimeType = _pickMimeType(<String>[
146
+ 'audio/webm;codecs=opus',
147
+ 'audio/webm',
148
+ ]);
149
+
150
+ _screenRecorder = html.MediaRecorder(
151
+ displayStream,
152
+ screenMimeType == null
153
+ ? null
154
+ : <String, String>{'mimeType': screenMimeType},
155
+ );
156
+
157
+ _displayEndedSub = displayStream.getVideoTracks().first.onEnded.listen((
158
+ _,
159
+ ) {
160
+ unawaited(_handleExternalStop());
161
+ });
162
+
163
+ _bindRecorder(
164
+ recorder: _screenRecorder!,
165
+ sourceKey: 'screen',
166
+ mimeType: screenMimeType ?? 'video/webm',
167
+ );
168
+ _microphoneRecorder = html.MediaRecorder(
169
+ microphoneStream,
170
+ micMimeType == null ? null : <String, String>{'mimeType': micMimeType},
171
+ );
172
+ _bindRecorder(
173
+ recorder: _microphoneRecorder!,
174
+ sourceKey: 'microphone',
175
+ mimeType: micMimeType ?? 'audio/webm',
176
+ );
177
+
178
+ _screenRecorder!.start(4000);
179
+ _microphoneRecorder!.start(4000);
180
+ _status = _status.copyWith(
181
+ active: true,
182
+ paused: false,
183
+ sessionId: sessionId,
184
+ startedAt: DateTime.now(),
185
+ errorMessage: null,
186
+ );
187
+ _log(
188
+ 'start_web.done',
189
+ data: <String, Object?>{
190
+ 'sessionId': sessionId,
191
+ 'screenMimeType': screenMimeType ?? 'video/webm',
192
+ 'micMimeType': micMimeType ?? 'audio/webm',
193
+ },
194
+ );
195
+ notifyListeners();
196
+ } catch (error) {
197
+ await _disposeStreams();
198
+ _status = _status.copyWith(
199
+ active: false,
200
+ paused: false,
201
+ sessionId: null,
202
+ startedAt: null,
203
+ errorMessage: error.toString(),
204
+ );
205
+ _log('start_web.failed', error: error);
206
+ notifyListeners();
207
+ rethrow;
208
+ }
209
+ }
210
+
211
+ @override
212
+ Future<void> startWebMicrophoneRecording({
213
+ required String baseUrl,
214
+ required String sessionId,
215
+ }) async {
216
+ _log(
217
+ 'start_web_mic_only.request',
218
+ data: <String, Object?>{
219
+ 'sessionId': sessionId,
220
+ 'baseUrl': baseUrl,
221
+ 'alreadyActive': _status.active,
222
+ },
223
+ );
224
+ if (_status.active) {
225
+ throw const RecordingBridgeException(
226
+ 'A recording is already in progress.',
227
+ );
228
+ }
229
+
230
+ try {
231
+ final mediaDevices = html.window.navigator.mediaDevices;
232
+ if (mediaDevices == null) {
233
+ throw const RecordingBridgeException(
234
+ 'This browser does not expose media device APIs.',
235
+ );
236
+ }
237
+
238
+ final microphoneStream = await mediaDevices.getUserMedia(
239
+ <String, dynamic>{
240
+ 'audio': <String, dynamic>{
241
+ 'channelCount': 1,
242
+ 'echoCancellation': false,
243
+ 'noiseSuppression': false,
244
+ 'autoGainControl': false,
245
+ },
246
+ },
247
+ );
248
+ if (microphoneStream.getAudioTracks().isEmpty) {
249
+ throw const RecordingBridgeException(
250
+ 'Microphone permission is required to start recording.',
251
+ );
252
+ }
253
+
254
+ _baseUrl = baseUrl;
255
+ _sessionId = sessionId;
256
+ _displayStream = null;
257
+ _microphoneStream = microphoneStream;
258
+ _nextSequenceBySource
259
+ ..clear()
260
+ ..addAll(<String, int>{'microphone': 0});
261
+ _lastEndMsBySource
262
+ ..clear()
263
+ ..addAll(<String, int>{'microphone': 0});
264
+ _uploadQueueBySource
265
+ ..clear()
266
+ ..addAll(<String, Future<void>>{
267
+ 'microphone': Future<void>.value(),
268
+ });
269
+ _stopwatch = Stopwatch()..start();
270
+
271
+ final micMimeType = _pickMimeType(<String>[
272
+ 'audio/webm;codecs=opus',
273
+ 'audio/webm',
274
+ ]);
275
+
276
+ _displayEndedSub = null;
277
+ _screenRecorder = null;
278
+ _microphoneRecorder = html.MediaRecorder(
279
+ microphoneStream,
280
+ micMimeType == null ? null : <String, String>{'mimeType': micMimeType},
281
+ );
282
+ _bindRecorder(
283
+ recorder: _microphoneRecorder!,
284
+ sourceKey: 'microphone',
285
+ mimeType: micMimeType ?? 'audio/webm',
286
+ );
287
+
288
+ _microphoneRecorder!.start(4000);
289
+ _status = _status.copyWith(
290
+ active: true,
291
+ paused: false,
292
+ sessionId: sessionId,
293
+ startedAt: DateTime.now(),
294
+ errorMessage: null,
295
+ );
296
+ _log(
297
+ 'start_web_mic_only.done',
298
+ data: <String, Object?>{
299
+ 'sessionId': sessionId,
300
+ 'micMimeType': micMimeType ?? 'audio/webm',
301
+ },
302
+ );
303
+ notifyListeners();
304
+ } catch (error) {
305
+ await _disposeStreams();
306
+ _status = _status.copyWith(
307
+ active: false,
308
+ paused: false,
309
+ sessionId: null,
310
+ startedAt: null,
311
+ errorMessage: error.toString(),
312
+ );
313
+ _log('start_web_mic_only.failed', error: error);
314
+ notifyListeners();
315
+ rethrow;
316
+ }
317
+ }
318
+
319
+ @override
320
+ Future<void> startBackgroundRecording({
321
+ required String baseUrl,
322
+ required String sessionCookie,
323
+ required String sessionId,
324
+ }) async {
325
+ throw const RecordingBridgeException(
326
+ 'Background microphone recording is available on Android only.',
327
+ );
328
+ }
329
+
330
+ @override
331
+ Future<void> startDesktopAudioRecording({
332
+ required String baseUrl,
333
+ required String sessionCookie,
334
+ required String sessionId,
335
+ bool autoShowToolbar = true,
336
+ }) async {
337
+ throw const RecordingBridgeException(
338
+ 'Native desktop audio recording is not available in the browser.',
339
+ );
340
+ }
341
+
342
+ @override
343
+ Future<void> pauseBackgroundRecording() async {
344
+ throw const RecordingBridgeException(
345
+ 'Pause is only available for Android background recording.',
346
+ );
347
+ }
348
+
349
+ @override
350
+ Future<void> pauseDesktopRecording() async {
351
+ throw const RecordingBridgeException(
352
+ 'Desktop recording controls are not available in the browser.',
353
+ );
354
+ }
355
+
356
+ @override
357
+ Future<void> resumeBackgroundRecording() async {
358
+ throw const RecordingBridgeException(
359
+ 'Resume is only available for Android background recording.',
360
+ );
361
+ }
362
+
363
+ @override
364
+ Future<void> resumeDesktopRecording() async {
365
+ throw const RecordingBridgeException(
366
+ 'Desktop recording controls are not available in the browser.',
367
+ );
368
+ }
369
+
370
+ @override
371
+ Future<void> showFloatingToolbar() async {}
372
+
373
+ @override
374
+ Future<void> hideFloatingToolbar() async {}
375
+
376
+ @override
377
+ Future<void> openMicrophoneSettings() async {
378
+ throw const RecordingBridgeException(
379
+ 'Open the browser site settings to manage microphone access.',
380
+ );
381
+ }
382
+
383
+ @override
384
+ Future<void> openSystemAudioSettings() async {
385
+ throw const RecordingBridgeException(
386
+ 'Browser screen-share audio permissions are controlled by the browser.',
387
+ );
388
+ }
389
+
390
+ @override
391
+ Future<void> stopActiveRecording({bool notifyEnded = false}) async {
392
+ if (_stopping || !_status.active) {
393
+ return;
394
+ }
395
+ _stopping = true;
396
+ final sessionId = _sessionId;
397
+ _log(
398
+ 'stop_active.request',
399
+ data: <String, Object?>{
400
+ 'sessionId': sessionId,
401
+ 'notifyEnded': notifyEnded,
402
+ 'active': _status.active,
403
+ },
404
+ );
405
+ try {
406
+ await _stopRecorders();
407
+ await Future.wait(_uploadQueueBySource.values);
408
+ await _disposeStreams();
409
+ _status = _status.copyWith(
410
+ active: false,
411
+ paused: false,
412
+ startedAt: null,
413
+ sessionId: sessionId,
414
+ );
415
+ _log(
416
+ 'stop_active.done',
417
+ data: <String, Object?>{
418
+ 'sessionId': sessionId,
419
+ 'notifyEnded': notifyEnded,
420
+ },
421
+ );
422
+ notifyListeners();
423
+ if (notifyEnded && sessionId != null && onRecordingStopped != null) {
424
+ _log(
425
+ 'stop_active.notify_ended',
426
+ data: <String, Object?>{'sessionId': sessionId},
427
+ );
428
+ await onRecordingStopped!(sessionId);
429
+ }
430
+ } finally {
431
+ _stopping = false;
432
+ }
433
+ }
434
+
435
+ void _bindRecorder({
436
+ required html.MediaRecorder recorder,
437
+ required String sourceKey,
438
+ required String mimeType,
439
+ }) {
440
+ recorder.addEventListener('dataavailable', (html.Event event) {
441
+ final blob = js_util.getProperty<html.Blob?>(event, 'data');
442
+ if (blob == null || blob.size == 0) {
443
+ return;
444
+ }
445
+ final endMs = _stopwatch?.elapsedMilliseconds ?? 0;
446
+ final startMs = _lastEndMsBySource[sourceKey] ?? 0;
447
+ _lastEndMsBySource[sourceKey] = endMs;
448
+ final sequence = _nextSequenceBySource[sourceKey] ?? 0;
449
+ _nextSequenceBySource[sourceKey] = sequence + 1;
450
+ final upload = (_uploadQueueBySource[sourceKey] ?? Future<void>.value())
451
+ .then(
452
+ (_) => _uploadChunk(
453
+ sourceKey: sourceKey,
454
+ sequence: sequence,
455
+ startMs: startMs,
456
+ endMs: endMs,
457
+ blob: blob,
458
+ mimeType: mimeType,
459
+ ),
460
+ );
461
+ _uploadQueueBySource[sourceKey] = upload.catchError((
462
+ Object error,
463
+ StackTrace stackTrace,
464
+ ) {
465
+ final stackText = stackTrace.toString();
466
+ _status = _status.copyWith(
467
+ errorMessage: stackText.isNotEmpty ? stackText : error.toString(),
468
+ );
469
+ _log(
470
+ 'chunk.upload_queue.error',
471
+ data: <String, Object?>{
472
+ 'sourceKey': sourceKey,
473
+ 'sequence': sequence,
474
+ 'stackTrace': stackText,
475
+ },
476
+ error: error,
477
+ stackTrace: stackTrace,
478
+ );
479
+ notifyListeners();
480
+ Error.throwWithStackTrace(error, stackTrace);
481
+ });
482
+ });
483
+ }
484
+
485
+ Future<void> _uploadChunk({
486
+ required String sourceKey,
487
+ required int sequence,
488
+ required int startMs,
489
+ required int endMs,
490
+ required html.Blob blob,
491
+ required String mimeType,
492
+ }) async {
493
+ final sessionId = _sessionId;
494
+ final baseUrl = _baseUrl;
495
+ if (sessionId == null || baseUrl == null) {
496
+ throw const RecordingBridgeException(
497
+ 'Recording session is not initialized.',
498
+ );
499
+ }
500
+ final bytes = await _blobToBytes(blob);
501
+ final uri = _resolveUri(baseUrl, '/api/recordings/$sessionId/chunks');
502
+ _log(
503
+ 'chunk.upload.request',
504
+ data: <String, Object?>{
505
+ 'sessionId': sessionId,
506
+ 'sourceKey': sourceKey,
507
+ 'sequence': sequence,
508
+ 'startMs': startMs,
509
+ 'endMs': endMs,
510
+ 'size': bytes.length,
511
+ 'mimeType': mimeType,
512
+ },
513
+ );
514
+ await _requestWithRetry(
515
+ uri.toString(),
516
+ headers: <String, String>{
517
+ 'Content-Type': mimeType,
518
+ 'X-Recording-Source-Key': sourceKey,
519
+ 'X-Recording-Sequence': '$sequence',
520
+ 'X-Recording-Start-Ms': '$startMs',
521
+ 'X-Recording-End-Ms': '$endMs',
522
+ },
523
+ body: bytes,
524
+ );
525
+ _log(
526
+ 'chunk.upload.done',
527
+ data: <String, Object?>{
528
+ 'sessionId': sessionId,
529
+ 'sourceKey': sourceKey,
530
+ 'sequence': sequence,
531
+ 'size': bytes.length,
532
+ },
533
+ );
534
+ }
535
+
536
+ Future<void> _handleExternalStop() async {
537
+ final sessionId = _sessionId;
538
+ if (sessionId == null) {
539
+ return;
540
+ }
541
+ _log(
542
+ 'external_stop.detected',
543
+ data: <String, Object?>{'sessionId': sessionId},
544
+ );
545
+ await stopActiveRecording(notifyEnded: true);
546
+ }
547
+
548
+ Future<void> _stopRecorders() async {
549
+ final futures = <Future<void>>[];
550
+ if (_screenRecorder != null && _screenRecorder!.state != 'inactive') {
551
+ futures.add(_waitForStop(_screenRecorder!));
552
+ _screenRecorder!.stop();
553
+ }
554
+ if (_microphoneRecorder != null &&
555
+ _microphoneRecorder!.state != 'inactive') {
556
+ futures.add(_waitForStop(_microphoneRecorder!));
557
+ _microphoneRecorder!.stop();
558
+ }
559
+ await Future.wait(futures);
560
+ }
561
+
562
+ Future<void> _disposeStreams() async {
563
+ await _displayEndedSub?.cancel();
564
+ _displayEndedSub = null;
565
+ _displayStream?.getTracks().forEach((track) => track.stop());
566
+ _microphoneStream?.getTracks().forEach((track) => track.stop());
567
+ _displayStream = null;
568
+ _microphoneStream = null;
569
+ _screenRecorder = null;
570
+ _microphoneRecorder = null;
571
+ _stopwatch?.stop();
572
+ _stopwatch = null;
573
+ }
574
+
575
+ Future<void> _requestWithRetry(
576
+ String url, {
577
+ required Map<String, String> headers,
578
+ required Uint8List body,
579
+ }) async {
580
+ Object? lastError;
581
+ for (var attempt = 0; attempt < 3; attempt += 1) {
582
+ try {
583
+ _log(
584
+ 'chunk.upload.attempt',
585
+ data: <String, Object?>{
586
+ 'url': url,
587
+ 'attempt': attempt + 1,
588
+ 'size': body.length,
589
+ 'headers': _filterHeadersForLog(headers),
590
+ },
591
+ );
592
+ await html.HttpRequest.request(
593
+ url,
594
+ method: 'POST',
595
+ sendData: body,
596
+ requestHeaders: headers,
597
+ withCredentials: true,
598
+ );
599
+ return;
600
+ } catch (error) {
601
+ lastError = error;
602
+ _log(
603
+ 'chunk.upload.attempt_failed',
604
+ data: <String, Object?>{'url': url, 'attempt': attempt + 1},
605
+ error: error,
606
+ );
607
+ await Future<void>.delayed(Duration(milliseconds: 400 * (attempt + 1)));
608
+ }
609
+ }
610
+ throw RecordingBridgeException(
611
+ 'Could not upload a recording chunk: ${lastError ?? 'unknown error'}',
612
+ );
613
+ }
614
+
615
+ Map<String, String> _filterHeadersForLog(Map<String, String> headers) {
616
+ final filtered = <String, String>{};
617
+ headers.forEach((key, value) {
618
+ if (_safeUploadHeaderKeys.contains(key.toLowerCase())) {
619
+ filtered[key] = value;
620
+ }
621
+ });
622
+ return filtered;
623
+ }
624
+
625
+ Future<Uint8List> _blobToBytes(html.Blob blob) {
626
+ final completer = Completer<Uint8List>();
627
+ final reader = html.FileReader();
628
+ reader.onError.first.then((_) {
629
+ if (!completer.isCompleted) {
630
+ completer.completeError(
631
+ const RecordingBridgeException(
632
+ 'Could not read recorded browser chunk.',
633
+ ),
634
+ );
635
+ }
636
+ });
637
+ reader.onLoadEnd.first.then((_) {
638
+ if (completer.isCompleted) {
639
+ return;
640
+ }
641
+ final result = reader.result;
642
+ if (result is ByteBuffer) {
643
+ completer.complete(result.asUint8List());
644
+ return;
645
+ }
646
+ if (result is Uint8List) {
647
+ completer.complete(result);
648
+ return;
649
+ }
650
+ completer.completeError(
651
+ const RecordingBridgeException(
652
+ 'Browser returned an unsupported chunk format.',
653
+ ),
654
+ );
655
+ });
656
+ reader.readAsArrayBuffer(blob);
657
+ return completer.future;
658
+ }
659
+
660
+ Future<void> _waitForStop(html.MediaRecorder recorder) {
661
+ final completer = Completer<void>();
662
+ late html.EventListener listener;
663
+ listener = (html.Event _) {
664
+ recorder.removeEventListener('stop', listener);
665
+ if (!completer.isCompleted) {
666
+ completer.complete();
667
+ }
668
+ };
669
+ recorder.addEventListener('stop', listener);
670
+ return completer.future;
671
+ }
672
+
673
+ String? _pickMimeType(List<String> candidates) {
674
+ for (final candidate in candidates) {
675
+ if (html.MediaRecorder.isTypeSupported(candidate)) {
676
+ return candidate;
677
+ }
678
+ }
679
+ return null;
680
+ }
681
+
682
+ Uri _resolveUri(String baseUrl, String path) {
683
+ final trimmed = baseUrl.trim();
684
+ if (trimmed.isEmpty) {
685
+ return Uri.parse(path);
686
+ }
687
+ return Uri.parse(trimmed.replaceFirst(RegExp(r'/$'), '') + path);
688
+ }
689
+ }