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.
- package/.env.example +39 -0
- package/README.md +2 -0
- package/docs/capabilities.md +2 -2
- package/docs/configuration.md +13 -5
- package/docs/integrations.md +4 -1
- package/flutter_app/.metadata +42 -0
- package/flutter_app/README.md +21 -0
- package/flutter_app/analysis_options.yaml +32 -0
- package/flutter_app/android/app/build.gradle.kts +109 -0
- package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
- package/flutter_app/android/app/src/main/AndroidManifest.xml +147 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +747 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
- package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
- package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
- package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
- package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
- package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
- package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
- package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
- package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
- package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
- package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
- package/flutter_app/android/build.gradle.kts +24 -0
- package/flutter_app/android/ci-release.keystore +0 -0
- package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
- package/flutter_app/android/gradle.properties +3 -0
- package/flutter_app/android/key.properties +4 -0
- package/flutter_app/android/settings.gradle.kts +26 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +119 -0
- package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
- package/flutter_app/lib/main.dart +23057 -0
- package/flutter_app/lib/main_app_shell.dart +1682 -0
- package/flutter_app/lib/main_integrations.dart +931 -0
- package/flutter_app/lib/main_launcher.dart +959 -0
- package/flutter_app/lib/main_launcher_entry.dart +5 -0
- package/flutter_app/lib/main_models.dart +3473 -0
- package/flutter_app/lib/main_shared.dart +2861 -0
- package/flutter_app/lib/main_theme.dart +204 -0
- package/flutter_app/lib/main_voice_assistant.dart +831 -0
- package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
- package/flutter_app/lib/src/android_app_installer.dart +22 -0
- package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
- package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
- package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
- package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
- package/flutter_app/lib/src/app_release_updater.dart +511 -0
- package/flutter_app/lib/src/backend_client.dart +1833 -0
- package/flutter_app/lib/src/desktop_companion.dart +2 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
- package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
- package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
- package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
- package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
- package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
- package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
- package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
- package/flutter_app/lib/src/health_bridge.dart +136 -0
- package/flutter_app/lib/src/live_voice_capture.dart +85 -0
- package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
- package/flutter_app/lib/src/network/app_http_client.dart +53 -0
- package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
- package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
- package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
- package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
- package/flutter_app/lib/src/oauth_launcher.dart +33 -0
- package/flutter_app/lib/src/oauth_launcher_io.dart +77 -0
- package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
- package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
- package/flutter_app/lib/src/recording_bridge.dart +232 -0
- package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
- package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
- package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
- package/flutter_app/lib/src/recording_payloads.dart +86 -0
- package/flutter_app/lib/src/theme/palette.dart +81 -0
- package/flutter_app/lib/src/widget_bridge.dart +49 -0
- package/flutter_app/linux/CMakeLists.txt +128 -0
- package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
- package/flutter_app/linux/flutter/generated_plugin_registrant.cc +43 -0
- package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
- package/flutter_app/linux/flutter/generated_plugins.cmake +31 -0
- package/flutter_app/linux/runner/CMakeLists.txt +26 -0
- package/flutter_app/linux/runner/main.cc +6 -0
- package/flutter_app/linux/runner/my_application.cc +144 -0
- package/flutter_app/linux/runner/my_application.h +18 -0
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
- package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +40 -0
- package/flutter_app/macos/Podfile +42 -0
- package/flutter_app/macos/Podfile.lock +87 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
- package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
- package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
- package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
- package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
- package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
- package/flutter_app/macos/Runner/Info.plist +36 -0
- package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
- package/flutter_app/macos/Runner/Release.entitlements +12 -0
- package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
- package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
- package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
- package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
- package/flutter_app/patch_strings.py +12 -0
- package/flutter_app/pubspec.lock +1088 -0
- package/flutter_app/pubspec.yaml +53 -0
- package/flutter_app/test/messaging_access_summary_test.dart +22 -0
- package/flutter_app/test/recording_payloads_test.dart +53 -0
- package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
- package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
- package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
- package/flutter_app/tool/generate_desktop_branding.py +219 -0
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +12 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/web/index.html +39 -0
- package/flutter_app/web/manifest.json +35 -0
- package/flutter_app/windows/CMakeLists.txt +108 -0
- package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.cc +47 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
- package/flutter_app/windows/flutter/generated_plugins.cmake +35 -0
- package/flutter_app/windows/runner/CMakeLists.txt +41 -0
- package/flutter_app/windows/runner/Runner.rc +121 -0
- package/flutter_app/windows/runner/flutter_window.cpp +533 -0
- package/flutter_app/windows/runner/flutter_window.h +37 -0
- package/flutter_app/windows/runner/main.cpp +53 -0
- package/flutter_app/windows/runner/resource.h +16 -0
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/flutter_app/windows/runner/runner.exe.manifest +14 -0
- package/flutter_app/windows/runner/utils.cpp +65 -0
- package/flutter_app/windows/runner/utils.h +19 -0
- package/flutter_app/windows/runner/win32_window.cpp +299 -0
- package/flutter_app/windows/runner/win32_window.h +102 -0
- package/lib/manager.js +231 -7
- package/package.json +3 -1
- package/server/db/database.js +68 -0
- package/server/http/middleware.js +50 -0
- package/server/http/routes.js +3 -1
- package/server/index.js +1 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +61 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +65262 -64422
- package/server/routes/integrations.js +86 -0
- package/server/routes/memory.js +11 -2
- package/server/routes/screenHistory.js +46 -0
- package/server/routes/triggers.js +81 -0
- package/server/services/ai/models.js +30 -0
- package/server/services/ai/providers/githubCopilot.js +97 -0
- package/server/services/ai/providers/openai.js +2 -1
- package/server/services/ai/providers/openaiCodex.js +31 -0
- package/server/services/ai/settings.js +20 -0
- package/server/services/ai/systemPrompt.js +1 -1
- package/server/services/ai/tools.js +35 -6
- package/server/services/browser/controller.js +47 -3
- package/server/services/desktop/screenRecorder.js +172 -0
- package/server/services/integrations/env.js +5 -0
- package/server/services/integrations/github/common.js +106 -0
- package/server/services/integrations/github/provider.js +499 -0
- package/server/services/integrations/github/repos.js +1124 -0
- package/server/services/integrations/home_assistant/provider.js +306 -26
- package/server/services/integrations/manager.js +63 -7
- package/server/services/integrations/oauth_provider.js +13 -6
- package/server/services/integrations/provider_config_store.js +76 -0
- package/server/services/integrations/registry.js +4 -0
- package/server/services/integrations/trello/provider.js +744 -0
- package/server/services/integrations/whatsapp/provider.js +6 -2
- package/server/services/manager.js +22 -0
- package/server/services/memory/manager.js +39 -2
- package/server/services/skills/base_catalog.js +33 -0
- package/server/services/tasks/adapters/index.js +1 -0
- package/server/services/tasks/adapters/manual.js +12 -0
- package/server/services/tasks/runtime.js +1 -1
- package/server/services/voice/openaiClient.js +4 -1
- package/server/services/voice/providers.js +2 -1
- package/server/services/widgets/service.js +49 -4
- package/server/utils/local_secrets.js +56 -0
- package/server/utils/logger.js +37 -9
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
import Cocoa
|
|
2
|
+
import FlutterMacOS
|
|
3
|
+
import AVFoundation
|
|
4
|
+
@preconcurrency import ScreenCaptureKit
|
|
5
|
+
|
|
6
|
+
// MARK: - Error Types
|
|
7
|
+
enum CaptureError: Error {
|
|
8
|
+
case noPermission
|
|
9
|
+
case noDisplay
|
|
10
|
+
case alreadyCapturing
|
|
11
|
+
case notCapturing
|
|
12
|
+
case invalidConfiguration(String)
|
|
13
|
+
case streamCreationFailed
|
|
14
|
+
case captureStartFailed(Error)
|
|
15
|
+
case captureStopFailed(Error)
|
|
16
|
+
|
|
17
|
+
var message: String {
|
|
18
|
+
switch self {
|
|
19
|
+
case .noPermission:
|
|
20
|
+
return "Screen recording permission not granted"
|
|
21
|
+
case .noDisplay:
|
|
22
|
+
return "No display found"
|
|
23
|
+
case .alreadyCapturing:
|
|
24
|
+
return "Capture already in progress"
|
|
25
|
+
case .notCapturing:
|
|
26
|
+
return "No active capture session"
|
|
27
|
+
case .invalidConfiguration(let detail):
|
|
28
|
+
return "Invalid configuration: \(detail)"
|
|
29
|
+
case .streamCreationFailed:
|
|
30
|
+
return "Failed to create capture stream"
|
|
31
|
+
case .captureStartFailed(let error):
|
|
32
|
+
return "Failed to start capture: \(error.localizedDescription)"
|
|
33
|
+
case .captureStopFailed(let error):
|
|
34
|
+
return "Failed to stop capture: \(error.localizedDescription)"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// MARK: - Audio Configuration
|
|
40
|
+
struct AudioConfiguration {
|
|
41
|
+
let sampleRate: Double
|
|
42
|
+
let channelCount: Int
|
|
43
|
+
|
|
44
|
+
static let `default` = AudioConfiguration(sampleRate: 16000, channelCount: 1)
|
|
45
|
+
|
|
46
|
+
static func from(_ dict: [String: Any]?) -> Result<AudioConfiguration, CaptureError> {
|
|
47
|
+
guard let dict = dict else {
|
|
48
|
+
return .success(.default)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let sampleRate = (dict["sampleRate"] as? NSNumber)?.doubleValue ?? 16000
|
|
52
|
+
let channelCount = (dict["channels"] as? NSNumber)?.intValue ?? 1
|
|
53
|
+
|
|
54
|
+
// Validate
|
|
55
|
+
guard [8000, 16000, 44100, 48000].contains(Int(sampleRate)) else {
|
|
56
|
+
return .failure(.invalidConfiguration("Sample rate must be 8000, 16000, 44100, or 48000"))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
guard (1...2).contains(channelCount) else {
|
|
60
|
+
return .failure(.invalidConfiguration("Channel count must be 1 or 2"))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return .success(AudioConfiguration(sampleRate: sampleRate, channelCount: channelCount))
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// MARK: - Main Plugin
|
|
68
|
+
@available(macOS 13.0, *)
|
|
69
|
+
final class SystemCapturePlugin: NSObject, FlutterPlugin, @unchecked Sendable {
|
|
70
|
+
private var methodChannel: FlutterMethodChannel?
|
|
71
|
+
private var eventChannel: FlutterEventChannel?
|
|
72
|
+
private var eventSink: FlutterEventSink?
|
|
73
|
+
|
|
74
|
+
private var statusEventChannel: FlutterEventChannel?
|
|
75
|
+
var statusEventSink: FlutterEventSink? // Changed to var for access from handler
|
|
76
|
+
|
|
77
|
+
private var decibelEventChannel: FlutterEventChannel?
|
|
78
|
+
var decibelEventSink: FlutterEventSink?
|
|
79
|
+
|
|
80
|
+
private var stream: SCStream?
|
|
81
|
+
var streamOutput: StreamOutput? // Internal access for stream handlers
|
|
82
|
+
|
|
83
|
+
// Thread-safe state management using serial queue
|
|
84
|
+
private let stateQueue = DispatchQueue(label: "com.system_audio_transcriber.state_queue", qos: .utility)
|
|
85
|
+
private var _isCapturing = false
|
|
86
|
+
var isCapturing: Bool {
|
|
87
|
+
get {
|
|
88
|
+
return stateQueue.sync { _isCapturing }
|
|
89
|
+
}
|
|
90
|
+
set {
|
|
91
|
+
stateQueue.async { [weak self] in
|
|
92
|
+
self?._isCapturing = newValue
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private let captureQueue = DispatchQueue(label: "com.system_audio_transcriber.capture_queue", qos: .userInitiated)
|
|
98
|
+
|
|
99
|
+
static func register(with registrar: FlutterPluginRegistrar) {
|
|
100
|
+
let instance = SystemCapturePlugin()
|
|
101
|
+
|
|
102
|
+
let methodChannel = FlutterMethodChannel(
|
|
103
|
+
name: "com.system_audio_transcriber/audio_capture",
|
|
104
|
+
binaryMessenger: registrar.messenger
|
|
105
|
+
)
|
|
106
|
+
instance.methodChannel = methodChannel
|
|
107
|
+
registrar.addMethodCallDelegate(instance, channel: methodChannel)
|
|
108
|
+
|
|
109
|
+
let eventChannel = FlutterEventChannel(
|
|
110
|
+
name: "com.system_audio_transcriber/audio_stream",
|
|
111
|
+
binaryMessenger: registrar.messenger
|
|
112
|
+
)
|
|
113
|
+
instance.eventChannel = eventChannel
|
|
114
|
+
eventChannel.setStreamHandler(instance)
|
|
115
|
+
|
|
116
|
+
let statusEventChannel = FlutterEventChannel(
|
|
117
|
+
name: "com.system_audio_transcriber/audio_status",
|
|
118
|
+
binaryMessenger: registrar.messenger
|
|
119
|
+
)
|
|
120
|
+
instance.statusEventChannel = statusEventChannel
|
|
121
|
+
statusEventChannel.setStreamHandler(SystemStatusStreamHandler(plugin: instance))
|
|
122
|
+
|
|
123
|
+
let decibelEventChannel = FlutterEventChannel(
|
|
124
|
+
name: "com.system_audio_transcriber/audio_decibel",
|
|
125
|
+
binaryMessenger: registrar.messenger
|
|
126
|
+
)
|
|
127
|
+
instance.decibelEventChannel = decibelEventChannel
|
|
128
|
+
decibelEventChannel.setStreamHandler(SystemDecibelStreamHandler(plugin: instance))
|
|
129
|
+
|
|
130
|
+
// Register for app termination to cleanup
|
|
131
|
+
NotificationCenter.default.addObserver(
|
|
132
|
+
instance,
|
|
133
|
+
selector: #selector(instance.applicationWillTerminate),
|
|
134
|
+
name: NSApplication.willTerminateNotification,
|
|
135
|
+
object: nil
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@objc private func applicationWillTerminate() {
|
|
140
|
+
Task {
|
|
141
|
+
await cleanupResources()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
deinit {
|
|
146
|
+
NotificationCenter.default.removeObserver(self)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
150
|
+
switch call.method {
|
|
151
|
+
|
|
152
|
+
case "requestPermissions":
|
|
153
|
+
requestPermissions(result: result)
|
|
154
|
+
|
|
155
|
+
case "startCapture":
|
|
156
|
+
let config = call.arguments as? [String: Any]
|
|
157
|
+
Task {
|
|
158
|
+
await startCapture(config: config, result: result)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
case "stopCapture":
|
|
162
|
+
Task {
|
|
163
|
+
await stopCapture(result: result)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
default:
|
|
167
|
+
result(FlutterMethodNotImplemented)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private func requestPermissions(result: @escaping FlutterResult) {
|
|
172
|
+
let hasPermission = CGPreflightScreenCaptureAccess()
|
|
173
|
+
|
|
174
|
+
if hasPermission {
|
|
175
|
+
result(true)
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let granted = CGRequestScreenCaptureAccess()
|
|
180
|
+
|
|
181
|
+
if granted {
|
|
182
|
+
result(true)
|
|
183
|
+
} else {
|
|
184
|
+
DispatchQueue.main.async { [weak self] in
|
|
185
|
+
self?.showPermissionAlert()
|
|
186
|
+
}
|
|
187
|
+
result(false)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private func showPermissionAlert() {
|
|
192
|
+
let alert = NSAlert()
|
|
193
|
+
alert.messageText = "Screen Recording Permission Required"
|
|
194
|
+
alert.informativeText = """
|
|
195
|
+
This app needs Screen Recording permission to capture system audio.
|
|
196
|
+
|
|
197
|
+
Please follow these steps:
|
|
198
|
+
1. Click "Open System Settings" below
|
|
199
|
+
2. In Privacy & Security → Screen Recording
|
|
200
|
+
3. Enable the toggle for this app
|
|
201
|
+
4. Restart the app
|
|
202
|
+
"""
|
|
203
|
+
alert.alertStyle = .warning
|
|
204
|
+
alert.addButton(withTitle: "Open System Settings")
|
|
205
|
+
alert.addButton(withTitle: "Cancel")
|
|
206
|
+
|
|
207
|
+
let response = alert.runModal()
|
|
208
|
+
if response == .alertFirstButtonReturn {
|
|
209
|
+
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture") {
|
|
210
|
+
NSWorkspace.shared.open(url)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private func startCapture(config: [String: Any]?, result: @escaping FlutterResult) async {
|
|
216
|
+
// Check if already capturing
|
|
217
|
+
if isCapturing {
|
|
218
|
+
print("⚠️ Already capturing, stopping first...")
|
|
219
|
+
await cleanupResources()
|
|
220
|
+
// Wait for cleanup
|
|
221
|
+
try? await Task.sleep(nanoseconds: 100_000_000)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check permission
|
|
225
|
+
guard CGPreflightScreenCaptureAccess() else {
|
|
226
|
+
print("❌ No screen recording permission")
|
|
227
|
+
let errorMessage = CaptureError.noPermission.message
|
|
228
|
+
DispatchQueue.main.async { [weak self] in
|
|
229
|
+
self?.showPermissionAlert()
|
|
230
|
+
result(FlutterError(
|
|
231
|
+
code: "NO_PERMISSION",
|
|
232
|
+
message: errorMessage,
|
|
233
|
+
details: nil
|
|
234
|
+
))
|
|
235
|
+
}
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Parse and validate configuration
|
|
240
|
+
let audioConfig: AudioConfiguration
|
|
241
|
+
switch AudioConfiguration.from(config) {
|
|
242
|
+
case .success(let cfg):
|
|
243
|
+
audioConfig = cfg
|
|
244
|
+
case .failure(let error):
|
|
245
|
+
DispatchQueue.main.async {
|
|
246
|
+
result(FlutterError(
|
|
247
|
+
code: "INVALID_CONFIG",
|
|
248
|
+
message: error.message,
|
|
249
|
+
details: nil
|
|
250
|
+
))
|
|
251
|
+
}
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
do {
|
|
256
|
+
print("🎬 Starting capture with config: \(audioConfig.sampleRate)Hz, \(audioConfig.channelCount)ch")
|
|
257
|
+
|
|
258
|
+
// Get shareable content
|
|
259
|
+
let availableContent = try await SCShareableContent.excludingDesktopWindows(
|
|
260
|
+
false,
|
|
261
|
+
onScreenWindowsOnly: true
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
guard let display = availableContent.displays.first else {
|
|
265
|
+
throw CaptureError.noDisplay
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
print("📺 Display: \(display.displayID)")
|
|
269
|
+
|
|
270
|
+
// Configure stream
|
|
271
|
+
let configuration = SCStreamConfiguration()
|
|
272
|
+
configuration.capturesAudio = true
|
|
273
|
+
configuration.sampleRate = Int(audioConfig.sampleRate)
|
|
274
|
+
configuration.channelCount = audioConfig.channelCount
|
|
275
|
+
configuration.excludesCurrentProcessAudio = true
|
|
276
|
+
|
|
277
|
+
// Video settings - minimal to reduce overhead
|
|
278
|
+
// ScreenCaptureKit requires video output even for audio-only capture
|
|
279
|
+
configuration.width = 100
|
|
280
|
+
configuration.height = 100
|
|
281
|
+
configuration.minimumFrameInterval = CMTime(value: 1, timescale: 1) // 1 FPS
|
|
282
|
+
configuration.queueDepth = 3
|
|
283
|
+
configuration.pixelFormat = kCVPixelFormatType_32BGRA
|
|
284
|
+
configuration.showsCursor = false
|
|
285
|
+
|
|
286
|
+
// Create filter and stream
|
|
287
|
+
let filter = SCContentFilter(display: display, excludingWindows: [])
|
|
288
|
+
let newStreamOutput = StreamOutput(eventSink: eventSink, decibelEventSink: decibelEventSink)
|
|
289
|
+
let newStream = SCStream(filter: filter, configuration: configuration, delegate: nil)
|
|
290
|
+
let stream = newStream
|
|
291
|
+
|
|
292
|
+
// Add output handlers
|
|
293
|
+
// Note: macOS 13.x requires both audio and video handlers even if video is minimal
|
|
294
|
+
try stream.addStreamOutput(newStreamOutput, type: .audio, sampleHandlerQueue: .main)
|
|
295
|
+
|
|
296
|
+
// Only add video handler if not explicitly disabled (macOS 13.x compatibility)
|
|
297
|
+
if #available(macOS 14.0, *) {
|
|
298
|
+
// No video handler needed on macOS 14+ when capturesVideo = false
|
|
299
|
+
} else {
|
|
300
|
+
try stream.addStreamOutput(newStreamOutput, type: .screen, sampleHandlerQueue: .main)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Start capture
|
|
304
|
+
try await stream.startCapture()
|
|
305
|
+
|
|
306
|
+
// Update state atomically on state queue
|
|
307
|
+
// Use sync to avoid Sendable capture issues - we're already on async context
|
|
308
|
+
stateQueue.sync { [weak self] in
|
|
309
|
+
guard let self = self else { return }
|
|
310
|
+
self.stream = stream
|
|
311
|
+
self.streamOutput = newStreamOutput
|
|
312
|
+
self._isCapturing = true
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Notify status change
|
|
316
|
+
sendStatusUpdate(isActive: true)
|
|
317
|
+
|
|
318
|
+
print("✅ Capture started successfully")
|
|
319
|
+
DispatchQueue.main.async {
|
|
320
|
+
result(true)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
} catch let error as CaptureError {
|
|
324
|
+
print("❌ Capture error: \(error.message)")
|
|
325
|
+
await cleanupResources()
|
|
326
|
+
DispatchQueue.main.async {
|
|
327
|
+
result(FlutterError(
|
|
328
|
+
code: "CAPTURE_ERROR",
|
|
329
|
+
message: error.message,
|
|
330
|
+
details: nil
|
|
331
|
+
))
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
print("❌ Unexpected error: \(error)")
|
|
335
|
+
await cleanupResources()
|
|
336
|
+
DispatchQueue.main.async {
|
|
337
|
+
result(FlutterError(
|
|
338
|
+
code: "CAPTURE_ERROR",
|
|
339
|
+
message: CaptureError.captureStartFailed(error).message,
|
|
340
|
+
details: "\(error)"
|
|
341
|
+
))
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private func stopCapture(result: @escaping FlutterResult) async {
|
|
347
|
+
guard isCapturing else {
|
|
348
|
+
DispatchQueue.main.async {
|
|
349
|
+
result(FlutterError(
|
|
350
|
+
code: "NOT_CAPTURING",
|
|
351
|
+
message: CaptureError.notCapturing.message,
|
|
352
|
+
details: nil
|
|
353
|
+
))
|
|
354
|
+
}
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
await cleanupResources()
|
|
359
|
+
|
|
360
|
+
print("✅ Capture stopped")
|
|
361
|
+
DispatchQueue.main.async {
|
|
362
|
+
result(true)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Centralized cleanup - idempotent and thread-safe
|
|
367
|
+
private func cleanupResources() async {
|
|
368
|
+
// Get current stream and output atomically
|
|
369
|
+
let (currentStream, currentOutput) = await withCheckedContinuation { (continuation: CheckedContinuation<(SCStream?, StreamOutput?), Never>) in
|
|
370
|
+
stateQueue.async { [weak self] in
|
|
371
|
+
guard let self = self else {
|
|
372
|
+
continuation.resume(returning: (nil, nil))
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
continuation.resume(returning: (self.stream, self.streamOutput))
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
guard let stream = currentStream else {
|
|
380
|
+
// Already cleaned up
|
|
381
|
+
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
|
|
382
|
+
stateQueue.async { [weak self] in
|
|
383
|
+
self?._isCapturing = false
|
|
384
|
+
continuation.resume()
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
do {
|
|
391
|
+
// Remove outputs
|
|
392
|
+
if let output = currentOutput {
|
|
393
|
+
try stream.removeStreamOutput(output, type: .audio)
|
|
394
|
+
|
|
395
|
+
// Only remove video handler if it was added (macOS 13.x)
|
|
396
|
+
if #available(macOS 14.0, *) {
|
|
397
|
+
// No video handler to remove
|
|
398
|
+
} else {
|
|
399
|
+
try stream.removeStreamOutput(output, type: .screen)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Stop stream
|
|
404
|
+
try await stream.stopCapture()
|
|
405
|
+
|
|
406
|
+
// Small delay for graceful shutdown
|
|
407
|
+
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
408
|
+
|
|
409
|
+
} catch {
|
|
410
|
+
print("⚠️ Cleanup error: \(error.localizedDescription)")
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Clear state atomically
|
|
414
|
+
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
|
|
415
|
+
stateQueue.async { [weak self] in
|
|
416
|
+
guard let self = self else {
|
|
417
|
+
continuation.resume()
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
self.stream = nil
|
|
421
|
+
self.streamOutput = nil
|
|
422
|
+
self._isCapturing = false
|
|
423
|
+
continuation.resume()
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Notify status change
|
|
428
|
+
sendStatusUpdate(isActive: false)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private func sendStatusUpdate(isActive: Bool) {
|
|
432
|
+
DispatchQueue.main.async { [weak self] in
|
|
433
|
+
self?.statusEventSink?([
|
|
434
|
+
"isActive": isActive,
|
|
435
|
+
"timestamp": Date().timeIntervalSince1970
|
|
436
|
+
])
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// MARK: - FlutterStreamHandler
|
|
442
|
+
@available(macOS 13.0, *)
|
|
443
|
+
extension SystemCapturePlugin: FlutterStreamHandler {
|
|
444
|
+
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
|
|
445
|
+
self.eventSink = events
|
|
446
|
+
streamOutput?.eventSink = events
|
|
447
|
+
return nil
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
|
451
|
+
self.eventSink = nil
|
|
452
|
+
streamOutput?.eventSink = nil
|
|
453
|
+
return nil
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// MARK: - Stream Output Handler
|
|
458
|
+
@available(macOS 13.0, *)
|
|
459
|
+
final class StreamOutput: NSObject, SCStreamOutput, @unchecked Sendable {
|
|
460
|
+
var eventSink: FlutterEventSink?
|
|
461
|
+
var decibelEventSink: FlutterEventSink?
|
|
462
|
+
private static var hasLoggedFormat = false
|
|
463
|
+
private let processingQueue = DispatchQueue(label: "com.system_audio_transcriber.processing", qos: .userInitiated)
|
|
464
|
+
|
|
465
|
+
init(eventSink: FlutterEventSink?, decibelEventSink: FlutterEventSink? = nil) {
|
|
466
|
+
self.eventSink = eventSink
|
|
467
|
+
self.decibelEventSink = decibelEventSink
|
|
468
|
+
super.init()
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) {
|
|
472
|
+
// Only process audio, ignore video frames
|
|
473
|
+
guard type == .audio else { return }
|
|
474
|
+
|
|
475
|
+
// Process on background queue
|
|
476
|
+
processingQueue.async { [weak self] in
|
|
477
|
+
guard let self = self else { return }
|
|
478
|
+
|
|
479
|
+
guard let audioData = self.extractAudioData(from: sampleBuffer) else {
|
|
480
|
+
return
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Calculate decibel from audio data
|
|
484
|
+
let decibel = self.calculateDecibel(from: audioData)
|
|
485
|
+
|
|
486
|
+
// Send to Flutter on main thread
|
|
487
|
+
DispatchQueue.main.async { [weak self] in
|
|
488
|
+
if let sink = self?.eventSink {
|
|
489
|
+
sink(FlutterStandardTypedData(bytes: audioData))
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Send decibel data
|
|
493
|
+
if let decibelSink = self?.decibelEventSink {
|
|
494
|
+
decibelSink([
|
|
495
|
+
"decibel": decibel,
|
|
496
|
+
"timestamp": Date().timeIntervalSince1970
|
|
497
|
+
])
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private func extractAudioData(from sampleBuffer: CMSampleBuffer) -> Data? {
|
|
504
|
+
guard let blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer),
|
|
505
|
+
let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer),
|
|
506
|
+
let audioStreamBasicDescription = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription) else {
|
|
507
|
+
return nil
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Log format once
|
|
511
|
+
if !StreamOutput.hasLoggedFormat {
|
|
512
|
+
StreamOutput.hasLoggedFormat = true
|
|
513
|
+
let desc = audioStreamBasicDescription.pointee
|
|
514
|
+
print("🎤 Audio Format:")
|
|
515
|
+
print(" Sample Rate: \(desc.mSampleRate) Hz")
|
|
516
|
+
print(" Channels: \(desc.mChannelsPerFrame)")
|
|
517
|
+
print(" Bits/Channel: \(desc.mBitsPerChannel)")
|
|
518
|
+
print(" Format ID: \(desc.mFormatID)")
|
|
519
|
+
print(" Format Flags: \(desc.mFormatFlags)")
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
var length: Int = 0
|
|
523
|
+
var dataPointer: UnsafeMutablePointer<Int8>?
|
|
524
|
+
|
|
525
|
+
let status = CMBlockBufferGetDataPointer(
|
|
526
|
+
blockBuffer,
|
|
527
|
+
atOffset: 0,
|
|
528
|
+
lengthAtOffsetOut: nil,
|
|
529
|
+
totalLengthOut: &length,
|
|
530
|
+
dataPointerOut: &dataPointer
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
guard status == kCMBlockBufferNoErr, let pointer = dataPointer else {
|
|
534
|
+
return nil
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
let desc = audioStreamBasicDescription.pointee
|
|
538
|
+
|
|
539
|
+
// Float32 to Int16 conversion
|
|
540
|
+
if desc.mFormatID == kAudioFormatLinearPCM && desc.mFormatFlags & kAudioFormatFlagIsFloat != 0 {
|
|
541
|
+
return convertFloat32ToInt16(pointer: pointer, length: length)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Already Int16
|
|
545
|
+
if desc.mFormatID == kAudioFormatLinearPCM && desc.mBitsPerChannel == 16 {
|
|
546
|
+
return Data(bytes: pointer, count: length)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
print("⚠️ Unsupported audio format: \(desc.mFormatID)")
|
|
550
|
+
return nil
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private func convertFloat32ToInt16(pointer: UnsafeMutablePointer<Int8>, length: Int) -> Data {
|
|
554
|
+
let floatPointer = pointer.withMemoryRebound(to: Float32.self, capacity: length / MemoryLayout<Float32>.size) { $0 }
|
|
555
|
+
let sampleCount = length / MemoryLayout<Float32>.size
|
|
556
|
+
|
|
557
|
+
var int16Data = Data(capacity: sampleCount * MemoryLayout<Int16>.size)
|
|
558
|
+
|
|
559
|
+
for i in 0..<sampleCount {
|
|
560
|
+
// Clamp and convert
|
|
561
|
+
let sample = min(max(floatPointer[i], -1.0), 1.0)
|
|
562
|
+
let int16Sample = Int16(sample * 32767.0)
|
|
563
|
+
withUnsafeBytes(of: int16Sample) { int16Data.append(contentsOf: $0) }
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return int16Data
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/// Calculate decibel (dB) from Int16 PCM audio data
|
|
570
|
+
/// Returns RMS-based decibel value, typically ranges from -∞ to 0 dB
|
|
571
|
+
private func calculateDecibel(from audioData: Data) -> Double {
|
|
572
|
+
guard audioData.count >= 2 else { return -120.0 } // Silence threshold
|
|
573
|
+
|
|
574
|
+
// Convert Data to Int16 array
|
|
575
|
+
let sampleCount = audioData.count / MemoryLayout<Int16>.size
|
|
576
|
+
var samples: [Int16] = []
|
|
577
|
+
samples.reserveCapacity(sampleCount)
|
|
578
|
+
|
|
579
|
+
audioData.withUnsafeBytes { bytes in
|
|
580
|
+
let int16Pointer = bytes.bindMemory(to: Int16.self)
|
|
581
|
+
for i in 0..<sampleCount {
|
|
582
|
+
samples.append(int16Pointer[i])
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
guard !samples.isEmpty else { return -120.0 }
|
|
587
|
+
|
|
588
|
+
// Calculate RMS (Root Mean Square)
|
|
589
|
+
let sumOfSquares = samples.reduce(0.0) { sum, sample in
|
|
590
|
+
let value = Double(sample)
|
|
591
|
+
return sum + (value * value)
|
|
592
|
+
}
|
|
593
|
+
let meanSquare = sumOfSquares / Double(samples.count)
|
|
594
|
+
let rms = sqrt(meanSquare)
|
|
595
|
+
|
|
596
|
+
// Calculate decibel: dB = 20 * log10(RMS / max_value)
|
|
597
|
+
// For Int16, max_value is 32767.0
|
|
598
|
+
let maxValue = 32767.0
|
|
599
|
+
guard rms > 0 else { return -120.0 } // Avoid log(0)
|
|
600
|
+
|
|
601
|
+
let decibel = 20.0 * log10(rms / maxValue)
|
|
602
|
+
|
|
603
|
+
// Clamp to reasonable range (-120 dB to 0 dB)
|
|
604
|
+
return max(-120.0, min(0.0, decibel))
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// MARK: - System Status Stream Handler
|
|
609
|
+
@available(macOS 13.0, *)
|
|
610
|
+
class SystemStatusStreamHandler: NSObject, FlutterStreamHandler {
|
|
611
|
+
weak var plugin: SystemCapturePlugin?
|
|
612
|
+
|
|
613
|
+
init(plugin: SystemCapturePlugin) {
|
|
614
|
+
self.plugin = plugin
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
|
|
618
|
+
plugin?.statusEventSink = events
|
|
619
|
+
// Send current status immediately
|
|
620
|
+
let isActive = plugin?.isCapturing ?? false
|
|
621
|
+
events([
|
|
622
|
+
"isActive": isActive,
|
|
623
|
+
"timestamp": Date().timeIntervalSince1970
|
|
624
|
+
])
|
|
625
|
+
return nil
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
|
629
|
+
plugin?.statusEventSink = nil
|
|
630
|
+
return nil
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// MARK: - System Decibel Stream Handler
|
|
635
|
+
@available(macOS 13.0, *)
|
|
636
|
+
class SystemDecibelStreamHandler: NSObject, FlutterStreamHandler {
|
|
637
|
+
weak var plugin: SystemCapturePlugin?
|
|
638
|
+
|
|
639
|
+
init(plugin: SystemCapturePlugin) {
|
|
640
|
+
self.plugin = plugin
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
|
|
644
|
+
plugin?.decibelEventSink = events
|
|
645
|
+
// Update StreamOutput with decibel sink
|
|
646
|
+
plugin?.streamOutput?.decibelEventSink = events
|
|
647
|
+
return nil
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
|
651
|
+
plugin?.decibelEventSink = nil
|
|
652
|
+
plugin?.streamOutput?.decibelEventSink = nil
|
|
653
|
+
return nil
|
|
654
|
+
}
|
|
655
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>NSPrivacyTrackingDomains</key>
|
|
6
|
+
<array/>
|
|
7
|
+
<key>NSPrivacyCollectedDataTypes</key>
|
|
8
|
+
<array/>
|
|
9
|
+
<key>NSPrivacyTracking</key>
|
|
10
|
+
<false/>
|
|
11
|
+
</dict>
|
|
12
|
+
</plist>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#
|
|
2
|
+
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
|
|
3
|
+
# Run `pod lib lint desktop_audio_capture.podspec` to validate before publishing.
|
|
4
|
+
#
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'desktop_audio_capture'
|
|
7
|
+
s.version = '0.0.1'
|
|
8
|
+
s.summary = 'A new Flutter plugin project.'
|
|
9
|
+
s.description = <<-DESC
|
|
10
|
+
A new Flutter plugin project.
|
|
11
|
+
DESC
|
|
12
|
+
s.homepage = 'http://example.com'
|
|
13
|
+
s.license = { :file => '../LICENSE' }
|
|
14
|
+
s.author = { 'Your Company' => 'email@example.com' }
|
|
15
|
+
|
|
16
|
+
s.source = { :path => '.' }
|
|
17
|
+
s.source_files = 'Classes/**/*'
|
|
18
|
+
|
|
19
|
+
# If your plugin requires a privacy manifest, for example if it collects user
|
|
20
|
+
# data, update the PrivacyInfo.xcprivacy file to describe your plugin's
|
|
21
|
+
# privacy impact, and then uncomment this line. For more information,
|
|
22
|
+
# see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
|
|
23
|
+
# s.resource_bundles = {'desktop_audio_capture_privacy' => ['Resources/PrivacyInfo.xcprivacy']}
|
|
24
|
+
|
|
25
|
+
s.dependency 'FlutterMacOS'
|
|
26
|
+
|
|
27
|
+
s.platform = :osx, '13.0'
|
|
28
|
+
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
|
|
29
|
+
s.swift_version = '5.0'
|
|
30
|
+
end
|